Claude Code Harness Agent 架构深度解析

4 阅读18分钟

Claude Code Harness Agent 架构深度解析

基于 @anthropic-ai/claude-code@2.1.88 源码逆向分析
核心目的:掌握构建 Harness Agent 所需的知识和最佳实践


达芬奇提问:直达本质

在分析 1382 个源文件之前,先问出最根本的问题:

第一性问题:什么是 Harness Agent?

"Agent 不是模型,Agent 是模型 + 循环 + 工具 + 判断。"

一个 Harness Agent 本质上回答一个问题:如何让 LLM 在现实世界中可靠地行动? 这要求解决五个子问题:

  1. 循环问题:模型何时该继续调用工具,何时该停下?(Agent Loop)
  2. 能力问题:模型能做什么,怎么做?(Tool System)
  3. 安全问题:谁决定模型能不能做?(Permission System)
  4. 记忆问题:上下文装不下怎么办?(Context Management)
  5. 可靠性问题:失败了怎么办?(Error Recovery & Retry)

Claude Code 对这五个问题给出了工程级的答案。以下逐一拆解。


一、全局架构概览

┌────────────────────────────────────────────────────────────┐
│                      User Interface                        │
│  REPL (Ink/React) │ CLI │ SDK │ Bridge (IDE/Web)           │
└──────────┬─────────────────────────────────────────────────┘
           │ UserMessage
           ▼
┌─────────────────────────────────────────────────────────────┐
│                     Agent Loop (query.ts)                   │
│  ┌───────────┐  ┌───────────┐  ┌──────────────────────────┐ │
│  │ Context   │  │ API Call  │  │ Tool Execution           │ │
│  │ Mgmt      │  │ + Stream  │  │ (Parallel/Serial)        │ │
│  │           │  │           │  │                          │ │
│  │ compact() │  │ callModel │  │ StreamingToolExecutor    │ │
│  │ snip()    │  │ withRetry │  │ runTools()               │ │
│  │ collapse()│  │ SSE parse │  │                          │ │
│  └───────────┘  └───────────┘  └───────┬──────────────────┘ │
│                                        │                    │
│  ┌─────────────────────────────────────┼──────────────────┐ │
│  │          Permission Gate            │                  │ │
│  │  Rules → Hooks → Classifier → User  │                  │ │
│  └─────────────────────────────────────┼──────────────────┘ │
│                                        │                    │
│  ┌─────────────────────────────────────┼──────────────────┐ │
│  │              Hooks System           │                  │ │
│  │  PreToolUse → PostToolUse → Stop    │                  │ │
│  │  SessionStart → FileChanged → ...   │                  │ │
│  └─────────────────────────────────────┘──────────────────┘ │
└─────────────────────────────────────────────────────────────┘
           │                    │
           ▼                    ▼
┌──────────────────┐  ┌───────────────────────────┐
│  Claude API      │  │  External Systems         │
│  (1P/Bedrock/    │  │  - File System            │
│   Vertex/Azure)  │  │  - Shell/Process          │
│                  │  │  - MCP Servers            │
│  SSE Streaming   │  │  - LSP Servers            │
│  Retry + Backoff │  │  - Git/GitHub             │
└──────────────────┘  └───────────────────────────┘

源码规模

目录文件数职责
src/utils/564工具函数、权限、Hook、Shell、MCP
src/components/389Ink/React 终端 UI 组件
src/commands/20780+ CLI 命令实现
src/tools/18465+ 工具实现
src/services/130API、MCP、Compact、Analytics
src/hooks/104React Hooks(UI 状态)
总计~1382

二、Agent Loop:循环的艺术

达芬奇之问:为什么用 async generator 而不是简单的 while 循环?

答:因为 Agent Loop 同时是生产者(产生流式事件)和消费者(消耗工具结果)。async generator 的 yield 语义天然支持这种双向数据流,让调用方可以实时接收每一个 token、每一个工具进度,而不必等待整个 turn 结束。

2.1 核心控制流

入口src/query.ts — 1729 行,整个 Agent 的心脏

// 外层包装:设置初始状态
export async function* query(params): AsyncGenerator<StreamEvent | Message, Terminal> {
  yield* queryLoop(params)
}

