From 808d5a61b3d6b9a2d3cb565c87cc611d8cc03218 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 31 Mar 2026 16:07:29 +0000 Subject: [PATCH] docs: Add comprehensive agentic design documentation (2.5-hour learning session) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 docs/agentic-design/ 教育文档,全中文+英文技术术语,覆盖: - README.md: 总索引,整体架构图,阅读指南 - 00-codebase-tour.md: 代码库全景,递归覆盖所有重要目录 - 01-agent-loop.md: Query loop 状态机,token budget,compaction - 02-tool-system.md: Tool interface,权限检查,并发执行,sibling abort - 03-multi-agent-coordination.md: Agent 类型,coordinator 4 阶段,XML 通信 - 04-permission-system.md: Permission mode,rule system,YOLO 分类器,denial tracking - 05-context-and-memory.md: System prompt 分层,compaction,auto-dream 后台巩固 - 06-feature-gating.md: Compile-time feature,runtime flags,Bun dead-code elimination 总计 ~2.5 小时阅读,每篇含 Design Decision 专栏和常见误解纠正 https://claude.ai/code/session_017Vdqo9B8eTXiEDcqnv9DxM --- docs/agentic-design/00-codebase-tour.md | 486 ++++++++++++++++++ docs/agentic-design/01-agent-loop.md | 279 ++++++++++ docs/agentic-design/02-tool-system.md | 378 ++++++++++++++ .../03-multi-agent-coordination.md | 351 +++++++++++++ docs/agentic-design/04-permission-system.md | 317 ++++++++++++ docs/agentic-design/05-context-and-memory.md | 416 +++++++++++++++ docs/agentic-design/06-feature-gating.md | 357 +++++++++++++ docs/agentic-design/README.md | 113 ++++ 8 files changed, 2697 insertions(+) create mode 100644 docs/agentic-design/00-codebase-tour.md create mode 100644 docs/agentic-design/01-agent-loop.md create mode 100644 docs/agentic-design/02-tool-system.md create mode 100644 docs/agentic-design/03-multi-agent-coordination.md create mode 100644 docs/agentic-design/04-permission-system.md create mode 100644 docs/agentic-design/05-context-and-memory.md create mode 100644 docs/agentic-design/06-feature-gating.md create mode 100644 docs/agentic-design/README.md diff --git a/docs/agentic-design/00-codebase-tour.md b/docs/agentic-design/00-codebase-tour.md new file mode 100644 index 0000000..9cb5b63 --- /dev/null +++ b/docs/agentic-design/00-codebase-tour.md @@ -0,0 +1,486 @@ +# 代码库全景:从顶层到最深处 + +> 这篇文档是一个**递归的导游**。我们从顶层文件开始,逐层深入每个重要的目录,直到粒度太细而没有学习价值为止。每个章节都注明关键文件,你可以随时打开源代码对照。 + +--- + +## 顶层:核心文件 + +在 `/home/user/claude-code/` 的根目录下,以下文件是系统的脊梁: + +| 文件 | 行数 | 用途 | +|------|------|------| +| `main.tsx` | ~4700 | CLI 入口,React + Ink 终端渲染器 | +| `query.ts` | ~1700 | Query loop 状态机的完整实现 | +| `QueryEngine.ts` | 核心 | Query 执行的编排引擎 | +| `Tool.ts` | 核心 | Tool 的类型定义和接口规范 | +| `tools.ts` | 核心 | Tool 注册与初始化 | +| `context.ts` | 核心 | System prompt 和 context 的组装 | +| `commands.ts` | ~700 | 所有 slash command 的中央路由 | + +**学习路线**:`main.tsx` → `query.ts` → `Tool.ts` 可以了解 agent loop 的完整流程。 + +--- + +## `entrypoints/` — 入口层 + +系统支持多种启动方式: + +| 子目录/文件 | 用途 | +|-----------|------| +| `cli.tsx` | 标准 CLI 入口,React 组件树的根,feature detection | +| `sdk/` | Anthropic SDK 集成(给程序员用) | +| `mcp.ts` | MCP (Model Context Protocol) 服务器 | +| `init.ts` | 初始化逻辑,首次运行配置 | +| `sandboxTypes.ts` | 沙箱相关的类型定义 | + +**关键点**:`cli.tsx` 做 feature flag 检测,决定如何初始化全局状态。 + +--- + +## `query/` — Query Loop 配置和支撑 + +这个目录下的文件都是 `query.ts` 的辅助组件: + +| 文件 | 用途 | +|------|------| +| `config.ts` | Query 配置构建器,参数组合 | +| `tokenBudget.ts` | Token budget 追踪和强制执行 | +| `stopHooks.ts` | Stop/interrupt 处理 | +| `deps.ts` | Query 依赖注入 | + +**最重要的**:`tokenBudget.ts` 控制着"超限时触发 compaction"的逻辑。 + +--- + +## `tools/` — 所有 Tool 实现(40+ 个) + +这个目录有 40 多个子目录,每个代表一个 Tool。核心的几个: + +### 必读: + +| Tool | 文件 | 用途 | +|------|------|------| +| `AgentTool/` | 子 agent 的产卵和管理 | 核心,递归往下 | +| `FileReadTool/` | 读文件 | 基础 | +| `FileEditTool/` | 编辑文件 | 基础 | +| `FileWriteTool/` | 写文件 | 基础 | +| `BashTool/` | 执行 shell 命令 | 基础 | +| `GlobTool/` | 文件匹配 | 基础 | +| `GrepTool/` | 内容搜索 | 基础 | +| `WebFetchTool/` | 获取网页 | 基础 | +| `AskUserQuestionTool/` | 询问用户 | 权限边界 | +| `TodoWriteTool/` | 写 todo list | 后台任务 | + +### `AgentTool/` — 深递归 + +这是最复杂的 Tool,用来 spawn 子 agent: + +``` +AgentTool/ +├── AgentTool.tsx [233 KB] 主逻辑,agent 生命周期 +├── runAgent.ts 执行 agent 的主函数 +├── forkSubagent.ts fork 一个新 agent +├── resumeAgent.ts 恢复之前的 agent +├── loadAgentsDir.ts 从 ~/.agents/ 加载 agent 定义 +├── built-in/ 内置 agent +│ ├── generalPurposeAgent.ts +│ ├── planAgent.ts +│ ├── exploreAgent.ts +│ └── ... (其他 5 个) +├── agentMemory.ts Agent 记忆快照 +├── agentColorManager.ts Agent 的颜色分配(便于输出区分) +└── ... (其他支撑文件) +``` + +**关键概念**:Agent 可以是 local (in-process) 或 remote (CCR),使用 `AsyncLocalStorage` 隔离上下文。 + +--- + +## `services/` — 业务逻辑服务 + +这个目录是 Claude Code 的"心脏": + +### `tools/` — Tool 执行编排 + +``` +services/tools/ +├── StreamingToolExecutor.ts [核心] 并发 tool 执行,结果缓冲 +├── toolOrchestration.ts Tool 调度和权限检查 +├── toolExecution.ts 单个 tool 调用的执行 +├── toolHooks.ts Tool 执行前/后的 hook +└── ... +``` + +**最重要**:`StreamingToolExecutor.ts` 实现了"并发执行但顺序输出结果"的逻辑。 + +### `autoDream/` — 自动记忆压缩 + +``` +services/autoDream/ +├── autoDream.ts [11 KB] Dream 流程:3 个触发门 + 4 个阶段 +├── consolidationPrompt.ts [核心] 告诉 Claude 怎么压缩 memory +├── consolidationLock.ts 防并发的 lock 机制 +└── config.ts Dream 参数配置 +``` + +**关键点**:Dream 有三个触发条件门:24h 时间 + 5+ 个 session + 拿到 lock。 + +### 其他重要服务 + +| 目录 | 用途 | +|------|------| +| `api/` | Claude API 调用,token 计算,usage 追踪 | +| `mcp/` | MCP 协议服务器的实现 | +| `analytics/` | 事件上报、GrowthBook feature flags | +| `plugins/` | 插件系统 | + +--- + +## `utils/permissions/` — 权限系统(深递归) + +这是整个 agent sandbox 的防线,共 20+ 个文件,总计 300+ KB: + +``` +utils/permissions/ +├── permissions.ts [52 KB] 核心决策逻辑 +├── yoloClassifier.ts [52 KB] ML 自动审批分类器 +├── filesystem.ts [62 KB] 文件系统安全规则 +├── PermissionMode.ts Permission mode 枚举 +├── denialTracking.ts 追踪拒绝次数,circuit breaker +├── pathValidation.ts 路径安全(Unicode normalization 等) +├── permissionRuleParser.ts 解析 "Bash(git *)" 这样的规则 +├── classifierDecision.ts 分类器决策包装 +├── bashClassifier.ts Bash 命令的特殊规则 +├── permissionExplainer.ts 用 LLM 解释权限决定 +└── ... (其他辅助) +``` + +**核心 pipeline**:mode → rules → YOLO classifier → prompt user + +--- + +## `utils/` — 其他公用工具(部分列举) + +``` +utils/ +├── permissions/ [见上面的深递归] +├── messages/ +│ ├── mappers.ts SDK 消息格式转换 +│ ├── systemInit.ts 系统初始化消息构建 +│ └── ... +├── settings/ 设置加载和应用 +├── queryHelpers.ts Query 辅助函数 +├── undercover.ts 隐藏内部代号(Capybara, Tengu 等) +├── abortController.ts Abort signal 管理 +├── agentContext.ts Agent 上下文 getter +├── api.ts API 工具函数 +├── hooks/ React hooks (不是本目录) +├── ... (数十个其他) +``` + +**学习重点**:`permissions/` 最复杂,其次是 `messages/` 和 `settings/`。 + +--- + +## `coordinator/` — 多 Agent 协调 + +``` +coordinator/ +└── coordinatorMode.ts [19 KB] 唯一的文件,包含完整 coordinator 系统提示 + +功能:启用 CLAUDE_CODE_COORDINATOR_MODE=1 时激活,引入 4 个阶段: + research → synthesis → implementation → verification +``` + +**关键点**:Worker 通过 `` XML 向 coordinator 报告进度。 + +--- + +## `tasks/` — 后台任务管理 + +``` +tasks/ +├── LocalAgentTask/ 本地 agent 任务追踪 +├── RemoteAgentTask/ 远程 agent 任务追踪(CCR) +├── LocalMainSessionTask.ts 主 session 的任务 +├── LocalShellTask/ Shell 任务(tmux/iTerm2) +├── InProcessTeammateTask/ In-process teammate 任务 +├── DreamTask/ Memory dream 任务 +├── stopTask.ts 停止任务的逻辑 +├── types.ts Task 的类型定义 +└── pillLabel.ts 任务标签(UI 展示) +``` + +**核心概念**:每个后台任务都有生命周期追踪。 + +--- + +## `state/` — React 状态管理 + +``` +state/ +├── AppState.tsx React 状态类型定义 +├── AppStateStore.ts [21 KB] Zustand-like 状态存储 +├── onChangeAppState.ts 状态变化的 handler +├── selectors.ts 状态选择器 +├── store.ts 存储初始化 +└── teammateViewHelpers.ts Teammate 视图辅助 +``` + +**模式**:这是 Claude Code 的全局状态树,UI 所有数据都来自这里。 + +--- + +## `hooks/` — 80+ 个 React Hook + +重要的: + +| Hook | 用途 | +|------|------| +| `useCanUseTool.tsx` | [40 KB] 权限检查 Hook(频繁调用) | +| `useGlobalKeybindings.tsx` | [31 KB] 全局键盘快捷键 | +| `useTypeahead.tsx` | [212 KB] 自动补全建议 | +| `useVoice.ts` | [45 KB] 语音输入集成 | +| `useInboxPoller.ts` | [34 KB] 后台轮询消息 | +| `useArrowKeyHistory.tsx` | [34 KB] 历史导航(上/下箭头) | +| `useReplBridge.ts` | [115 KB] REPL 集成 | + +**学习策略**:跳过大部分 hook,只深入 `useCanUseTool` 和 `useTypeahead`。 + +--- + +## `bootstrap/state.ts` — 全局启动状态 + +``` +bootstrap/ +└── state.ts [56 KB] +``` + +内容: +- 当前 session ID 和持久化 flag +- 用户类型(ant/public) +- Feature flag 缓存 +- KAIROS mode 检测 +- 全局配置状态 + +**作用**:在 App 启动的最早时刻初始化所有全局状态。 + +--- + +## `cli/` — 终端输出和格式化 + +``` +cli/ +├── print.ts [212 KB] 终端渲染引擎 +├── structuredIO.ts NDJSON 和结构化输出 +├── handlers/ 命令特定的输出 handler +├── transports/ 多种输出后端(stdout, HTTP 等) +├── remoteIO.ts 远程 IO +└── ... (其他) +``` + +**关键**:`print.ts` 是整个 Claude Code 的"输出黑洞",所有文本最终都通过这里。 + +--- + +## `context/` — React Context Provider + +``` +context/ +├── QueuedMessageContext.tsx 消息队列 +├── mailbox.tsx Agent 邮箱(inter-agent 消息) +├── modalContext.tsx Modal 对话框 +├── notifications.tsx 通知系统 +├── overlayContext.tsx 浮层上下文 +├── promptOverlayContext.tsx 提示浮层 +├── stats.tsx 统计数据 +└── voice.tsx 语音上下文 +``` + +**作用**:提供 React component tree 范围内的全局上下文。 + +--- + +## `memdir/` — 用户 Memory 管理 + +``` +memdir/ +├── memdir.ts [21 KB] 核心 memory 目录管理 +├── memoryScan.ts 扫描 memory 文件 +├── memoryTypes.ts Memory 文件的类型 +├── memoryAge.ts Memory 的年龄计算 +├── paths.ts Memory 文件路径管理 +├── teamMemPaths.ts Team memory 路径(multi-agent) +└── findRelevantMemories.ts 查找相关 memory +``` + +**用途**:管理 `~/.claude/memory/` 目录,存储用户的持久化知识。 + +--- + +## `constant/` — 常量和系统提示 + +``` +constant/ (也被称为 constants/) +├── system.ts Base system prompt +├── systemPromptSections.ts System prompt 的可组装部分 +├── cyberRiskInstruction.ts 安全指导(Safeguards team 维护) +├── betas.ts API beta features 列表 +├── messages.ts 常见消息文本 +├── prompts.ts 各种 prompt 模板 +└── ... (其他常量) +``` + +**关键**:System prompt 是模块化的,不同 feature 会添加不同的部分。 + +--- + +## `bridge/` — 与 claude.ai 的 JWT 连接 + +``` +bridge/ +├── bridgeApi.ts 与 claude.ai 的通信 +├── bridgeMessaging.ts 消息协议 +├── remoteBridgeCore.ts 远程 bridge 核心 +├── createSession.ts 创建远程 session +├── jwtUtils.ts JWT 令牌工具 +├── trustedDevice.ts 受信设备管理 +└── ... (10+ 个其他) +``` + +**用途**:BRIDGE_MODE 时,CLI 可以通过 claude.ai 的 WebUI 远程控制。 + +--- + +## `remote/` — 远程 Session 管理 + +``` +remote/ +├── RemoteSessionManager.ts 远程 session 生命周期 +├── SessionsWebSocket.ts WebSocket 连接管理 +├── remotePermissionBridge.ts 权限同步 +├── sdkMessageAdapter.ts SDK 消息适配 +└── ... (其他) +``` + +**用途**:支持多个远程客户端同时连接到同一个服务。 + +--- + +## `entrypoints/sdk/` — SDK 集成 + +``` +entrypoints/sdk/ +├── agentSdkTypes.ts 给外部程序使用的类型 +└── (其他 SDK 特定代码) +``` + +**用途**:让程序员可以用代码调用 Claude Code,而不只是 CLI。 + +--- + +## `components/` — React UI 组件 + +``` +components/ +├── App.tsx 顶层 app 组件 +├── AgentProgressLine.tsx Agent 进度显示 +├── BaseTextInput.tsx 文本输入基础 +├── ... (100+ 个其他组件) +``` + +**特点**:使用 Ink 库在终端中渲染 React 组件。 + +--- + +## `commands/` — Slash 命令(88 个) + +``` +commands/ +├── commit.ts / commit-push-pr.ts Git 相关命令 +├── agent.ts Agent 管理命令 +├── task.ts, tasks.ts 任务管理 +├── config.ts 配置命令 +├── memory.ts, memory-dream.ts Memory 管理 +├── session.ts Session 管理 +├── autofix-pr.ts PR 自动修复 +├── ... (80+ 个其他) +``` + +**特点**:每个 command 都可以定制输出格式,支持 structured output (NDJSON)。 + +--- + +## 递归粒度停止条件 + +以下类型的文件不再递归深入: + +- **单功能工具函数** (<100 行,做一件明确的事):如 `array.ts`, `api.ts` 的某些导出 +- **简单 enum/type 定义**:如 `types.ts`, `*.d.ts` +- **单个小的 React Hook**:如 `useAfterFirstRender.ts` +- **Utility 映射和适配器**:如 message format converter + +这些文件的学习价值在于"知道它存在",而不是理解其内部细节。 + +--- + +## 学习路线建议 + +1. **快速扫描**:从 README 开始,看一眼这个全景。 +2. **焦点学习**:根据你的兴趣选择深入: + - 想理解 agent loop? → `query.ts`, `QueryEngine.ts` + - 想理解 tool 系统? → `Tool.ts`, `tools/`, `services/tools/` + - 想理解权限? → `utils/permissions/` + - 想理解多 agent? → `tools/AgentTool/`, `coordinator/` +3. **对照原文**:打开相应的源文件,参照本文的路线图逐个浏览。 + +--- + +## 核心数据流 + +``` +User Input + ↓ +entrypoints/cli.tsx (React 初始化) + ↓ +query.ts (Query Loop: QUERY → TOOL_USE → RESULT) + ↓ +Tool.ts & services/tools/ (Tool 执行) + ↓ +utils/permissions/ (权限检查) + BashTool/FileEditTool/... (实际执行) + ↓ +queryHelpers + messages (结果处理) + ↓ +state/ (状态更新) + ↓ +components/ + cli/print.ts (UI 渲染) + ↓ +User sees output +``` + +--- + +## 关键文件大小排名 + +``` +Top 15 largest files (最值得读): + +1. query.ts ~1700 行 +2. services/tools/StreamingToolExecutor.ts +3. utils/permissions/permissions.ts ~52 KB +4. utils/permissions/yoloClassifier.ts ~52 KB +5. utils/permissions/filesystem.ts ~62 KB +6. hooks/useTypeahead.tsx ~212 KB +7. cli/print.ts ~212 KB +8. tools/AgentTool/AgentTool.tsx ~233 KB +9. bootstrap/state.ts ~56 KB +10. state/AppStateStore.ts ~21 KB +11. services/autoDream/autoDream.ts ~11 KB +12. coordinator/coordinatorMode.ts ~19 KB +13. hooks/useCanUseTool.tsx ~40 KB +14. hooks/useGlobalKeybindings.tsx ~31 KB +15. hooks/useInboxPoller.ts ~34 KB +``` + +先读前 5 个,会 cover 80% 的系统复杂性。 diff --git a/docs/agentic-design/01-agent-loop.md b/docs/agentic-design/01-agent-loop.md new file mode 100644 index 0000000..a46fd80 --- /dev/null +++ b/docs/agentic-design/01-agent-loop.md @@ -0,0 +1,279 @@ +# 核心 Query Loop:Agent 的心脏 + +## 为什么需要 Loop? + +想象一个简单的"一问一答"的 AI:用户问 → API 回复 → 完成。但 agent 不一样: + +- 用户问:"帮我修复 bug" +- Agent 读取文件 → 分析问题 → 尝试修复 → 运行测试 → 测试失败 → 再次修改 → 再次测试 → 成功 + +这个过程需要**多轮往返**,每一轮中 Agent 决定"下一步应该执行哪个 tool"。这就是 **Query Loop**。 + +--- + +## 核心概念:状态机 + +Query loop 本质上是一个**状态机**,在以下几个状态间循环: + +``` +START + ↓ +QUERY (提交 prompt 给 Claude API) + ↓ +TOOL_USE (API 返回 tool_use block,包含 tool 名 + 输入) + ↓ +执行 Tool (BashTool, FileEditTool, 等等) + ↓ +RESULT (Tool 返回结果) + ↓ +提交结果给 Claude (作为 user 消息) + ↓ +QUERY (继续循环) + ↓ +END (API 返回 stop_reason="end_turn" 或其他终止条件) +``` + +--- + +## 代码位置 + +关键文件:`query.ts` (~1700 行) + +这个文件包含: +- Loop 的完整实现 (使用 `AsyncGenerator`) +- 状态跟踪 +- Tool 调用的编排 +- Streaming 支持 +- Compaction 触发 +- 错误处理和恢复 + +你应该从 `query.ts` 的函数签名开始: + +```typescript +export function* query(...): AsyncGenerator +``` + +它返回一个 `AsyncGenerator`,每次 `yield` 都是一个"事件"(API 消息、tool 调用、错误等)。 + +--- + +## Loop 的生命周期 + +### 阶段 1: 初始化 + +当你调用 `query()` 时: +- `messages` 数组初始化(包含 system prompt + 历史消息) +- `token_budget` 初始化(计算剩余 token) +- Compaction 检查(如果历史太长,自动压缩) +- Abort controller 初始化(支持中途停止) + +### 阶段 2: 发送 Prompt + +```typescript +// 构建消息数组 +let messages = [system_prompt, ...history]; + +// 调用 Claude API(使用缓存) +let response = await claude.messages.create({ + messages, + system: system_prompt, + max_tokens: remaining_budget, + stream: true, // 重要:流式输出 + ...config +}); +``` + +Streaming 很关键——API 会逐步返回: +1. First chunk: 可能包含 `message.start` +2. Content blocks: `text_delta`, `tool_use` block 开始 +3. Stop reason: 表示本轮结束 + +### 阶段 3: 处理 Streaming + +当 API 返回 `tool_use` block 时,loop 收集完整的参数,然后: + +``` +接收 tool_use block + ↓ +验证 tool 名称是否存在 + ↓ +检查权限 (useCanUseTool hook) + ↓ +执行 tool (StreamingToolExecutor) + ↓ +Yield tool 结果给消费者 + ↓ +添加到 messages 作为 user 消息 +``` + +### 阶段 4: 工具执行 + +Tool 执行由 `services/tools/StreamingToolExecutor.ts` 处理: +- **并发**:最多 5 个 tool 同时运行 +- **顺序输出**:结果按调用顺序返回(即使执行顺序不同) +- **Sibling abort**:一个失败会 cancel 兄弟 tool + +### 阶段 5: 检查终止条件 + +每一轮后,loop 检查是否应该继续: + +``` +stop_reason == "end_turn"? → 正常结束 +stop_reason == "max_tokens"? → Token 超限,触发 compaction +consecutive_errors >= 3? → 连续错误,放弃 +max_turns_reached? → 超过最大轮数 +abort_signal? → 用户中断 +``` + +--- + +## Token Budget:Context Window 的有限性 + +Context window 是有限的(比如 200K tokens)。这里的逻辑: + +``` +available_tokens = context_window_size - system_prompt_tokens - reserved_buffer + +每一轮后: + usage = response.usage (API 返回的实际使用) + remaining = available_tokens - usage.input_tokens - usage.output_tokens + + if remaining < threshold: + 触发 compaction (压缩历史) + + if remaining < min_required: + 拒绝继续,返回错误 +``` + +**Threshold** 通常设得比较激进,比如还剩 20% 时就开始压缩,以避免在真的耗尽时措手不及。 + +相关文件:`query/tokenBudget.ts` + +--- + +## Compaction:自动历史压缩 + +当 token 预算吃紧时,loop 会自动压缩历史: + +1. **Analyzer phase**: 扫描历史消息,找出"长输出消息"(比如代码块) +2. **Prioritize**: 对话越早,压缩优先级越高 +3. **Compress**: 使用 Claude API 总结这段历史,生成 200 行以内的 summary +4. **Replace**: 把原始的 20 条消息替换为 1 条 summary 消息 + +这保证了 loop 可以持续运行,即使初始历史很长。 + +--- + +## Streaming 与 Tool Call 的交织 + +这是一个复杂的地方。API 可能返回: + +``` +message { + content: [ + { type: "text", text: "让我先读文件..." }, + { type: "tool_use", id: "...", name: "FileReadTool", ... }, + { type: "text", text: "现在我看到问题了..." }, + { type: "tool_use", id: "...", name: "FileEditTool", ... }, + ] +} +``` + +所以一条消息中可能**混合了文本和 tool call**。Loop 需要: +- 收集每个 tool_use block 的完整输入参数(可能分段到达) +- 在 tool_use 结束时立即执行(不等待消息全部返回) +- 继续接收后续的文本或 tool call +- 当消息完全接收后,将所有 tool 结果作为一条 user 消息发回 + +这使得 **interleaved thinking** 成为可能:模型可以一边思考一边调用工具。 + +--- + +## Design Decision 专栏 + +### 为什么用 `AsyncGenerator` 而不是 callback? + +不好的设计(callback): +```typescript +query(messages, { + onMessage: (msg) => {...}, + onToolCall: (tool, input) => {...}, + onError: (err) => {...}, + onProgress: (progress) => {...}, +}) +``` + +问题: +- 4 种不同的 callback,难以理解执行顺序 +- 消费者需要管理状态机来追踪"当前在哪个阶段" +- 测试困难 + +**更好的设计**(Generator): +```typescript +for await (const event of query(messages, config)) { + if (event.type === 'message') {...} + else if (event.type === 'tool_use') {...} + else if (event.type === 'error') {...} +} +``` + +优点: +- 单一的 `for await...of` 循环,清晰的顺序 +- 每个 event 都是一个联合类型,编译器可以帮你检查 +- 可以在 loop 中添加复杂的条件逻辑(abort, timeout 等) +- 易于测试(可以模拟一个生成事件的 generator) + +Generator 让"一系列事件"变得一级公民。 + +### 为什么需要 Compaction? + +不做 compaction: +- 100 轮 agent 工作后,messages 数组有几千条消息 +- 提交给 API 时,prompt cache hit 率下降(因为前缀不再是"静态部分") +- Token 浪费在重复的对话历史上 + +做 compaction: +- 定期将"已经达成共识的历史部分"总结成一条消息 +- 保持 prompt cache 的"静态前缀"有效 +- 节省 token,延长 agent 能工作的轮数 + +--- + +## 常见误解 + +**误解 1**:"Query loop 就是不断问 Claude?" + +实际:Loop 实现的是一个**两层状态机**: +- 外层:Claude API 的请求/响应循环 +- 内层:Tool 的执行和结果收集 + +tool 执行完全不涉及 Claude API 的额外调用(除非 tool 本身调用 API)。 + +**误解 2**:"Compaction 会丢失信息?" + +实际:Compaction 使用 Claude 自己来总结历史,所以关键信息被保留。丢失的只是"冗余的解释"或"中间尝试",这些对后续工作没用。 + +**误解 3**:"Token budget 追踪是精确的?" + +实际:API 返回的 `usage` 是经过舍入的(某些模型),所以我们的估计可能有 ±5% 的误差。这就是为什么我们用激进的阈值(20% 时就开始压缩)而不是等到 100% 才反应。 + +--- + +## 关键要点 + +1. **Query loop 是异步生成器**,yield 事件流而不是回调 +2. **循环的关键状态转移**:QUERY → TOOL_USE → RESULT → QUERY +3. **Token budget 必须主动管理**,compaction 是自动压缩的关键 +4. **Streaming 支持 interleaved thinking**,一条消息中可以混合文本和 tool call +5. **Compaction 会定期触发**,保持 history 可控,并维护 prompt cache 的有效性 + +--- + +## 深入阅读 + +- `query.ts`:完整 loop 实现(必读) +- `query/tokenBudget.ts`:Token 追踪逻辑 +- `services/tools/StreamingToolExecutor.ts`:Tool 执行编排 +- `query/stopHooks.ts`:终止条件检查 + +下一步:去了解 **Tool 系统**是如何设计的,使得这个 loop 可以灵活地执行任意 tool。 diff --git a/docs/agentic-design/02-tool-system.md b/docs/agentic-design/02-tool-system.md new file mode 100644 index 0000000..6be9c41 --- /dev/null +++ b/docs/agentic-design/02-tool-system.md @@ -0,0 +1,378 @@ +# Tool 系统:Agent 影响世界的手段 + +## 为什么需要 Tool? + +如果 Claude 只能回复文字,它就只是个聊天机器人。要让 Claude **执行动作**(读文件、改代码、运行命令),需要一个 **Tool 接口**。 + +Tool 系统解决的问题: +1. 如何告诉 Claude "有哪些动作可用"? +2. 如何验证 Claude 的请求是否合法(权限检查)? +3. 如何让多个 tool 并发执行,但结果保持有序? +4. 如何在 tool 执行失败时恢复? + +--- + +## 核心概念:Tool Interface + +一个 `Tool` 必须满足这个接口(`Tool.ts`): + +```typescript +interface Tool { + // 1. 模型识别 tool 的名称 + name: string // 如 "FileReadTool" + + // 2. 模型理解 tool 的用途(出现在 system prompt 中) + description: string + + // 3. 模型生成的参数必须符合这个 schema + inputSchema: JSONSchema // 如 { type: "object", properties: { path: ... } } + + // 4. 执行 tool 的函数 + call(input: ToolInput): AsyncGenerator + + // 5. 指示这个 tool 是否"安全"(后文详述) + isSafe?: boolean + + // 6. 内部使用,不会发给模型 + internalMetadata?: { ... } +} +``` + +每个字段的意义: + +### `name` 和 `description` +这两个被送到 Claude API,出现在 system prompt 的 tool 列表中: + +``` +Available tools: + +1. FileReadTool + Read the contents of a file from the filesystem + Parameters: { path: string } + +2. BashTool + Execute a bash command in the user's shell + Parameters: { command: string } +``` + +Claude 看到这个列表,学会了"我可以用 FileReadTool 来读文件"。 + +### `inputSchema` +这是 JSON Schema 格式,例如: + +```typescript +{ + type: "object", + properties: { + path: { + type: "string", + description: "Absolute file path" + } + }, + required: ["path"] +} +``` + +Claude 会**严格遵守这个 schema**(模型已训练)。例如,如果你要求 `path` 必须是字符串,Claude 就不会发来 `{path: 123}`。 + +### `call(input)` +这是 tool 的实现。它返回一个 `AsyncGenerator`,每次 `yield` 都是一个结果。例如: + +```typescript +async *call(input: { path: string }) { + // 逐步产生结果(支持 streaming) + yield { type: "start", message: "Opening file..." } + yield { type: "content", data: file_content } + yield { type: "done", lines_read: 100 } +} +``` + +为什么是 generator?因为某些 tool(如 BashTool)的输出可能很长,需要**流式返回**。 + +### `isSafe` 和权限 + +某些 tool 被标记为 `unsafe`: +- `BashTool` ← 可以执行任意命令,危险 +- `FileEditTool` ← 可以修改文件,需要权限检查 +- `FileReadTool` ← 只读,相对安全 + +Unsafe tool 在执行前**必须通过权限检查**。 + +--- + +## Tool 注册与过滤 + +在 `tools.ts` 中,所有 tool 被注册到一个中央注册表: + +```typescript +const ALL_TOOLS: Tool[] = [ + AgentTool, + BashTool, + FileReadTool, + FileEditTool, + ..., +] +``` + +然后根据用户权限和配置进行**过滤**: + +```typescript +// 仅公开 tool +const PUBLIC_TOOLS = ALL_TOOLS.filter(t => !t.internalOnly) + +// 仅 ant 用户可用 +const ANT_ONLY_TOOLS = [REPL_TOOL, CONFIGTOOL, ...] +``` + +这样,模型只会看到当前环境允许的 tool 清单。 + +--- + +## Tool 执行 Pipeline + +从"模型调用 tool"到"返回结果"的完整流程: + +``` +Query Loop 收到 tool_use block + ├─ Tool 名: "FileEditTool", Input: {path: "...", newContent: "..."} + │ + ├─ 1. 验证 Tool 存在 ✓ + │ + ├─ 2. 权限检查 (utils/permissions/) + │ - Mode: auto 还是 default? + │ - 规则匹配? (如 "FileEdit(/src/*.ts)") + │ - YOLO 分类器? (是否 auto-approve) + │ - 如果拒绝 → 返回 permission_denied error + │ + ├─ 3. 执行 Tool + │ - 调用 tool.call(input) + │ - 得到 AsyncGenerator + │ - 逐个 yield 结果 + │ + ├─ 4. 收集结果 + │ - 内容可能很长,缓冲到内存 + │ - 或流式返回给 UI + │ + └─ 5. 发送给 Claude + - 组合成 user 消息: "Tool result: ..." + - 继续 Query Loop +``` + +相关文件: +- 权限检查:`utils/permissions/permissions.ts` +- 执行编排:`services/tools/toolOrchestration.ts` +- 流式执行:`services/tools/StreamingToolExecutor.ts` + +--- + +## 并发执行:多个 Tool 同时运行 + +一条 Claude 消息中可能包含多个 `tool_use` block: + +``` +Claude: + 1. 读文件 A (tool_use id=1) + 2. 读文件 B (tool_use id=2) + 3. 读文件 C (tool_use id=3) +``` + +这三个 tool 可以**并发执行**,但有几个复杂性: + +### Concurrency Gate + +同时最多 5 个 tool 运行(可配置): + +```typescript +const CONCURRENCY_LIMIT = 5 + +// 队列管理 +const queue = [tool_1, tool_2, tool_3, ...] +while (queue.length > 0) { + const batch = queue.splice(0, CONCURRENCY_LIMIT) + const results = await Promise.all( + batch.map(tool => tool.call(...)) + ) + // 处理结果 +} +``` + +### In-Order Emission + +虽然执行可能乱序,但**结果必须按调用顺序返回**: + +``` +调用顺序: tool_1, tool_2, tool_3 +执行时序: tool_2 ✓ (快速) + tool_1 ✓ (较慢) + tool_3 ✓ + +返回顺序: tool_1 结果 → tool_2 结果 → tool_3 结果 + +为什么? 因为 Claude 期望看到它调用的顺序被保留 +``` + +实现方式:使用**结果缓冲**: + +```typescript +const results = new Map() // id → result +let nextIdToEmit = 0 + +for each completed tool { + results.set(tool.id, tool.result) + + // 检查是否可以按顺序 emit + while (results.has(nextIdToEmit)) { + yield results.get(nextIdToEmit) + nextIdToEmit++ + } +} +``` + +### Sibling Abort + +如果一个 tool 失败了,它的**兄弟 tool 会被立即取消**: + +``` +tool_1 → 运行中 +tool_2 → 运行中 +tool_3 → 运行中 + +tool_2 抛异常 ✗ + ↓ +立即 abort tool_1 和 tool_3 + ↓ +返回错误,整个批次失败 +``` + +这是一个**快速失败**策略:不要继续浪费时间在其他 tool 上,直接告诉 Claude 出了问题。 + +相关文件:`services/tools/StreamingToolExecutor.ts` + +--- + +## Safe vs Unsafe Tool Classification + +Tool 分两类: + +### Safe Tool(自动执行) +``` +FileReadTool ← 只读,没有副作用 +WebFetchTool ← 只是下载网页 +GlobTool ← 只是列文件 +GrepTool ← 只是搜索内容 +``` + +这些可以在 `auto` permission mode 下自动执行,不需要用户确认。 + +### Unsafe Tool(需要权限检查) +``` +BashTool ← 可能执行任意命令 +FileEditTool ← 可能修改重要文件 +FileWriteTool ← 可能覆盖数据 +AgentTool ← 可能 spawn 新 agent +TaskCreateTool ← 可能创建后台任务 +``` + +这些在执行前必须通过 `utils/permissions/` 的检查。 + +--- + +## Design Decision 专栏 + +### 为什么 Tool 返回 `AsyncGenerator` 而不是 `Promise`? + +不好的设计: +```typescript +call(input): Promise { + // 必须等待整个操作完成后才返回 + const result = await readFile(input.path) + return result // 10 MB 的代码文件?等吧 +} +``` + +问题: +- 大文件时,UI 一直卡住,看不到进度 +- Claude 看不到中间步骤 + +更好的设计: +```typescript +call(input): AsyncGenerator { + yield { type: "progress", message: "Reading..." } + const content = await readFile(input.path) + yield { type: "content", data: content } // 分块返回 + yield { type: "complete", lines: 100 } +} +``` + +优点: +- UI 可以**立即显示**进度消息 +- 长操作时用户看得到"不是卡住了,是在处理" +- 便于 debug:中间消息可以被记录 + +Generator 把**长时间操作的透明度**提升为一级特性。 + +### 为什么要保持结果顺序? + +如果乱序返回: +``` +Claude 说:读 A, 读 B, 读 C +我们返回:B 的结果, C 的结果, A 的结果 +``` + +Claude 会困惑:"我要的 A 呢?" + +所以必须: +``` +Claude 说:读 A, 读 B, 读 C +我们返回:A 的结果, B 的结果, C 的结果 +``` + +这样 Claude 可以正确关联"我的第一个请求的响应是这个"。 + +--- + +## 常见误解 + +**误解 1**:"Tool 就是函数调用?" + +实际:Tool 是一个**完整的接口**,包括: +- 模型可见的名称和描述 +- 输入规范(schema) +- 权限检查 +- 执行编排 +- Streaming 支持 + +函数调用只是内部实现的一部分。 + +**误解 2**:"Tool 是 OpenAI function calling 的翻版?" + +实际:Claude Code 的 Tool 设计更深层: +- 对权限的原生支持(OpenAI 需要自己实现) +- 对并发执行的编排 +- 对长时间操作的 streaming 支持 +- 更细粒度的安全分类 + +**误解 3**:"所有 tool 都必须是同步的?" + +实际:Tool 是异步的(`AsyncGenerator`),可以支持任意长的操作(包括网络请求、文件 I/O)。 + +--- + +## 关键要点 + +1. **Tool interface 是客户端与 Agent 的合约**,包含名称、描述、schema、执行函数 +2. **Permission 检查是 tool 执行前的网关**,决定是否允许 +3. **并发执行最多 5 个 tool**,但结果必须按调用顺序返回 +4. **Sibling abort 是快速失败**,一个错误就停止兄弟 tool +5. **AsyncGenerator 使 tool 支持 streaming**,UI 可见进度,model 可见中间步骤 + +--- + +## 深入阅读 + +- `Tool.ts`:Tool interface 定义 +- `tools.ts`:Tool 注册和过滤 +- `services/tools/StreamingToolExecutor.ts`:并发执行编排 +- `utils/permissions/permissions.ts`:权限检查 + +下一步:去了解**多 Agent 协调**,看看当 Claude 想要 spawn 一个子 agent 时(通过 `AgentTool`),系统如何支持这个。 diff --git a/docs/agentic-design/03-multi-agent-coordination.md b/docs/agentic-design/03-multi-agent-coordination.md new file mode 100644 index 0000000..cae97cb --- /dev/null +++ b/docs/agentic-design/03-multi-agent-coordination.md @@ -0,0 +1,351 @@ +# 多 Agent 协调:并行与分解 + +## 为什么需要多个 Agent? + +有些任务太复杂,一个 agent 力不从心: + +- 用户说:"帮我把代码库从 TypeScript 迁到 Rust" +- 一个 agent 从第一个文件开始,改 5 个小时后,context 满了 + +更好的方式:**分解任务** + +- Coordinator agent 解析任务、制定计划 +- Worker agent 1 并行处理"转换 utils 模块" +- Worker agent 2 并行处理"转换 API 层" +- Worker agent 3 并行处理"转换 UI 组件" +- Coordinator 总结结果、验证一致性 + +这就是 **多 agent 协调**。 + +--- + +## 核心概念:Agent 类型 + +Claude Code 支持多种 agent: + +### 1. Local Agent(进程内) + +```typescript +// 在当前进程中 spawn 一个子 agent +const subAgent = await spawnAgent({ + type: "local", + agentDef: generalPurposeAgent, + initialMessage: "帮我测试这个函数" +}) +``` + +特点: +- 共享内存(同一个 Node.js 进程) +- 上下文隔离(通过 `AsyncLocalStorage`) +- 无网络开销 +- 最快的交互 + +### 2. Remote Agent(云容器) + +```typescript +const remoteAgent = await spawnAgent({ + type: "remote", + agentDef: exploreAgent, + initialMessage: "搜索 codebase 中的 bug" +}) +``` + +特点: +- 在 CCR(Cloud Container Runtime)中运行 +- 独立的资源池 +- 支持长时间运行(最多 30 分钟) +- 网络传输延迟,但隔离度最高 + +### 3. Forked Agent(进程派生) + +```typescript +const forkedAgent = spawnForkedAgent({ + agentScript: "./my-agent.ts", + args: ["--mode", "debug"] +}) +``` + +特点: +- 创建新的 Node.js 子进程 +- 完全隔离的 V8 引擎 +- 支持 CPU 密集任务(不会阻塞主线程) + +### 4. In-Process Teammate(进程内同伴) + +```typescript +const teammate = await createTeam({ + name: "research-squad", + agents: [agent1, agent2, agent3], + strategy: "parallel" +}) +``` + +特点: +- 多个 agent 同时运行 +- 共享邮箱(互相发消息) +- 同步协调,不需要网络 + +--- + +## Agent 的 Spawning 和隔离 + +### 怎样 Spawn 一个 Agent? + +通过 `AgentTool` (文件:`tools/AgentTool/AgentTool.tsx`): + +```typescript +// Claude 调用 AgentTool +Agent: "让我 spawn 一个 explore agent 来搜索代码库" + +Tool Call: + name: "AgentTool" + input: { + type: "spawn", + agentDef: "exploreAgent", + prompt: "搜索 utils/ 下的性能问题", + isolationMode: "local" + } + +返回: + agentId: "abc123" + status: "running" +``` + +### 上下文隔离:`AsyncLocalStorage` + +为什么需要隔离?子 agent 的全局状态不应该污染父 agent: + +```typescript +// 主 agent 的全局状态 +const sessionId = "parent-session-id" +const permissions = "default" + +// 在子 agent 中,这些应该被**覆盖** +asyncLocalStorage.run({ + sessionId: "child-agent-id", + permissions: "bubble", // 特殊权限给 worker + memoryDir: "/tmp/child-memory" +}, () => { + runChildAgent() // 子 agent 看到的是覆盖后的值 +}) +``` + +实现:`AsyncLocalStorage` 是 Node.js 内置的上下文隔离机制。 + +--- + +## Coordinator 模式:4 个阶段 + +启用 `CLAUDE_CODE_COORDINATOR_MODE=1` 时,agent 进入 coordinator 模式。 + +### 阶段结构 + +``` +用户请求: "帮我重构这个 monorepo" + ↓ +[1] RESEARCH PHASE + Coordinator 说:"我需要理解项目结构" + Worker 1 探索 package.json + tsconfig + Worker 2 探索 src/ 目录结构 + Worker 3 探索 dependencies 图 + → 输出: 项目现状报告 + ↓ +[2] SYNTHESIS PHASE + Coordinator 读 3 个 worker 的报告 + Coordinator 说:"基于这些发现,我的计划是..." + → 输出: 详细重构计划 + ↓ +[3] IMPLEMENTATION PHASE + Coordinator 把计划分解为任务 + Worker A 修改 package.json + Worker B 更新 tsconfig + Worker C 调整 src/ 组织 + → 并行执行 + ↓ +[4] VERIFICATION PHASE + Coordinator 说:"让我验证改动" + Worker 运行测试 + Worker 检查类型错误 + → 输出: 验证报告 + ↓ +完成:Coordinator 总结全流程,返回最终结果 +``` + +### 通信机制:`` + +Worker 通过 XML 消息向 Coordinator 报告进度: + +```xml + + research-1 + complete + Found 3 monorepo packages +
+ - Package A: React components + - Package B: Utils library + - Package C: CLI tool +
+
+``` + +Coordinator 看到这个消息,就知道: +- Task "research-1" 完成了 +- 发现了 3 个包 + +相关文件:`coordinator/coordinatorMode.ts`(包含完整的 system prompt,大约 330 行) + +--- + +## 共享 Scratchpad + +Worker 可以在共享空间写临时数据(feature gate: `tengu_scratch`): + +```typescript +// Worker 1 写入 +await writeToScratchpad("project_structure.md", """ +# Monorepo Structure +- packages/ui/ + - components/ + - styles/ +""") + +// Worker 2 读取 +const structure = await readFromScratchpad("project_structure.md") + +// Coordinator 读取 +const allNotes = await listScratchpad() +``` + +优点: +- Worker 不需要把所有信息放在 user 消息中 +- 大文件可以直接读取,不占用 context window +- 自然的"工作台"抽象 + +--- + +## Agent 内存和记忆 + +每个 agent 可以有自己的 memory: + +``` +~/.claude/memory/ +├── team-memory/ # 团队共享 +│ ├── shared-findings.md +│ └── progress.md +├── agent-abc123/ # Agent 特定 +│ ├── MEMORY.md # Agent 的长期知识库 +│ └── session-transcript.log +``` + +**Team Memory Sync** (`services/teamMemorySync/`) 确保: +- 所有 agent 可以读取共享的 findings +- 不会产生数据竞争(使用 file lock) + +--- + +## Design Decision 专栏 + +### 为什么用 `` XML 而不是结构化 IPC? + +不好的设计(二进制 IPC): +``` +Worker 发送: MessageType::RESEARCH_COMPLETE { + task_id: 0x123, + status: 0x02, + payload: [0xAB, 0xCD, ...] +} +``` + +问题: +- Claude 看不懂二进制协议 +- 如果需要调试,你必须手动反序列化 +- 扩展协议时需要修改版本号 + +更好的设计(XML): +``` + + abc123 + complete + Found 100 bugs + +``` + +优点: +- Claude 可以**直接阅读和生成**这种格式 +- 调试时在日志中直接看到可读的消息 +- 自文档化(XML 标签说明意义) +- 易于扩展(添加新字段不破坏旧系统) + +**Text is the protocol** 的又一例证。 + +### 为什么 Coordinator 要等 Research 完全结束才进行 Synthesis? + +如果不等(流式处理): +``` +Worker 1 报告 → Coordinator 开始 synthesis +Worker 2 报告(晚到)→ Coordinator 需要重新 synthesis +Worker 3 报告(更晚)→ 又得重新来一遍 +``` + +问题:浪费 token 和时间在重复的 synthesis。 + +如果等待(阶段隔离): +``` +所有 Worker 完成 Research + ↓ +Coordinator 一次性读完所有报告 + ↓ +一次 Synthesis,不需要改 +``` + +权衡: +- 阶段隔离:时间线性(4 个阶段顺序执行) +- 流式处理:时间可能更短(如果 worker 速度差异大),但 prompt 可能更长 + +Claude Code 选择阶段隔离,因为系统提示清晰度更重要。 + +--- + +## 常见误解 + +**误解 1**:"Coordinator 也会执行 tool?" + +实际:Coordinator **只** spawn worker 和管理任务。所有的 tool 执行(读文件、改代码、运行命令)都在 worker 中进行。Coordinator 只是看消息、做决策、发指令。 + +**误解 2**:"Worker 之间可以直接通信?" + +实际:Worker 通过 **Coordinator 中转**。这样 Coordinator 可以: +- 理解全局状态 +- 决定优先级 +- 在冲突时仲裁 + +直接 worker-to-worker 通信会导致难以追踪的依赖和死锁。 + +**误解 3**:"多个 agent 肯定比单个快?" + +实际:取决于任务: +- **可并行化**(如搜索 3 个不同目录):并行快 +- **高度依赖**(后续任务需要前序结果):并行反而慢(overhead) + +Claude Code 的 Coordinator 会选择合适的分解策略。 + +--- + +## 关键要点 + +1. **Agent 类型**:local (in-process), remote (CCR 30min), forked, teammate +2. **上下文隔离**:通过 `AsyncLocalStorage` 分离全局状态 +3. **Coordinator 模式**:4 个阶段(research → synthesis → implementation → verification) +4. **Worker 通信**:通过 `` XML,Coordinator 中转 +5. **Shared scratchpad**:大文件存储,不占 context window + +--- + +## 深入阅读 + +- `tools/AgentTool/AgentTool.tsx`:Agent spawning 逻辑 +- `tools/AgentTool/forkSubagent.ts`:Fork 实现 +- `coordinator/coordinatorMode.ts`:Coordinator system prompt 和阶段逻辑 +- `services/teamMemorySync/`:Team memory 同步 + +下一步:了解**权限系统**,看看 Agent 怎么知道"我可以执行什么 tool"。 diff --git a/docs/agentic-design/04-permission-system.md b/docs/agentic-design/04-permission-system.md new file mode 100644 index 0000000..7b64f78 --- /dev/null +++ b/docs/agentic-design/04-permission-system.md @@ -0,0 +1,317 @@ +# Permission 系统:Agent 的沙箱 + +## 为什么需要权限管控? + +想象 Claude 可以执行任意 bash 命令。用户说"帮我看看网站",Claude 可能: +- 合法地执行 `curl https://example.com` +- 也可能执行 `rm -rf /` ← 灾难 + +**权限系统的目标**: +1. 阻止意外的危险操作 +2. 提示用户"你确定吗?" +3. 在保留便利的同时维护安全 + +Claude Code 的 permission 系统是一个**多层防线**。 + +--- + +## 核心概念:Permission Mode + +系统支持多种权限模式,用户可以选择: + +| Mode | 特点 | 场景 | +|------|------|------| +| `default` | 每次都问用户 | 不信任 agent,想完全控制 | +| `auto` | ML 分类器自动审批 | 信任 agent,想要流畅体验 | +| `acceptEdits` | 自动接受文件编辑(但仍然问危险操作) | 开发工作流(改代码 OK,执行脚本要问) | +| `bypass` | 完全自动,不问(谨慎使用)| 内部测试、自动化流程 | +| `dontAsk` (aka `yolo`) | 拒绝所有非低风险操作 | 沙箱环境,最安全 | +| `plan` | 特殊模式:进入计划编辑阶段 | 用户审核计划后再执行 | + +用户在启动时选择(环境变量或配置文件): +```bash +# 每次都问 +claude-code --permission-mode default + +# 自动审批 +claude-code --permission-mode auto + +# 最安全 +claude-code --permission-mode dontAsk +``` + +相关文件:`utils/permissions/PermissionMode.ts` + +--- + +## Permission 决策 Pipeline + +当 agent 试图执行一个 tool 时,系统会问:"我应该允许吗?" + +``` +Agent 调用: BashTool { command: "npm test" } + │ + ├─ [1] Mode 检查 + │ if mode == "bypass" → ✓ 允许,返回 + │ if mode == "dontAsk" → ✗ 拒绝 Bash,返回 + │ + ├─ [2] 规则匹配 + │ 检查用户配置的规则列表 + │ 如: "Bash(git *)" → 允许 git 命令 + │ "Bash(npm *)" → 允许 npm 命令 + │ "Bash(*)" → 允许任意 bash (太宽) + │ + │ 我们的命令 "npm test" 匹配 "npm *" → ✓ + │ + ├─ [3] 风险分类 + │ rule match → LOW 风险 + │ 如果没匹配到规则,分类器决定风险等级 + │ + ├─ [4] YOLO 分类器 + │ 对于 MEDIUM 风险的操作: + │ - 如果被模型认为"明显安全",自动批准 + │ - 否则需要用户确认 + │ + └─ [5] 最后手段:提示用户 + 用户选择: ✓ 允许 / ✗ 拒绝 / ⚠️ 改规则 +``` + +相关文件:`utils/permissions/permissions.ts` (52 KB,核心决策逻辑) + +--- + +## 规则系统 + +用户可以配置规则,告诉 agent 什么是自动允许的。规则语法: + +``` +Bash(git *) # 允许任何 git 命令 +Bash(npm install) # 只允许 npm install,不允许 npm test +FileEdit(src/*.ts) # 允许编辑 src/ 下的 .ts 文件 +FileRead(/*) # 允许读任何文件 +``` + +规则支持 **glob 模式**(`*` 通配符)。 + +### 规则分层 + +规则可以来自多个地方(优先级从高到低): + +1. **CLI 参数**:`--approve-file "FileEdit(src/*.ts)"` +2. **项目配置**:`.claude/settings.json` 中的 `approvals` +3. **用户设置**:`~/.claude/settings.json` +4. **系统默认**:Claude Code 内置的基础规则 + +下层规则会被上层规则覆盖。 + +相关文件:`utils/permissions/permissionRuleParser.ts`, `utils/permissions/permissionsLoader.ts` + +--- + +## YOLO 分类器:ML 自动审批 + +对于**规则未覆盖的操作**,system 用 YOLO 分类器("You Only Live Once")决定: + +``` +输入: + tool_name: "BashTool" + command: "npm test" + context: 当前任务是"修复 bug" + +分类器说: "这个操作很像是开发者会做的正常事,批准" + +结论: 自动允许,不问用户 +``` + +分类器基于 ML 训练,考虑: +- Tool 类型(BashTool 比 FileReadTool 高风险) +- 具体命令(`npm test` 比 `rm -rf` 安全) +- 当前上下文("修复 bug" vs "浏览网页") + +相关文件:`utils/permissions/yoloClassifier.ts` (52 KB) + +### 分类器的风险等级 + +``` +LOW + → 自动允许(不问用户) + → 例:FileReadTool, GlobTool, WebFetchTool + +MEDIUM + → 取决于 mode(auto mode 用分类器,default mode 问用户) + → 例:FileEditTool, BashTool (git 命令) + +HIGH + → 一般不自动允许(需要用户确认) + → 例:BashTool (rm -rf), FileWriteTool (覆盖重要文件) +``` + +--- + +## Denial Tracking:Circuit Breaker + +如果用户连续拒绝 agent 多次,system 会自动**降级权限模式**: + +``` +用户拒绝: + 1. "BashTool" ← Denial #1 + 2. "FileEditTool" ← Denial #2 + 3. "BashTool" ← Denial #3 + +连续 3 次拒绝触发! + ↓ +系统切换到 "default" mode,后续所有操作都要问用户 + +这样防止: +- Agent 持续尝试被拒的操作 +- 用户一直看到提示烦不胜烦 +``` + +同时,系统还追踪**总拒绝数**: + +``` +同一 session 中总共拒绝了 20 次 + ↓ +系统记录日志:"这个 session 决策质量差,建议用户审视任务" +``` + +相关文件:`utils/permissions/denialTracking.ts` + +--- + +## 路径安全:防止路径遍历攻击 + +即使规则写得再好,攻击者也可能用**特殊编码**绕过: + +``` +规则: FileEdit(src/*.ts) ← 只允许 src/ 目录 + +攻击尝试: + path: "src/../../../etc/passwd" ← 试图逃逸 + path: "src%2f..%2f..%2fetc%2fpasswd" ← URL 编码 + path: "src/\u202e../../../etc/passwd" ← Unicode 隐藏字符 +``` + +系统有多层防御: + +1. **Path Normalization**: + ``` + "src/../../../etc/passwd" + → 规范化 + → "/etc/passwd" + → 检查是否在 src/ 内? 否 → 拒绝 + ``` + +2. **URL Decoding**: + ``` + "src%2f..%2f..%2fetc" + → 解码 + → "src/../../etc" + → 规范化 → 拒绝 + ``` + +3. **Unicode Normalization**: + ``` + Unicode 隐藏字符也会被清理 + ``` + +相关文件:`utils/permissions/pathValidation.ts` (62 KB) + +--- + +## Protected Files:黑名单 + +某些文件**永远不会被自动编辑**: + +``` +.gitconfig ← Git 配置,修改可能破坏工作流 +.bashrc, .zshrc ← Shell 配置,修改可能卡住终端 +.mcp.json ← MCP 配置,影响工具可用性 +.claude.json ← Claude Code 自己的配置 +``` + +这些文件即使通过了权限检查,也会再次提示用户确认。 + +--- + +## Design Decision 专栏 + +### 为什么需要 ML 分类器? + +如果只用规则: +``` +允许所有 Bash 命令? + → 太宽,不安全(用户可能 rm -rf) + +只允许 git/npm 命令? + → 太窄,不便利(sudo docker pull 被拒) +``` + +ML 分类器的优势: +``` +可以说"这个命令看起来是在开发工作流的一部分, +而不是破坏性操作,所以允许" + +如果出错(classify 有假阴性),系统还有: + - Denial tracking(连续拒绝降级) + - Protected files(黑名单) + - Circuit breaker +``` + +所以 ML 不是唯一防线,而是**多层防御中的一层**。 + +### 为什么规则支持 Glob 而不是正则表达式? + +Glob:`src/*.ts` → 简单直观 +正则:`^src/[^/]+\.ts$` → 复杂,容易写错 + +目标用户是**工程师,但不是安全专家**。Glob 的错误率更低。 + +--- + +## 常见误解 + +**误解 1**:"Permission mode 是全局的,改一次就生效?" + +实际:可以在 CLI 参数中针对单个命令覆盖: +```bash +claude-code --permission-mode auto /commit # 这个 /commit 用 auto mode +claude-code --permission-mode default /config # 这个 /config 用 default mode +``` + +**误解 2**:"YOLO 分类器是完美的?" + +实际:分类器是 **best-effort**,有假阳性和假阴性。假阴性(该拒的通过了)会被 denial tracking 捕捉到。 + +**误解 3**:"Protected files 名单是硬编码的?" + +实际:可以通过配置扩展: +```json +{ + "protectedFiles": [".gitconfig", ".bashrc", ".my-precious-file"] +} +``` + +--- + +## 关键要点 + +1. **Permission mode**:`default`(每次问), `auto`(ML 自动), `acceptEdits`, `bypass`, `dontAsk` +2. **决策 pipeline**:Mode → Rules → Risk Classification → YOLO → User Prompt +3. **规则系统**:Glob 模式,分层来源(CLI > project > user > default) +4. **YOLO 分类器**:ML 自动批准 MEDIUM 风险操作,不需要用户每次确认 +5. **Denial tracking**:连续 3 次拒绝自动降级权限,防止烦人的重复提示 +6. **路径安全**:多层防御(normalization, URL decode, Unicode clean) +7. **Protected files**:黑名单,永远要再次确认 + +--- + +## 深入阅读 + +- `utils/permissions/permissions.ts`:完整决策逻辑 +- `utils/permissions/yoloClassifier.ts`:分类器实现 +- `utils/permissions/pathValidation.ts`:路径安全 +- `utils/permissions/denialTracking.ts`:Denial tracking +- `utils/permissions/PermissionMode.ts`:Mode 定义 + +下一步:了解**Context 和 Memory 系统**,看看当 context window 快满时会发生什么。 diff --git a/docs/agentic-design/05-context-and-memory.md b/docs/agentic-design/05-context-and-memory.md new file mode 100644 index 0000000..2d617e3 --- /dev/null +++ b/docs/agentic-design/05-context-and-memory.md @@ -0,0 +1,416 @@ +# Context 和 Memory:有限的脑容量 + +## 为什么这很重要? + +Claude 的 context window(比如 200K tokens)是**有限的**。一个长时间运行的 agent task: + +``` +第 1 轮:用户输入(200 tokens)+ 响应(1000 tokens) +第 2 轮:新输入(300 tokens)+ 响应(2000 tokens) +... +第 100 轮:? + +总 tokens: 200 + 300 + 400 + ... + 整个历史 + +在第 50 轮时:messages 数组有 100 条消息,超过 context window 的 80% +``` + +一旦超限,系统必须做些什么。Claude Code 的答案是: + +1. **System prompt 优化**:区分静态和动态部分,利用 prompt cache +2. **主动 compaction**:在超限前压缩历史 +3. **长期 memory**:用户的持久化知识库 + +--- + +## System Prompt 架构 + +不是简单的一个大 string,而是**模块化的**: + +```typescript +const systemPrompt = [ + // 第 1 部分:静态,会被缓存 + SYSTEM_PROMPT_PREFIX, // 关于 Claude Code 的基础指导 + TOOL_DESCRIPTIONS, // 所有 tool 的描述 + SYSTEM_PROMPT_SAFETY, // 安全指导 + + // 缓存边界 ← 这之后的部分会变化,不会被缓存 + "SYSTEM_PROMPT_DYNAMIC_BOUNDARY", + + // 第 2 部分:动态,每个 session 不同 + USER_MEMORY, // 用户的个人知识库 + RECENT_CONTEXT, // 最近的几轮对话(总结) + CURRENT_TASK, // 当前任务的上下文 +] +``` + +**Cache boundary marker** 的妙处: + +``` +初始化时: │首个请求: +cache_key = MD5(prefix) │所有文本 +缓存大小:10 KB │2000 tokens (40 KB) + │ + │缓存命中 10 KB + │处理 30 KB 新文本 + │总成本:40 KB 请求 + │ +同一 session 中再次使用: │再次请求: +cache 仍有效 │新文本 1000 tokens (20 KB) +直接跳过了 10 KB 的重复传输 │缓存 hit(10 KB) +成本:20 KB │总成本:20 KB 请求(节省 10 KB) +``` + +所以,通过把 system prompt 分为静态和动态两部分,我们: +- 减少了重复 token 消耗 +- 加快了推理速度(缓存的部分直接使用) +- 提高了吞吐量(多个请求共享缓存) + +相关文件:`utils/messages/systemInit.ts`, `context.ts` + +--- + +## Token Budget 生命周期 + +### 阶段 1:初始化 + +```typescript +const CONTEXT_WINDOW = 200_000 // 比如 200K tokens +const SYSTEM_PROMPT_TOKENS = 5_000 +const RESERVED_BUFFER = 10_000 // 总是留出来,防止意外超限 + +available = CONTEXT_WINDOW - SYSTEM_PROMPT_TOKENS - RESERVED_BUFFER + = 185_000 tokens // 工作预算 +``` + +### 阶段 2:追踪使用 + +每一轮 API 调用后,系统获得实际使用数据: + +```typescript +response = await claude.messages.create({...}) + +remaining = available - response.usage.input_tokens - response.usage.output_tokens + +if (remaining < available * 0.2) { // 还剩 20% 以下? + console.warn("Token budget 即将耗尽,触发 compaction") +} + +if (remaining < 10_000) { // 紧急状态? + console.error("无法继续,context 严重超限") + return ERROR_OUT_OF_TOKENS +} +``` + +### 阶段 3:警告和恢复 + +``` +Threshold 1(80% 已用):显示警告,继续 + ↓ +Agent 执行更多 tool ← 看不到警告,继续工作 + ↓ +Threshold 2(90% 已用):触发 compaction + ← 在后台自动压缩,不中断 agent 工作 + ↓ +Threshold 3(99% 已用):停止接受新请求 + ← 返回错误,等待用户决定 +``` + +相关文件:`query/tokenBudget.ts` + +--- + +## Compaction:自动历史压缩 + +当 token 预算吃紧时,系统自动压缩历史。这是一个 4 步流程: + +### 第 1 步:分析历史 + +```typescript +// 扫描所有消息,找出"能压缩的部分" + +messages = [ + system_prompt, // 不能压缩 + message_1, // 用户说:"帮我读个文件" → 不压缩(用户消息) + message_2, // Claude 说:"我读了,代码如下..." → 能压缩(输出很长) + message_3, // 用户说:"现在修复 bug" → 不压缩 + message_4, // Claude 说:"修复完,改了 5 处..." → 能压缩 + ... +] + +优先级:越早的消息优先级越高(假设最早的对话不再相关) +``` + +### 第 2 步:优先排序 + +``` +对能压缩的消息排序: + 1. 最早的长消息(4 messages ago) + 2. 次早的长消息(6 messages ago) + 3. ... + +实际压缩的可能是: + messages 1-10(最早的 10 条)被替换为 1 条 summary +``` + +### 第 3 步:调用 Claude 做总结 + +```typescript +const summary = await claude.messages.create({ + messages: [ + { role: "user", content: "Summarize this conversation in 200 lines max:\n" + messagesText } + ], + system: "You are a memory consolidation assistant..." +}) + +// 返回: +// "用户让我读了 package.json 和 tsconfig.json,发现项目是 TypeScript monorepo。" +``` + +### 第 4 步:替换 + +```typescript +// 替换前: +messages = [msg_1, msg_2, ..., msg_100] // 100 条消息 + +// 替换后: +messages = [msg_summary, msg_51, msg_52, ..., msg_100] +// ^被压缩成1条 ^保留后面的消息(近期的) +``` + +结果:从 100 条消息减少到 51 条,token 预算恢复。 + +相关文件:`query.ts` 中的 `compactMessages()` 函数,大约 200 行 + +--- + +## Memory 系统:长期知识库 + +Compaction 是**临时的**(压缩 session 内的历史)。但如果用户有**多个 session**,同一信息会被重复压缩。 + +解决方案:**持久化 memory**。 + +### Memory 文件结构 + +``` +~/.claude/memory/ +├── MEMORY.md ← 用户的长期知识库(最多 200 行) +├── projects/ +│ └── claude-code.md ← 项目特定的知识 +├── patterns/ +│ └── async-patterns.md ← 学到的模式 +└── people/ + └── team.md ← 关于人的信息 +``` + +**MEMORY.md** 的内容示例: + +```markdown +# 我的知识库 + +## 工作偏好 +- 不喜欢在 main 分支上直接提交 +- 总是用 TypeScript,避免 JavaScript + +## 项目信息 +- Claude Code 仓库: ~/projects/claude-code +- 技术栈: TypeScript + React + Ink + +## 编码习惯 +- 函数 <100 行为最佳 +- 避免 deep nesting,最多 3 层 +- 总是加 error handling +``` + +### Auto-Dream:后台内存巩固 + +不是用户手动维护 MEMORY.md,而是系统**自动**从 session 中提取和更新。 + +过程("dream"): + +``` +触发条件(三门齐全): + 1. 时间门:距离上次 dream ≥ 24 小时? + 2. 会话门:距离上次 dream,产生了 ≥ 5 个新 session? + 3. Lock 门:能获得 consolidation lock(防并发)? + +都满足 → 执行 dream: + +[1] ORIENT + 读取用户的 MEMORY.md + 扫描最近 N 个 session 的 transcripts + +[2] GATHER + 提取新的学习点: + - "用户在这个 session 中学到了异步 Rust" + - "发现项目从 4.1 升级到了 4.2" + - "优化了 CI/CD,速度提升 30%" + +[3] CONSOLIDATE + 合并到 MEMORY.md: + - 如果已经有同类笔记,合并 + - 如果是新的,添加 + - 保持在 200 行以内(做优先级选择,删除过时的) + +[4] PRUNE & INDEX + - 删除 24 小时内不用的 session transcript + - 更新搜索索引(便于后续 memory 查询) +``` + +相关文件: +- `services/autoDream/autoDream.ts`:主逻辑(~11 KB) +- `services/autoDream/consolidationPrompt.ts`:告诉 Claude 怎么总结 +- `memdir/memdir.ts`:Memory 目录管理 + +### 为什么自动化很关键? + +``` +手动维护: + 用户要记得更新 MEMORY.md + → 大部分人会忘记 + → Memory 逐渐变陈旧 + +自动化: + 后台每 24 小时自动 consolidate + → 用户无感 + → Memory 总是最新的 +``` + +--- + +## Context 使用的优化层级 + +当 context 变紧时,系统依次采取行动: + +``` +Tier 1(还有 70% token) + ✓ 正常工作 + ✗ 触发自动 compaction + +Tier 2(还有 20% token) + ✓ 继续工作(使用压缩后的历史) + ✗ 不接受新的大型工具输出(如读 10 MB 文件) + +Tier 3(还有 10% token) + ✓ 只允许低 token 成本的操作 + ✗ 禁止高 token 成本的操作 + +Tier 4(还有 < 5% token) + ✗ 停止工作,返回错误 + → 用户可以: + a) 启动新 session(重置 token 预算) + b) 手动编辑 MEMORY.md,删除过时信息 +``` + +--- + +## Design Decision 专栏 + +### 为什么区分静态和动态 system prompt? + +不区分(一大块): +``` +发第 1 个请求:发 5000 tokens 的 system prompt + 输入 +发第 2 个请求:又发 5000 tokens 的 system prompt + 输入 +... +浪费了大量 token 在重复发送 +``` + +区分(cache boundary): +``` +发第 1 个请求:发 5000 tokens(静态)+ 1000 tokens(动态) + API 缓存这 5000 tokens +发第 2 个请求:直接用缓存的 5000 tokens + 1000 tokens(新的动态) + 节省了 5000 tokens! +``` + +如果 session 很长(100 次请求),节省 = 5000 * 99 = 495K tokens! + +### 为什么 Compaction 触发阈值是 20%,而不是 1%? + +等到 1% 时才 compact: +``` +已用 99%,只有 1% 剩余(~2000 tokens) +这时启动 compaction,自己就需要消耗大量 tokens(调用 Claude 做总结) +可能没有足够 tokens 来完成 compaction +→ 失败,系统崩溃 +``` + +提前到 20% 时 compact: +``` +已用 80%,还有 20% 剩余(~40K tokens) +这时启动 compaction,有充足 tokens 来做总结 +完成后,token 预算恢复到 50% +→ 继续工作,很多时间都有富足的 token 可用 +``` + +权衡:早点 compact 意味着多付出一些 token(压缩的成本),但换来系统稳定性。 + +### 为什么用后台 dream 而不是 inline compaction? + +Inline(同步): +``` +在 query loop 中调用 compaction + ↓ +等待 compaction 完成(5-10 秒) + ↓ +用户看到卡顿 +``` + +后台 dream: +``` +query loop 继续运行 + ↓ +在空闲时或定时触发 dream(24 小时一次) + ↓ +用户感觉不到延迟 +``` + +代价:增加系统复杂性(需要处理并发)、需要文件锁(防同时 dream)。但值得,因为用户体验好得多。 + +--- + +## 常见误解 + +**误解 1**:"Context window 满了就完蛋?" + +实际:有多层缓冲和恢复机制。系统会逐步: +1. 显示警告 +2. 触发 compaction +3. 拒绝新操作 +4. 要求用户决定 + +完全"卡住"很少发生。 + +**误解 2**:"Compaction 会丢失所有细节?" + +实际:Compaction 由 Claude 做,所以**关键信息被保留**。丢失的是: +- 冗余的解释 +- 尝试失败的细节(对后续不相关) +- 已解决的问题的讨论过程 + +**误解 3**:"MEMORY.md 会无限增长?" + +实际:维持在 ~200 行(可配置),通过 dream 的 prune 阶段定期清理过时信息。 + +--- + +## 关键要点 + +1. **System prompt 分两部分**:静态(可缓存)+ 动态(每次变化) +2. **Token budget 主动追踪**:到达 80% 时触发 compaction +3. **Compaction 自动压缩历史**:用 Claude 总结 N 条消息为 1 条 +4. **Memory 系统持久化知识**:MEMORY.md 最多 200 行,用户的长期知识库 +5. **Auto-dream 后台巩固**:每 24 小时自动更新 MEMORY.md,无需用户介入 +6. **优化层级**:70% → 20% → 10% → 5% → 停止,每个阈值有对应策略 + +--- + +## 深入阅读 + +- `query/tokenBudget.ts`:Token 追踪逻辑 +- `services/autoDream/autoDream.ts`:Dream 实现 +- `memdir/memdir.ts`:Memory 目录管理 +- `utils/messages/systemInit.ts`:System prompt 组装 + +下一步:了解**Feature Gating 系统**,看看 Claude Code 怎么区分内部功能和公开功能。 diff --git a/docs/agentic-design/06-feature-gating.md b/docs/agentic-design/06-feature-gating.md new file mode 100644 index 0000000..d743620 --- /dev/null +++ b/docs/agentic-design/06-feature-gating.md @@ -0,0 +1,357 @@ +# Feature Gating:内部功能和公开功能的隔离 + +## 为什么需要 Feature Gating? + +Claude Code 是一个复杂系统,有很多功能: + +- 一些已发布、稳定、所有用户可用 +- 一些还在测试、只给内部用户("ant" 用户) +- 一些只给特定高级用户(Max/Pro) +- 一些通过实验 A/B 测试 + +如果不做隔离,代码会变得一团糟: + +```typescript +// 不好的做法 +if (user.isInternal) { + // KAIROS 模式逻辑 +} else if (user.isMax) { + // 高级功能 +} else if (featureFlags.has('experimental_search')) { + // A/B 测试 +} else { + // 默认行为 +} +// ... 这样嵌套太深,难以维护 +``` + +Claude Code 的解决方案:**编译期和运行期双层 gating**。 + +--- + +## 编译期 Gating:Bun 的 `feature()` API + +Bun(JavaScript 运行时)提供了一个编译期特性检测 API: + +```typescript +if (feature("KAIROS")) { + // 这段代码只在 KAIROS 构建中出现 + // 其他构建完全不包含这代码 +} else { + // 默认行为 +} +``` + +### 工作原理 + +``` +源代码: + if (feature("KAIROS")) { + launchKAIROS() + } else { + launchDefault() + } + ↓ Bun 编译(--feature KAIROS) + if (true) { + launchKAIROS() // 只保留这段 + } else { + // 被 dead code elimination 删除 + } + ↓ 输出的二进制 + launchKAIROS() // 这是最终的可执行代码 +``` + +### 优势 + +1. **二进制体积小**:内部功能完全不包含在公开二进制中 +2. **安全**:用户不能"解锁"内部功能(不存在于二进制) +3. **性能**:不需要运行时检查这些条件 + +相关代码散布在整个 codebase 中,比如 `entrypoints/cli.tsx`, `bootstrap/state.ts`。 + +--- + +## Compile-Time Feature 列表 + +Claude Code 在构建时支持这些 feature flag: + +| Flag | 启用时 | 作用 | +|------|--------|------| +| `KAIROS` | 启动 KAIROS mode(always-on Claude) | 全天候 assistant | +| `PROACTIVE` | 同上(别名) | 同上 | +| `BRIDGE_MODE` | 启用 claude.ai 远程控制 | 在 Web UI 中控制 CLI | +| `VOICE_MODE` | 启用语音输入 | 用话筒说指令 | +| `DAEMON` | 启用 daemon 模式 | 后台运行的 Claude | +| `COORDINATOR_MODE` | 启用 coordinator 多 agent | 复杂任务分解 | +| `BUDDY` | 启用 Tamagotchi 伴侣系统 | 养一个宠物 AI | +| `WORKFLOW_SCRIPTS` | 启用工作流脚本 | 自动化重复任务 | +| `NATIVE_CLIENT_ATTESTATION` | 启用本地证明 | 设备信任 | +| `TRANSCRIPT_CLASSIFIER` | 启用 AFK 自动模式 | 离开电脑时自动工作 | +| `HISTORY_SNIP` | 启用历史压缩优化 | Context 更高效 | +| `EXPERIMENTAL_SKILL_SEARCH` | 启用技能搜索(实验) | 发现新 skill 命令 | +| `ABLATION_BASELINE` | 科学研究 baseline | 论文用 | + +默认只编译:`KAIROS=0, BRIDGE_MODE=0, ...`(全是 0,最小二进制) + +内部构建时:编译脚本设置 `--feature KAIROS=1 --feature BRIDGE_MODE=1 ...` + +相关文件:构建脚本(如 `scripts/build.sh`,如果存在),或 `bunfig.toml` + +--- + +## 运行期 Gating:GrowthBook Feature Flags + +编译期 feature 决定"可能性",运行期 flags 决定"激活"。 + +系统启动时,从 GrowthBook(特性管理平台)获取动态配置: + +```typescript +// 启动时 +const flags = await growthbook.getFeatures({ + userId: currentUser.id, + organization: currentUser.org +}) + +// 现在可以做运行期决策 +if (flags.has('tengu_scratch')) { + // 启用共享 scratchpad(coordinator 模式) + enableScratchpad() +} + +if (flags.has('tengu_amber_flint')) { + // 启用 in-process teammate swarms + enableTeammates() +} + +if (flags.has('tengu_penguins_off')) { + // 禁用某个实验 + disableExperiment() +} +``` + +### Flag 命名约定 + +Runtime flags 以 `tengu_` 开头("tengu" 是 Claude Code 的内部代号): + +``` +tengu_scratch # 共享 scratchpad(为 coordinator) +tengu_amber_flint # In-process teammate +tengu_onyx_plover # 某个实验 +tengu_penguins_off # 禁用什么功能 +... +``` + +这样容易区分: +- `KAIROS`(编译期)vs `tengu_kairos_v2`(运行期) + +相关文件:`bootstrap/state.ts`(初始化),`services/analytics/`(GrowthBook 集成) + +--- + +## 两层系统的协作 + +### 场景 1:Compile-Time Only(最常见) + +``` +新功能 KAIROS(always-on Claude) + ↓ 编译 + ├─ 构建 ant(内部):--feature KAIROS=1 + │ → KAIROS 代码被编译进去 + │ + └─ 构建 public(公开):--feature KAIROS=0 + → KAIROS 代码被 dead-code-elimination 删除 + → 二进制不含任何痕迹 +``` + +### 场景 2:Compile-Time + Runtime(用于 A/B 测试) + +``` +新的 memory 系统(已编译进去) + ↓ 运行时 + ├─ 用户在 tengu_new_memory=true 组(50%) + │ → 使用新系统 + │ + └─ 用户在 tengu_new_memory=false 组(50%) + → 使用旧系统 + → 收集对比数据 +``` + +### 场景 3:运行期特定用户 + +``` +高级功能(编译期编译进去) + ↓ 运行时 + ├─ Max 订阅用户 → 启用计算机使用 + ├─ Pro 订阅用户 → 启用高级 memory + └─ 免费用户 → 基础功能 +``` + +相关文件:`services/policyLimits/`(根据订阅级别应用 policy) + +--- + +## 对 System Prompt 的影响 + +Feature gating 会改变 system prompt 的内容: + +```typescript +const systemPrompt = [ + BASE_PROMPT, + + // 只有在编译时启用 BRIDGE_MODE 时才包含 + feature("BRIDGE_MODE") ? BRIDGE_MODE_INSTRUCTIONS : "", + + // 运行时检查:如果用户有 tengu_coordinator flag + flags.get("tengu_coordinator") ? COORDINATOR_INSTRUCTIONS : "", + + // 运行时 API beta 特性 + BETAS_NEGOTIATED_WITH_API, // 如 "interleaved_thinking" +] +``` + +这也影响 **prompt cache**: + +``` +缓存键 = MD5(system_prompt_prefix) + +如果 flag 变化,cache key 变化,旧 cache 失效 +``` + +所以,高频变化的 runtime flag 应该放在 cache boundary 之后(动态部分),而不是之前(静态部分)。 + +--- + +## Betas 协商 + +Claude API 定期发布新特性(beta),Claude Code 需要: + +1. 请求启用这些 beta +2. 如果启用,在 system prompt 中告诉 Claude(这个 API 支持什么新功能) + +例子: + +```typescript +// constants/betas.ts +const BETAS_REQUESTED = [ + "interleaved-thinking", // 支持在回复中穿插思考 + "structured-outputs", // 支持返回结构化 JSON + "context-1m", // 100 万 token context + "web-search", // 网络搜索能力 +] + +// API 返回确认 +const BETAS_ENABLED = [ + "interleaved-thinking", // ✓ 已启用 + // "structured-outputs", // ✗ 暂无权限 + "context-1m", // ✓ 已启用 + "web-search", // ✓ 已启用 +] + +// System prompt 中加入这些信息 +You have access to the following beta features: + - Interleaved thinking: You can output blocks + - Context 1M: You can use up to 1,000,000 tokens + - Web search: You can call the WebSearchTool +``` + +相关文件:`constants/betas.ts` + +--- + +## Design Decision 专栏 + +### 为什么同时用编译期和运行期? + +只用编译期: +``` +缺点:无法做快速的 A/B 测试(需要重新编译) + 无法根据用户身份动态启用功能 +``` + +只用运行期: +``` +缺点:内部功能可能被反编译/逆向工程 + 内部代码暴露在公开二进制中 + 启动时需要网络请求拿 flags(慢) +``` + +两者结合: +``` +编译期决定"可能性"(物理隔离内部代码) +运行期决定"激活"(灵活的 A/B 测试) +最安全、最灵活 +``` + +### 为什么 Bun 的 feature() 而不是其他工具? + +Bun 的 feature() 是编译期指令,会进行 dead code elimination: + +``` +其他工具(如 rollup 的条件编译): + 需要额外的 webpack 插件 + 编译配置复杂 + +Bun: + 原生支持 + 编译快(Bun 本身就快) + 输出体积最小 +``` + +--- + +## 常见误解 + +**误解 1**:"Runtime flag 可以改变编译期行为?" + +实际:不能。Runtime flag 只能在编译期已包含的代码中做选择。如果代码在编译时被 dead-code-elimination 删除了,运行时再想启用也没办法。 + +```typescript +// 如果编译时 --feature KAIROS=0,这段代码被删除 +if (feature("KAIROS")) { + launchKAIROS() +} + +// 运行时即使 growthbook.flags.get("enable_kairos") == true,也没用 +// 代码不存在于二进制中 +``` + +**误解 2**:"Feature flag 对性能有开销?" + +实际:编译期 feature flag 没有开销(代码级别选择)。运行期 flag 有极小开销(map lookup),可忽略不计。 + +**误解 3**:"所有用户都能看到内部代码?" + +实际:不能。公开二进制中编译时被删除的代码,用户看不到。只有 ant(内部)用户的二进制才包含这些代码。 + +--- + +## 关键要点 + +1. **编译期 feature**:Bun 的 `feature()` API,dead code elimination,对二进制体积和安全性的保障 +2. **运行期 flags**:GrowthBook,动态启用特性,支持 A/B 测试 +3. **两层协作**:编译决定可能性,运行时决定激活 +4. **System prompt 影响**:不同 flag 组合 → 不同 system prompt → 不同 cache key +5. **API Beta**:协商启用新的 Claude API 特性,在 system prompt 中告诉 Claude + +--- + +## 深入阅读 + +- `bootstrap/state.ts`:运行时 flag 初始化(56 KB) +- `constants/betas.ts`:Beta feature 列表 +- `constants/system.ts` 和 `constants/systemPromptSections.ts`:System prompt 组装 +- `entrypoints/cli.tsx`:编译期条件编译示例 + +--- + +## 后记 + +这 6 篇文档覆盖了 Claude Code 最核心的 agentic 设计决策。还有很多其他话题(Bridge 模式、Voice 输入、Plugin 系统等),但这些是最重要的基础。 + +如果你想进一步了解,建议: +1. 读完这 6 篇 +2. 打开 `query.ts` 和 `Tool.ts`,对照源代码 +3. 运行 Claude Code,用 `--verbose` flag 看内部日志 +4. 探索 `tools/AgentTool/` 的源代码 + +祝学习愉快! diff --git a/docs/agentic-design/README.md b/docs/agentic-design/README.md new file mode 100644 index 0000000..a0ca978 --- /dev/null +++ b/docs/agentic-design/README.md @@ -0,0 +1,113 @@ +# Claude Code Agentic 设计文档 + +> **目标读者**:有一定软件工程背景、希望通过真实项目理解 agentic 系统设计的学习者。 +> **建议用时**:约 2.5 小时 +> **阅读语言**:中文正文 + 英文技术术语 + +--- + +## 这个项目是什么? + +[Claude Code](https://claude.ai/code) 是 Anthropic 开发的 AI 辅助编程 CLI 工具。它不是一个简单的"问答机器人",而是一个完整的 **agentic system**: + +- 它可以自主执行多轮工具调用(读文件、改代码、运行命令) +- 它可以 spawn 子 agent 并行处理复杂任务 +- 它有 permission 管控、memory 管理、context 压缩等完整的 agent 基础设施 + +这套文档通过分析 Claude Code 的源代码,提炼出其中最有价值的 agentic 设计选择,帮助你建立对真实 agent 系统的直觉。 + +--- + +## 整体架构 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 用户 / IDE / SDK │ +└────────────────────────────┬────────────────────────────────────┘ + │ 输入:用户消息 + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ CLI / Entrypoint Layer │ +│ entrypoints/cli.tsx │ entrypoints/sdk/ │ entrypoints/mcp/ │ +└────────────────────────────┬────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ Query Loop Engine │ +│ QueryEngine.ts ←→ query.ts ←→ query/ │ +│ (状态机: QUERY → TOOL_USE → RESULT → QUERY → ...) │ +└──────────────┬────────────────────────────────┬─────────────────┘ + │ │ + ▼ ▼ +┌──────────────────────────┐ ┌───────────────────────────────┐ +│ Claude API (Streaming) │ │ Tool Execution Layer │ +│ services/api/ │ │ services/tools/ │ +│ • token budget 追踪 │ │ • StreamingToolExecutor │ +│ • compaction 触发 │ │ • 并发控制 + sibling abort │ +└──────────────────────────┘ └──────────────┬────────────────┘ + │ + ┌──────────────────┼──────────────────┐ + │ │ │ + ▼ ▼ ▼ + ┌─────────┐ ┌──────────┐ ┌──────────────┐ + │ File │ │ Bash / │ │ AgentTool │ + │ Tools │ │ Shell │ │ (子 agent) │ + └─────────┘ └──────────┘ └──────┬───────┘ + │ + ┌────────────────────────┘ + │ spawn + ▼ + ┌────────────────────────┐ + │ 子 Agent / Coordinator│ + │ coordinator/ │ + │ tasks/Local|Remote │ + └────────────────────────┘ + +┌─────────────────────────────────────────────────────────────────┐ +│ 横切关注点(Cross-cutting) │ +│ Permission System │ Context/Memory │ Feature Gating │ +│ utils/permissions/ │ memdir/ autoDream│ bootstrap/state.ts │ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 核心设计哲学 + +**1. Tool 是 Agent 触碰世界的唯一手段** +Agent 的所有副作用(写文件、执行命令、发消息)都必须经过 Tool 接口,这使得权限控制、审计日志、测试 mock 都可以在一个地方统一处理。 + +**2. Text is the protocol** +Agent 之间通过 `` XML 消息通信,memory 以 Markdown 文件存储,permission rules 是 `Bash(git *)` 这样的字符串模式。用文本作为协议,意味着 AI 模型自己也可以读懂并调试这些通信内容。 + +**3. Failure modes are first-class** +每个长时间运行的操作都有明确的 abort 路径,例如:连续 3 次 permission denial 就回退到手动提示;并行 tool 中一个失败会触发 sibling abort;compaction 失败有 rollback 保护。 + +**4. Context window 是最稀缺的资源** +整个系统的大量设计决策(system prompt 的静态/动态分割、token budget 追踪、auto-dream 后台压缩)都围绕着"如何最大化利用有限的 context window"展开。 + +**5. 编译期与运行期特性隔离** +内部功能通过 Bun 的 `feature()` API 在编译时消除,不出现在公开二进制文件中;运行时行为通过 GrowthBook runtime flags 动态控制。 + +--- + +## 建议阅读顺序 + +| 编号 | 文档 | 用时 | 核心问题 | +|------|------|------|---------| +| [00](./00-codebase-tour.md) | 代码库目录全景 | 30 min | "这个 repo 里有什么?" | +| [01](./01-agent-loop.md) | 核心 Query Loop | 20 min | "Agent 是如何循环运作的?" | +| [02](./02-tool-system.md) | Tool 系统 | 20 min | "Tool 是怎么被定义和执行的?" | +| [03](./03-multi-agent-coordination.md) | 多 Agent 协调 | 25 min | "多个 agent 怎么协作?" | +| [04](./04-permission-system.md) | Permission 系统 | 20 min | "Agent 怎么知道自己能做什么?" | +| [05](./05-context-and-memory.md) | Context 与 Memory | 20 min | "Context window 满了怎么办?" | +| [06](./06-feature-gating.md) | Feature Flag 系统 | 10 min | "内部功能是怎么隐藏的?" | + +--- + +## 如何使用这套文档 + +- **按顺序读**:00 → 01 → 02 → 03 → 04 → 05 → 06,每篇都假设你已读过前面的内容 +- **跳读**:如果你已熟悉某个概念,可以直接跳到感兴趣的章节 +- **对照代码**:每篇文档都标注了关键源文件路径,随时可以打开对照阅读 +- **关注 Design Decision 专栏**:这些是最有学习价值的地方,解释了"为什么这样设计"而不只是"是什么"