// 内层循环:真正的 Agent Loop
async function* queryLoop(params): AsyncGenerator<...> {
  let state: State = { messages, toolUseContext, turnCount: 1, ... }
  
  while (true) {
    // ① 上下文压缩阶段
    // ② API 调用 + 流式接收阶段
    // ③ 错误恢复阶段
    // ④ 工具执行阶段
    // ⑤ 附件收集阶段(Memory、Skill 预取)
    // ⑥ 最大轮次检查
    // ⑦ 状态更新 → continue(隐式递归)
  }
}

2.2 状态机

type State = {
  messages: Message[]                          // 完整对话历史
  toolUseContext: ToolUseContext               // 工具执行上下文(贯穿整个会话)
  autoCompactTracking: AutoCompactTrackingState // 自动压缩跟踪
  maxOutputTokensRecoveryCount: number         // 输出 token 恢复计数(最多 3 次)
  hasAttemptedReactiveCompact: boolean         // 是否已尝试反应式压缩
  pendingToolUseSummary: Promise<...>          // 待处理的工具使用摘要
  stopHookActive: boolean                      // Stop Hook 是否活跃
  turnCount: number                            // 当前轮次
  transition: Continue | undefined             // 状态转移原因
}

关键设计:状态在 continue 站点被整体替换而非局部修改,保证了每轮迭代开始时状态的一致性。

2.3 一轮完整的 Turn

Turn N 开始
  │
  ├── [1] 上下文管理
  │   ├── applyToolResultBudget()    → 工具结果大小预算
  │   ├── snipCompact()              → 裁剪最旧消息
  │   ├── microcompact()             → API 级缓存编辑(仅 1P)
  │   ├── contextCollapse()          → 分阶段上下文折叠
  │   └── autoCompact()             → 全量摘要生成
  │
  ├── [2] API 调用
  │   ├── normalizeMessagesForAPI()  → 消息标准化
  │   ├── callModel()               → 流式 API 调用
  │   │   └── withRetry()           → 重试 + 指数退避
  │   ├── for await (message of stream)
  │   │   ├── yield 流式消息         → 实时推送给 UI
  │   │   └── StreamingToolExecutor.addTool()  → 边流边执行工具
  │   └── 收集 assistantMessages + toolUseBlocks
  │
  ├── [3] 错误恢复(如果 API 报错)
  │   ├── Prompt Too Long → contextCollapse → reactiveCompact
  │   ├── Max Output Tokens → 升级到 64k → 多轮恢复(最多 3 次)
  │   └── Media Size Error → 剥离图片 → 重试
  │
  ├── [4] 工具执行
  │   ├── StreamingToolExecutor.getRemainingResults()  // 已在流式期间启动
  │   │   或 runTools()                                 // 非流式模式
  │   ├── for await (update of toolUpdates)
  │   │   ├── yield 进度消息
  │   │   └── 收集 toolResults
  │   └── 处理 Stop Hooks(post-sampling)
  │
  ├── [5] 附件收集
  │   ├── 消耗 Memory 预取结果
  │   ├── 消耗 Skill 预取结果
  │   └── 排空命令队列
  │
  └── [6] 决策:继续还是停止?
      ├── needsFollowUp && turnCount < maxTurns → state 更新 → continue
      └── 否则 → return Terminal

2.4 流式工具执行(核心创新)

达芬奇之问:为什么不等模型说完再执行工具?

答:因为模型输出通常需要 5-30 秒。如果工具(如读文件)只需 50ms,那等模型说完再执行白白浪费了几十秒。流式工具执行让工具与模型输出并行,在模型还在输出下一个 tool_use block 时,前一个工具已经完成了。

// StreamingToolExecutor.ts — 核心并发控制
class StreamingToolExecutor {
  addTool(toolBlock, message) {
    this.tools.push({ block: toolBlock, status: 'pending' })
    this.processQueue()  // 立即尝试执行
  }
  
  private canExecuteTool(isConcurrencySafe: boolean): boolean {
    const executing = this.tools.filter(t => t.status === 'executing')
    return (
      executing.length === 0 ||  // 无执行中的工具
      (isConcurrencySafe && executing.every(t => t.isConcurrencySafe))  // 全部并发安全
    )
  }
}

并发策略

  • 只读工具(Read、Glob、Grep):标记为 isConcurrencySafe,可并行执行(最多 10 个)
  • 写入工具(Edit、Write、Bash):独占执行,必须等其他工具完成
  • 并发度上限:环境变量 CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY 控制

三、Tool System:能力的边界

达芬奇之问:工具是名词还是动词?

答:工具同时是声明(schema 告诉模型"我能做什么")和执行(call 方法真正"去做")。这种二元性决定了工具系统必须同时关注模型可见性运行时安全性

3.1 Tool 接口

// src/Tool.ts — 核心接口(362-695 行)
interface Tool<Input extends AnyObject, Output = unknown, P extends ToolProgressData> {
  // === 声明侧(模型看到的) ===
  name: string                          // 唯一标识符
  aliases?: string[]                    // 向后兼容名
  description(): Promise<string>        // 动态描述(可基于上下文变化)
  inputSchema: Input                    // Zod schema(类型安全)
  inputJSONSchema?: ToolInputJSONSchema // JSON Schema(给 MCP 工具用)
  searchHint?: string                   // 工具搜索关键词(3-10 词)
  shouldDefer?: boolean                 // 是否延迟加载 schema
  alwaysLoad?: boolean                  // 始终加载到 prompt
  
  // === 执行侧(运行时调用的) ===
  call(args, context, canUseTool, parentMessage, onProgress?): Promise<ToolResult<Output>>
  validateInput?(input, context): Promise<ValidationResult>
  checkPermissions(input, context): Promise<PermissionResult>
  
  // === 元数据侧(系统决策用的) ===
  isConcurrencySafe(input): boolean     // 是否可并行
  isReadOnly(input): boolean            // 是否只读
  isDestructive?(input): boolean        // 是否有破坏性
  maxResultSizeChars: number            // 结果持久化阈值
  
  // === 渲染侧(UI 显示用的) ===
  renderToolResultMessage?(output, progress, options): ReactNode
  mapToolResultToToolResultBlockParam(data, toolUseID): ToolResultBlockParam
}

3.2 工具注册与过滤

getAllBaseTools()                    // 65+ 工具全集
  │
  ├── 条件导入(feature flags)      // 功能门控
  ├── 懒加载(break circular deps) // 打破循环依赖
  │
  ▼
getTools(permissionContext)         // 按模式过滤
  │
  ├── SIMPLE 模式 → 仅 Bash/Read/Edit
  ├── deny rules → 过滤被禁工具
  │
  ▼
assembleToolPool()                  // 合并内置 + MCP 工具
  │
  ├── 去重(MCP 覆盖同名内置工具)
  ├── 按名称排序(prompt cache 稳定性)
  └── 内置工具保持连续前缀

3.3 工具执行管线

runToolUse(toolUse, assistantMessage, canUseTool, context)
  │
  ├── [1] 查找工具   → findToolByName(),检查 aliases
  ├── [2] 输入验证   → inputSchema.safeParse() → Zod 验证
  │                  → tool.validateInput()      → 语义验证
  ├── [3] 输入回填   → tool.backfillObservableInput() → 补充派生字段
  ├── [4] Pre-Hooks  → runPreToolUseHooks() → 可阻止/修改输入
  ├── [5] 权限检查   → canUseTool() → 多层权限决策
  ├── [6] 执行       → tool.call() → 带进度回调
  ├── [7] 结果映射   → mapToolResultToToolResultBlockParam()
  ├── [8] 结果持久化 → 超过阈值 → 写磁盘 → 返回预览 + 路径引用
  └── [9] Post-Hooks → runPostToolUseHooks() → 可修改输出

3.4 工具结果大小管理

ToolResult<T> = {
  data: T                                           // 实际输出
  newMessages?: Message[]                           // 可选附加消息
  contextModifier?: (ctx) => ToolUseContext          // 上下文变更闭包
  mcpMeta?: { _meta, structuredContent }            // MCP 协议元数据
}

// 持久化策略
if (resultSize > maxResultSizeChars) {
  // 写入 ~/.claude/sessions/{sessionId}/tool-results/{toolUseId}.txt
  // 返回前 2000 字节预览 + 文件路径引用
}

四、Permission System:安全的哲学

达芬奇之问:安全和便利如何平衡?

答:Claude Code 的答案是分层判决:先查规则、再问 Hook、再用分类器、最后问用户。每一层都可以提前终止,避免不必要的交互。这不是"要么全开要么全关",而是一个渐进式信任模型

4.1 权限模式

type PermissionMode =
  | 'default'           // 每个工具都询问用户
  | 'acceptEdits'       // 自动批准编辑,其他询问
  | 'bypassPermissions' // 全部自动批准(需要用户二次确认)
  | 'dontAsk'           // 自动拒绝,不提示
  | 'plan'              // 计划模式(只分析不执行)
  | 'auto'              // ML 分类器决定(内部功能)

4.2 权限判决管线

canUseTool(tool, input, context)
  │
  ├── [1] 工具级禁止    → getDenyRuleForTool()
  │                     → 无内容匹配的全局 deny 规则
  │
  ├── [2] Pre-Hook 决策 → executePreToolHooks()
  │                     → Hook 可返回 allow/deny/ask
  │                     → Hook 可修改输入
  │
  ├── [3] 规则匹配      → checkRuleBasedPermissions()
  │   ├── allow rules  → toolMatchesRule() + 内容匹配
  │   ├── deny rules   → 同上
  │   └── ask rules    → 同上
  │
  ├── [4] 工具自检      → tool.checkPermissions()
  │                     → 每个工具的自定义权限逻辑
  │
  ├── [5] 自动模式分类器 → classifyYoloAction()(仅 Bash)
  │                     → 小模型打分:safe/risky/dangerous
  │
  └── [6] 用户交互      → 弹出权限对话框
                        → 用户可选择:允许/拒绝/始终允许/始终拒绝

4.3 权限规则来源

type PermissionRuleSource =
  | 'userSettings'      // ~/.claude/settings.json
  | 'projectSettings'   // .claude/settings.json(项目级)
  | 'localSettings'     // .claude/settings.local.json
  | 'flagSettings'      // 远程功能标志
  | 'policySettings'    // 企业管理策略
  | 'cliArg'            // 命令行参数 --allow/--deny
  | 'command'           // 命令内置规则
  | 'session'           // 运行时动态添加

// 规则示例
{ source: 'userSettings', ruleBehavior: 'allow', ruleValue: { toolName: 'Bash', ruleContent: 'git *' } }
// → 允许所有以 "git " 开头的 Bash 命令

4.4 权限判决结果

type PermissionDecision =
  | { behavior: 'allow', updatedInput?, decisionReason? }
  | { behavior: 'ask',   message, suggestions?, pendingClassifierCheck? }
  | { behavior: 'deny',  message, decisionReason? }

// 判决原因追踪
type PermissionDecisionReason =
  | { type: 'rule', rule: PermissionRule }        // 规则匹配
  | { type: 'hook', hookName, reason? }           // Hook 决策
  | { type: 'classifier', classifier, reason }    // ML 分类器
  | { type: 'mode', mode: PermissionMode }        // 权限模式
  | { type: 'sandboxOverride', reason }           // 沙箱覆盖
  | { type: 'asyncAgent', reason }                // 异步代理

五、Context Management:记忆的经济学

达芬奇之问:上下文窗口满了怎么办?不是"丢掉什么",而是"保留什么最有价值"。

答:Claude Code 设计了五级压缩体系,从最轻量到最重量,像内存层次结构(L1→L2→L3→磁盘)一样逐级升级。只有低级压缩不够用时才启动高级压缩。

5.1 五级压缩体系

Level 0: 工具结果预算(最轻量)
├── 每条消息的工具结果总字节数限制
├── 超出  持久化到磁盘,返回预览 + 文件引用
└── 成本:几乎为零

Level 1: Snip Compact(裁剪)
├── 从最旧的消息开始移除
├── 保留系统消息和最近 N 
└── 成本:丢失早期上下文

Level 2: Microcompact(API 级缓存编辑)
├──  1P(Anthropic 直连)可用
├── 使用 clear_tool_uses_20250919 策略
├── 清除旧工具调用的输入/输出,保留最近的
├── 阈值:180K tokens  目标压缩到 40K
├── 可清除的工具:Bash, Glob, Grep, Read, WebFetch, WebSearch
├── 不可清除的工具:Edit, Write, NotebookEdit
└── 成本:由 API 侧处理,无额外推理

Level 3: Context Collapse(上下文折叠)
├── 分阶段折叠粒度较高的上下文
├── 保留结构化信息,移除细节
└── 成本:中等推理开销

Level 4: Auto Compact(全量摘要)
├── 使用模型生成完整摘要
├── 最大输出预算:50K tokens
├── Skill 文件截断:每个 5K tokens,总计 25K tokens
├── 插入 SystemCompactBoundaryMessage 标记
└── 成本:一次完整 API 调用

5.2 系统提示构建

buildSystemPromptBlocks()
  │
  ├── 属性头(不缓存)           → 计费标识
  ├── 静态前缀(org 级缓存)      → CLI 系统提示模板
  ├── 工具描述(org 级缓存)      → 65+ 工具的 schema + description
  ├── 系统上下文(不缓存)        → gitStatus, cacheBreaker
  ├── 用户上下文(不缓存)        → CLAUDE.md 内容, currentDate
  └── MCP 工具描述(org 级缓存)  → 外部 MCP 工具的 schema

// 缓存策略:三段式
splitSysPromptPrefix() → [attribution(null), prefix(org), rest(org)]
// 工具排序保证内置工具连续前缀 → 最大化 prompt cache 命中率

5.3 消息标准化

normalizeMessagesForAPI(messages)
  │
  ├── 重排附件         → 上浮直到遇到工具结果或助手消息
  ├── 剥离虚拟消息     → Display-only 消息不发给 API
  ├── 错误清理         → 基于错误文本移除有问题的块(PDF、图片)
  ├── 工具引用处理     → 按 ToolSearch 开关决定保留/剥离
  ├── 连续消息合并     → 合并相邻的 user 消息(Bedrock 要求)
  └── 工具输入标准化   → 移除 API 不兼容的字段

ensureToolResultPairing(messages)
  │
  ├── 跨消息去重       → 追踪 tool_use ID 检测重复
  ├── 孤儿检测
  │   ├── 缺失 tool_result → 创建合成错误块
  │   └── 孤立 tool_result → 剥离
  └── 角色交替维护     → 保持 user-assistant-user 模式

六、Error Recovery & Retry:韧性的工程

达芬奇之问:失败是常态还是异常?

答:在分布式系统中,失败是常态。Claude Code 的设计哲学是永远不在可恢复的错误上崩溃

6.1 API 层重试

// src/services/api/withRetry.ts
async function* withRetry(getClient, operation, options) {
  // 指数退避:500ms → 1s → 2s → 4s → 8s → 16s → 32s(上限)
  // 抖动:±25% 随机,防止惊群效应
  
  // 401/403 → 刷新凭据 → 重试
  // ECONNRESET/EPIPE → 禁用 keep-alive → 重试
  // 429/529 → 尊重 Retry-After 头 → 指数退避
  // Max Output Tokens → 调整 token 限制 → 重试
}

// 529 过载特殊处理
MAX_529_RETRIES = 3  // 最多连续 3 次 529
// 恢复路径:
// 1. 快速模式降级 → 切换标准速度模型
// 2. 模型降级 → 切换到备选模型
// 3. 持久重试 → 无人值守会话无限重试(分块等待)

6.2 查询层错误恢复

API 错误
  │
  ├── Prompt Too Long (413)
  │   ├── [1] Context Collapse Drain → 轻量级,保留粒度
  │   ├── [2] Reactive Compact → 全量摘要生成
  │   └── [3] 上浮错误 → 告知用户
  │
  ├── Max Output Tokens
  │   ├── [1] 升级到 64K tokens(如果之前是 8K)
  │   ├── [2] 多轮恢复(最多 3 次)
  │   └── [3] 上浮错误
  │
  ├── Media Size Error
  │   ├── [1] Reactive Compact(启用时)
  │   └── [2] 剥离图片 → 重试
  │
  └── Stop Hook 阻塞
      └── 将阻塞错误作为用户消息追加 → 模型自行修正

6.3 工具层错误处理

// 工具错误分类
function classifyToolError(error) {
  // TelemetrySafeError → telemetryMessage(安全的错误描述)
  // ENOENT → 文件不存在
  // Error.name → 错误类型名
  // 兜底 → 'Error'
}

// 工具失败后的 Hook
runPostToolUseFailureHooks(toolName, error, context)
// → 允许插件对失败做出反应(日志、清理、重试决策)

七、Hooks System:可扩展性的骨架

达芬奇之问:如何让系统在不修改核心代码的情况下改变行为?

答:Hook 是"观察者模式"在 Agent 架构中的终极实现。它不只是事件监听,还能改变控制流(阻止工具执行)、修改数据流(改变工具输入/输出)、扩展权限(自定义权限判决)。

7.1 Hook 事件完整列表

事件触发时机可影响
PreToolUse工具执行前阻止执行、修改输入、权限决策
PostToolUse工具执行后修改输出、追加上下文
PostToolUseFailure工具执行失败后清理、日志、重试
PermissionDenied自动模式拒绝后自定义处理
PermissionRequest权限请求时自动批准/拒绝、修改权限规则
SessionStart会话开始注入初始上下文、注册文件监视
Setup初始化维护任务
Stop模型停止输出后台任务、记忆提取
SubagentStart子代理启动跟踪、配置
SubagentStop子代理结束清理、记录
FileChanged文件变更按文件名 glob 过滤
CwdChanged工作目录变更环境变量导出
WorktreeCreateWorktree 创建VCS 无关的隔离
WorktreeRemoveWorktree 移除清理
ElicitationMCP 用户输入请求自定义 UI

7.2 四种 Hook 类型

// 1. Command Hook — 最常用
{ type: 'command', command: 'npm test', timeout: 30, async: false }

// 2. Prompt Hook — LLM 驱动
{ type: 'prompt', prompt: 'Is this safe?', model: 'haiku', timeout: 10 }

// 3. HTTP Hook — 外部服务
{ type: 'http', url: 'https://api.example.com/check', 
  allowedEnvVars: ['API_KEY'], timeout: 5 }

// 4. Agent Hook — 代理验证器
{ type: 'agent', prompt: 'Verify this change is safe', timeout: 60 }

7.3 Hook 执行流

executeHooks(hookInput, matchQuery, signal, timeoutMs)
  │
  ├── 信任检查      → 所有 Hook 都需要工作区信任(RCE 防御)
  ├── 匹配 Hook     → getMatchingHooks() → 按事件 + 匹配器过滤
  ├── 并行执行      → 每个 Hook 独立超时
  │   ├── Command → spawn 子进程
  │   ├── Prompt  → 调用 LLM
  │   ├── Agent   → 启动子代理
  │   ├── HTTP    → POST 请求
  │   └── Function → 调用内存回调
  ├── 解析输出      → JSON 验证
  ├── 处理结果      → permissionBehavior / blockingError
  └── 发射事件      → SDK 事件处理器

7.4 Hook 退出码语义(PreToolUse)

退出码含义行为
0检查通过,隐藏继续执行,不显示 Hook 输出
2阻塞停止工具执行,显示 Hook 输出给模型
其他非阻塞错误继续执行,记录错误

八、MCP 集成:能力的无限扩展

达芬奇之问:为什么不把所有工具都内置?

答:因为世界上的工具是无限的,而 Agent 的核心应该是协议而非实现。MCP(Model Context Protocol)让 Agent 可以连接任何符合协议的工具服务器。

8.1 MCP 工具命名

// 内置工具:直接名字
tool.name = 'Bash'

// MCP 工具:三段式命名
tool.name = 'mcp__<serverName>__<toolName>'
// 例:mcp__github__create_issue

// 权限规则匹配
getToolNameForPermissionCheck() → 'mcp__github__create_issue'

8.2 MCP 服务器加载

loadPluginMcpServers()
  │
  ├── 从插件 manifest 加载      → mcpServers 字段
  ├── 从 .mcp.json 加载        → 项目级配置
  ├── 从 MCPB/DXT manifest 加载 → 扩展包
  ├── 冲突解决                  → 最后加载的覆盖
  └── Schema 验证               → McpServerConfigSchema()

8.3 MCP 作为 Server(SDK 模式)

// src/entrypoints/mcp.ts
// Claude Code 本身也可以作为 MCP Server 暴露工具
const server = new Server({ capabilities: { tools: {} } })

// ListTools → 暴露所有内置工具的 schema
// CallTool → 通过 tool.call() 执行

九、Subagent 系统:分治的智慧

达芬奇之问:一个 Agent 不够用怎么办?

答:分治法。主 Agent 可以派生子 Agent,每个子 Agent 有自己的上下文窗口、权限范围和隔离级别。

9.1 子代理类型

// src/tools/AgentTool/AgentTool.tsx
input: {
  subagent_type?: string     // 指定代理类型(general-purpose, code-reviewer, ...)
  isolation?: 'worktree'     // Git worktree 隔离
  mode?: PermissionMode      // 子代理权限模式
  run_in_background?: boolean // 后台运行
}

output:
  | { type: 'completed', result }          // 同步完成
  | { type: 'async_launched', taskId }     // 后台运行
  | { type: 'teammate_spawned', agentId }  // 多代理团队成员
  | { type: 'remote_launched', sessionId } // 远程 CCR 任务

9.2 子代理上下文传递

  • Forked Agent:克隆父上下文 + 缓存(Session Memory 用)
  • Fresh Agent:零上下文,需要完整 briefing(prompt 必须自包含)
  • Worktree Agent:隔离的 Git 副本,独立工作目录

十、构建 Harness Agent 的最佳实践

从 Claude Code 的 1382 个源文件中提炼出的核心模式

10.1 架构原则

原则Claude Code 实现你该怎么做
Async Generator Loopquery()AsyncGenerator<Event, Terminal>用 async generator 作为 Agent Loop 的核心抽象
Streaming-FirstStreamingToolExecutor 边流边执行模型输出与工具执行并行化
Layered PermissionRules → Hooks → Classifier → User构建渐进式信任模型
Graceful Degradation5 级压缩 + 多路径重试永远不在可恢复错误上崩溃
Hook-Driven Extension15+ 事件点 × 4 种 Hook 类型用 Hook 实现行为扩展而非代码修改
Protocol over ImplementationMCP 集成核心是协议,工具是可插拔的
DI for TestabilityQueryDeps 注入用依赖注入隔离外部依赖

10.2 关键设计模式

Pattern 1: Async Generator as Agent Loop
async function* agentLoop(params): AsyncGenerator<Event, Result> {
  let state = initState(params)
  while (true) {
    const response = yield* callModel(state)
    if (!response.hasToolCalls) return finalResult(state)
    const toolResults = yield* executeTools(response.toolCalls)
    state = updateState(state, response, toolResults)
  }
}

// 消费者
for await (const event of agentLoop(params)) {
  renderToUI(event)  // 实时渲染
}
Pattern 2: Tool as First-Class Object
interface Tool<I, O> {
  // 声明 + 执行 + 元数据 + 渲染 四位一体
  schema: ZodSchema<I>          // 声明
  call(input: I): Promise<O>    // 执行
  isConcurrencySafe: boolean    // 元数据
  render(output: O): ReactNode  // 渲染
}
Pattern 3: Permission Pipeline
async function checkPermission(tool, input): Promise<Decision> {
  // 1. 静态规则(O(1))
  const ruleResult = checkRules(tool, input)
  if (ruleResult) return ruleResult
  
  // 2. Hook 决策(O(hook_count))
  const hookResult = await runHooks('PreToolUse', tool, input)
  if (hookResult.decision) return hookResult.decision
  
  // 3. 工具自检(O(1))
  const toolResult = await tool.checkPermissions(input)
  if (toolResult.behavior !== 'ask') return toolResult
  
  // 4. 用户交互(阻塞)
  return await askUser(tool, input)
}
Pattern 4: Tiered Context Compression
async function manageContext(messages, tokenCount) {
  if (tokenCount < BUDGET) return messages           // 不压缩
  
  const snipped = snipOldest(messages)              // Level 1
  if (estimateTokens(snipped) < BUDGET) return snipped
  
  const collapsed = await collapse(snipped)          // Level 2
  if (estimateTokens(collapsed) < BUDGET) return collapsed
  
  return await summarize(collapsed)                  // Level 3
}
Pattern 5: Streaming Tool Execution
class StreamingToolExecutor {
  // 模型还在输出时就开始执行工具
  addTool(block) {
    this.queue.push(block)
    this.tryExecuteNext()  // 立即尝试
  }
  
  private tryExecuteNext() {
    const next = this.queue.find(t => this.canExecute(t))
    if (next) {
      next.status = 'executing'
      next.promise = this.executeTool(next)
    }
  }
}

10.3 从零构建 Harness Agent 的路线图

Phase 1: 最小可行 Agent
├── 实现 async generator agent loop
├── 实现 3 个基本工具(Read, Write, Bash)
├── 实现简单的权限检查(全部询问)
├── 连接 Claude API(流式)
└── 实现基本 REPL

Phase 2: 生产级能力
├── 添加流式工具执行
├── 实现工具并发控制
├── 添加权限规则系统
├── 实现基本上下文压缩
├── 添加重试和错误恢复
└── 实现工具结果持久化

Phase 3: 可扩展架构
├── 实现 Hook 系统(PreToolUse, PostToolUse)
├── 添加 MCP 客户端支持
├── 实现子代理系统
├── 添加 Session Memory
├── 实现多级上下文管理
└── 添加遥测和分析

Phase 4: 企业级特性
├── 多提供商支持(Bedrock, Vertex, Azure)
├── 企业权限策略
├── 团队协作特性
├── 远程执行环境
└── 审计和合规

十一、关键源文件索引

子系统关键文件行数职责
Agent Loopsrc/query.ts1729主循环:上下文管理 → API 调用 → 工具执行 → 状态更新
Tool 定义src/Tool.ts695Tool 接口、ToolUseContext、ToolResult
Tool 注册src/tools.ts~200工具全集、过滤、池组装
Tool 执行src/services/tools/toolExecution.ts1800+完整执行管线
流式执行器src/services/tools/StreamingToolExecutor.ts~300并发控制、边流边执行
工具编排src/services/tools/toolOrchestration.ts~200批次划分、并行/串行执行
API 客户端src/services/api/client.ts~300多提供商、认证、代理
API 调用src/services/api/claude.ts3237+消息构建、系统提示、流式调用
重试逻辑src/services/api/withRetry.ts822指数退避、错误分类、模型降级
权限类型src/types/permissions.ts~300权限模式、规则、决策类型
权限逻辑src/utils/permissions/permissions.ts~300规则匹配、决策逻辑
Hook 类型src/types/hooks.ts~290Hook 事件、回调、结果类型
Hook Schemasrc/schemas/hooks.ts~213Hook 配置 schema(command/prompt/http/agent)
Hook 执行src/utils/hooks.ts3500+核心执行引擎
上下文压缩src/services/compact/compact.ts~150客户端压缩策略
微压缩src/services/compact/apiMicrocompact.ts~150API 级缓存编辑
消息处理src/utils/messages.ts5400+标准化、配对、合并
MCP 集成src/services/mcp/~200MCP 客户端、类型、字符串工具
状态管理src/state/AppStateStore.ts~200全局状态树
会话启动src/utils/sessionStart.ts~200Hook 加载、会话初始化
配置加载src/utils/config.ts~250项目/全局配置

最后的达芬奇之问:Claude Code 最深层的设计直觉是什么?

答:Agent 是一个有限状态机在无限可能性空间中的导航器。 它的每一个设计决策——async generator 的流式控制、分层权限的渐进信任、五级压缩的记忆管理、Hook 系统的行为扩展——都在回答同一个问题:如何让一个有限的系统在无限的可能性中做出可靠的选择。

这不是工程问题,这是一个哲学问题的工程解。