Skill: siyrs/claude-code-patterns (version 1.0.0)

--- name: claude-code-patterns description: Claude Code 源码中提取的 AI Agent 工程模式。当需要设计 agent 调度、工具并发执行、上下文压缩、状态机循环、流式处理等复杂 agent 系统时使用此 skill。触发场景：(1) 设计多轮对话 agent (2) 实现工具并发执行 (3) 构建上下文管理策略 (4) 优化 agent 启动性能 (5) 设计错误恢复机制。 --- # Claude Code 工程模式 Claude Code 是 Anthropic 官方 CLI 产品，其源码展现了业界顶级的 AI Agent 工程实践。本 skill 提炼其核心设计模式。 ## 核心模式概览 | 模式 | 解决的问题 | 适用场景 | |------|-----------|---------| | 启动并行化 | 启动延迟高 | 需要 I/O 预取的 agent 系统 | | 状态机 Query Loop | 递归栈溢出、状态追踪困难 | 多轮对话、复杂任务编排 | | 流式工具并发执行 | 工具执行阻塞模型输出 | 多工具调用场景 | | 多层上下文压缩 | 上下文溢出 | 长对话、大项目 | | 错误恢复 withhold | 中间错误暴露给用户 | 任何可能失败的 API 调用 | | 熔断器 | 失败操作无限重试 | 自动重试逻辑 | | 工具并发安全声明 | 工具执行冲突 | 多工具并发系统 | --- ## 1. 启动并行化 + Profiling **问题**：Agent 启动需要加载配置、连接服务、预取数据，串行执行导致启动慢。 **解决方案**：所有阻塞操作并行化，用 checkpoint 测量瓶颈。 ```typescript // 入口文件最顶部 — 模块加载前就开始计时 profileCheckpoint('main_entry'); // 并行启动所有 I/O const [mdmResult, keychainResult, commandsResult] = await Promise.all([ startMdmRawRead(), // 并行读取 MDM 配置 startKeychainPrefetch(), // 并行预取 keychain getCommands(), // 并行加载命令 ]); profileCheckpoint('all_parallel_done'); ``` **关键技巧**： - 纯文件读取可以在信任对话框显示前开始（无执行风险） - `--bare` 模式跳过所有非必要预取，专为脚本调用优化 - 用 `profileCheckpoint()` 定位瓶颈，不要猜测 **应用建议**： ```typescript // 而不是 await loadConfig(); await connectMCP(); await prefetchMemory(); // 应该 await Promise.all([ loadConfig(), connectMCP(), prefetchMemory(), ]); ``` --- ## 2. 状态机 Query Loop **问题**：多轮对话用递归实现会导致栈溢出，且难以追踪状态变化原因。 **解决方案**：用 `while(true)` + 不可变 State 对象，每次 continue 记录原因。 ```typescript type State = { messages: Message[] toolUseContext: ToolUseContext transition: Continue | undefined // 记录上次 continue 的原因 maxOutputTokensRecoveryCount: number hasAttemptedReactiveCompact: boolean // ... } type Continue = | { reason: 'next_turn' } | { reason: 'max_output_tokens_recovery'; attempt: number } | { reason: 'reactive_compact_retry' } | { reason: 'stop_hook_blocking' } // ... async function* queryLoop(params: QueryParams) { let state: State = initialState(params) while (true) { const { messages, toolUseContext, transition } = state // 执行查询逻辑... if (needsFollowUp) { state = { messages: [...messages, ...toolResults], toolUseContext: updatedContext, transition: { reason: 'next_turn' }, } continue // 不用递归，用 continue } return { reason: 'completed' } } } ``` **为什么不用递归**： - 递归栈会增长，长对话可能溢出 - `transition` 字段让每次 continue 的原因可追踪 - 测试可以断言 `transition.reason` 而不是检查消息内容 **Continue 的各种路径**： | 原因 | 触发条件 | 处理方式 | |------|---------|---------| | `next_turn` | 正常工具调用后 | 合并消息继续 | | `max_output_tokens_recovery` | 输出被截断 | 注入恢复提示 | | `reactive_compact_retry` | 上下文太长 | 压缩后重试 | | `stop_hook_blocking` | hook 要求继续 | 注入 hook 消息 | --- ## 3. 流式工具并发执行（StreamingToolExecutor） **问题**：传统模式是等模型输出完毕再执行工具，浪费大量时间。 **解决方案**：工具在流式输出时就开始执行，结果缓冲后按序 yield。 ```typescript class StreamingToolExecutor { private tools: TrackedTool[] = [] // 添加工具到队列，立即开始执行（如果并发条件允许） addTool(block: ToolUseBlock, assistantMessage: AssistantMessage) { const isConcurrencySafe = this.checkConcurrencySafe(block) this.tools.push({ block, status: 'queued', isConcurrencySafe }) void this.processQueue() // 立即处理，不等待 } // 获取已完成的结果（非阻塞） *getCompletedResults() { for (const tool of this.tools) { if (tool.status === 'completed' && tool.results) { tool.status = 'yielded' yield* tool.results } } } } // 使用方式 for await (const message of callModel()) { if (message.type === 'assistant') { for (const toolBlock of message.toolUseBlocks) { executor.addTool(toolBlock, message) // 立即开始执行 } } // 同时 yield 已完成的结果 for (const result of executor.getCompletedResults()) { yield result } } ``` **并发安全分区**： ```typescript // 工具声明自己是否并发安全 class ReadTool { isConcurrencySafe(input: Input): boolean { return true // 只读，可并发 } } class BashTool { isConcurrencySafe(input: Input): boolean { return false // 可能写文件，串行 } } // 调度器根据声明分区 function partitionToolCalls(tools: ToolUseBlock[]): Batch[] { // 连续的并发安全工具合并成一个 batch // 非并发安全工具单独一个 batch } ``` **Bash 错误级联取消**： ```typescript // Bash 出错时取消所有并行工具 if (tool.block.name === 'BASH' && isErrorResult) { this.hasErrored = true this.siblingAbortController.abort('sibling_error') } ``` --- ## 4. 多层上下文压缩 **问题**：单一压缩策略无法平衡性能和信息保留。 **解决方案**：5 层防御，从轻到重。 ``` Layer 1: History Snip → 删除旧消息（最轻量，~0 cost） Layer 2: Microcompact → 压缩单个工具结果（缓存友好） Layer 3: Context Collapse → 折叠历史段落（保留粒度） Layer 4: Auto Compact → 全量摘要（最重量） Layer 5: Reactive Compact → 被动响应 API 413 错误 ``` **各层触发条件**： | 层 | 触发条件 | 特点 | |---|---------|-----| | Snip | 消息数 > 阈值 | 删除最旧的非关键消息 | | Microcompact | 单个工具结果 > 阈值 | 只压缩该结果，不动其他 | | Collapse | 上下文 > 90% | 折叠旧段落为摘要 | | Auto Compact | 上下文 > 93% | 全量摘要 | | Reactive Compact | API 返回 413 | 最后防线 | **熔断器设计**： ```typescript const MAX_CONSECUTIVE_FAILURES = 3 if (tracking?.consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) { // 停止重试，避免浪费 API 调用 return { wasCompacted: false } } ``` --- ## 5. 错误恢复 Withhold 模式 **问题**：流式输出中遇到可恢复错误，如果直接 yield 给调用方，调用方会终止。 **解决方案**：先 withhold（扣留）错误，尝试恢复，成功则调用方无感知。 ```typescript // 流式输出中 if (isRecoverableError(message)) { withheld = true // 不 yield，先尝试恢复 } // 尝试恢复 const recovered = await tryRecovery() if (recovered) { // 继续新的查询循环，调用方完全不知道出过错 state = { messages: recovered.messages, ... } continue } // 恢复失败才 yield 错误 yield withheldError ``` **适用场景**： - `prompt_too_long` → 尝试压缩后重试 - `max_output_tokens` → 升级 token 限制或注入恢复提示 - `media_size_error` → 压缩图片后重试 --- ## 6. Task 系统的类型设计 **问题**：任务 ID 难以识别类型，日志可读性差。 **解决方案**：Task ID 带类型前缀。 ```typescript const TASK_ID_PREFIXES = { local_bash: 'b', local_agent: 'a', in_process_teammate: 't', remote_agent: 'r', workflow: 'w', monitor: 'm', dream: 'd', } // 生成: "t3f8a2b1c9" 一眼知道是 teammate function generateTaskId(type: TaskType): string { const prefix = TASK_ID_PREFIXES[type] const random = crypto.randomBytes(8).toString('hex').slice(0, 8) return prefix + random } ``` --- ## 7. 其他实用模式 ### 内容哈希替代随机 UUID ```typescript // 避免每次 API 调用都破坏 prompt cache const contentHash = hashContent(settings) const tempFile = `${cacheDir}/settings_${contentHash}.json` ``` ### `using` 关键字管理资源 ```typescript // 自动 dispose，即使发生异常 using pendingMemoryPrefetch = startRelevantMemoryPrefetch(messages) // ... 代码 ... // 离开作用域时自动调用 pendingMemoryPrefetch[Symbol.dispose]() ``` ### Tombstone 消息 ```typescript // fallback 时用 tombstone 标记需要移除的消息，而不是直接删除 yield { type: 'tombstone', message: orphanedMessage } ``` ### Speculation（预测执行） ```typescript // 用户还在打字时，开始预测并执行 type SpeculationState = { status: 'idle' } | { status: 'active' id: string messagesRef: { current: Message[] } timeSavedMs: number // 累计节省的时间 } ``` --- ## 参考文档 - [state-machine-loop.md](references/state-machine-loop.md) — 状态机循环的完整实现 - [streaming-executor.md](references/streaming-executor.md) — 流式执行器的详细设计 - [context-management.md](references/context-management.md) — 上下文压缩的完整策略 --- ## 快速应用清单 设计 AI Agent 系统时，检查以下问题： - [ ] 启动时是否并行化了所有 I/O？ - [ ] 多轮对话是否用状态机而非递归？ - [ ] 工具执行是否可以和模型输出重叠？ - [ ] 工具是否声明了并发安全属性？ - [ ] 是否有多层上下文压缩策略？ - [ ] 可恢复错误是否在内部消化？ - [ ] 重试逻辑是否有熔断器？ - [ ] Task ID 是否包含类型信息？

path: references/context-management.md
content: # 多层上下文压缩策略

本参考文档描述 Claude Code 的 5 层上下文压缩体系。

## 为什么需要多层？

单一压缩策略无法同时满足：
- **低延迟**：压缩越简单越快，但压缩效果差
- **高保真**：保留关键信息，但压缩率低
- **低 API 消耗**：摘要需要额外 API 调用

多层策略按需渐进升级：先尝试轻量手段，撑不住再升级到重量手段。

## 5 层防御体系

```
┌─────────────────────────────────────────────────────────────────┐
│                    上下文使用量                                  │
├─────────────────────────────────────────────────────────────────┤
│  0% ─────┬────────────────────────────────────────────────── │
│           │                                                     │
│  70% ────┼── Snip ────────────────────────────────────────── │
│           │    删除旧消息                                        │
│  80% ────┼── Microcompact ─────────────────────────────────── │
│           │    压缩单个工具结果                                  │
│  90% ────┼── Context Collapse ──────────────────────────────│
│           │    折叠历史段落                                      │
│  93% ────┼── Auto Compact ────────────────────────────────────│
│           │    全量摘要                                          │
│  95% ────┼── [Blocking!] ──────────────────────────────────── │
│           │    禁止继续                                         │
│  100% ───┴── Manual Compact Required ───────────────────────│
└─────────────────────────────────────────────────────────────────┘
         ↑ API 返回 413 时触发 Reactive Compact（跨层）
```

## 各层详解

### Layer 1: History Snip（最轻量）

**原理**：直接删除最旧的非关键消息，不调用 API。

```typescript
function snipCompactIfNeeded(messages: Message[]): {
  messages: Message[]
  tokensFreed: number
  boundaryMessage?: Message
} {
  const MAX_MESSAGES_BEFORE_SNIP = 500
  if (messages.length <= MAX_MESSAGES_BEFORE_SNIP) {
    return { messages, tokensFreed: 0 }
  }

  // 保留最近的 N 条消息和所有 tool_use/tool_result 对
  const recentMessages = messages.slice(-MAX_MESSAGES_BEFORE_SNIP)
  const preserved = preserveToolPairs(recentMessages)

  return {
    messages: preserved,
    tokensFreed: estimateTokensRemoved(messages, preserved),
    boundaryMessage: createBoundaryMessage('snip', tokensFreed),
  }
}
```

**特点**：
- 零 API 消耗
- 删除时保留 tool_use/tool_result 对的完整性
- 适合消息数过多但 token 数不超的场景

### Layer 2: Microcompact（缓存友好）

**原理**：压缩单个超大的 tool_result 内容（如长文件读取），不影响其他消息，不破坏 prompt cache。

```typescript
async function microcompact(
  messages: Message[],
  toolUseContext: ToolUseContext,
  querySource: string,
): Promise<{
  messages: Message[]
  compactionInfo?: {
    pendingCacheEdits: {
      baselineCacheDeletedTokens: number
      trigger: string
      deletedToolIds: string[]
    }
  }
}> {
  const MAX_TOOL_RESULT_CHARS = 50_000
  const compressedMessages: Message[] = []

  for (const msg of messages) {
    if (msg.type === 'user' && hasLargeToolResult(msg)) {
      const compressed = compressToolResult(msg, MAX_TOOL_RESULT_CHARS)
      compressedMessages.push(compressed)
    } else {
      compressedMessages.push(msg)
    }
  }

  return { messages: compressedMessages }
}
```

**特点**：
- 不修改其他消息，prompt cache 命中率保持
- 压缩的是内容，不是结构
- 适合单次工具结果过长但上下文整体不超的场景

### Layer 3: Context Collapse（保留粒度）

**原理**：把历史段落折叠成摘要，但保留近期消息的完整形态。

```typescript
async function applyCollapsesIfNeeded(
  messages: Message[],
  toolUseContext: ToolUseContext,
  querySource: string,
): Promise<{ messages: Message[] }> {
  const collapseThreshold = getContextWindowForModel(model) * 0.9

  if (tokenCount(messages) < collapseThreshold) {
    return { messages }
  }

  // 分段折叠：每 N 条消息压缩成 1 条摘要
  const segments = segmentMessages(messages, segmentSize = 20)
  const collapsedSegments: Message[] = []

  for (const segment of segments) {
    const summary = await generateSummary(segment)  // 调用 API
    collapsedSegments.push(createSummaryMessage(segment, summary))
  }

  // 保留最近 2 段不折叠（保持近期上下文完整）
  return {
    messages: [
      ...collapsedSegments.slice(0, -2),
      ...segments.slice(-2),  // 保留最后 2 段
    ]
  }
}
```

**特点**：
- 折叠后可回滚（摘要消息保留原消息引用）
- 保留最近的完整上下文，模型仍然可以访问最新信息
- 适合中等长度对话

### Layer 4: Auto Compact（全量摘要）

**原理**：调用专门的小模型生成对话摘要，替换整个历史。

```typescript
async function autoCompactIfNeeded(
  messages: Message[],
  toolUseContext: ToolUseContext,
  ...
): Promise<{ compactionResult?: CompactionResult }> {
  const model = toolUseContext.options.mainLoopModel
  const threshold = getAutoCompactThreshold(model)  // 93% 窗口大小

  if (tokenCount(messages) < threshold) {
    return { wasCompacted: false }
  }

  // 调用摘要模型
  const summary = await compactConversation(messages, toolUseContext, {
    isAutoCompact: true,
    suppressUserQuestions: true,
  })

  return {
    compactionResult: {
      summaryMessages: [summary],
      preCompactTokenCount: tokenCount(messages),
      postCompactTokenCount: summary.tokens,
    }
  }
}
```

**特点**：
- 最高的压缩率（可从 200k token 压到 2k）
- 需要额外的 API 调用（使用便宜的小模型）
- 摘要消息保留关键决策和文件路径

### Layer 5: Reactive Compact（被动响应）

**原理**：API 返回 413 时才触发，作为其他策略失效后的最后防线。

```typescript
async function tryReactiveCompact(params): Promise<CompactionResult | null> {
  // 等待 API 返回 413（prompt_too_long）
  const response = await callModel(params)

  if (response.error !== 'prompt_too_long') {
    return null  // 不需要压缩
  }

  // 413 错误，触发紧急压缩
  return await compactConversation(messages, {
    isAutoCompact: false,
    suppressUserQuestions: true,
  })
}
```

**与 Auto Compact 的区别**：
| 方面 | Auto Compact | Reactive Compact |
|------|--------------|-----------------|
| 触发时机 | 上下文达到 93% | API 返回 413 |
| 目的 | 预防性压缩 | 补救性压缩 |
| 失败代价 | 浪费一次 API 调用 | 用户看到错误消息 |

## 熔断器设计

连续失败多次后停止重试：

```typescript
const MAX_CONSECUTIVE_FAILURES = 3

type AutoCompactTrackingState = {
  consecutiveFailures?: number  // 连续失败次数
}

// 在 autoCompactIfNeeded 中
if (tracking?.consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) {
  logForDebugging('Circuit breaker: skipping autocompact after 3 failures')
  return { wasCompacted: false }
}

// 成功后重置
return { wasCompacted: true, consecutiveFailures: 0 }

// 失败后增加
return { wasCompacted: false, consecutiveFailures: prev + 1 }
```

## Withhold 模式

错误不立即暴露，尝试内部恢复：

```typescript
// 流式输出中
if (isPromptTooLong(message)) {
  withheld = true  // 不 yield
}

// 流结束后尝试恢复
if (withheld) {
  const compacted = await tryReactiveCompact(...)
  if (compacted) {
    // 恢复成功，继续新的查询
    state = { messages: compacted.messages, transition: { reason: 'reactive_compact_retry' } }
    continue
  }
  // 恢复失败，暴露错误
  yield withheldMessage
}
```

## 各层触发条件速查

| 层 | 触发条件 | 触发时机 | API 调用 | 压缩率 |
|----|---------|---------|---------|-------|
| Snip | 消息数 > 500 | 每轮之前 | 无 | 低 |
| Microcompact | 单个结果 > 50k chars | 每轮之前 | 无 | 中 |
| Collapse | 上下文 > 90% | 每轮之前 | 是（摘要） | 高 |
| Auto Compact | 上下文 > 93% | 每轮之前 | 是（摘要） | 最高 |
| Reactive | API 413 | API 返回后 | 是（摘要） | 最高 |

## 恢复优先级

当 `prompt_too_long` 发生时，按以下顺序尝试：

1. **Collapse Drain** — 排空已 staged 的折叠（便宜，保持粒度）
2. **Reactive Compact** — 紧急全量摘要
3. **Surface Error** — 放弃，向用户展示错误

## 实现检查清单

- [ ] Snip：消息数超阈值时自动删除旧消息
- [ ] Microcompact：单个工具结果超阈值时压缩
- [ ] Collapse：上下文接近阈值时折叠历史段落
- [ ] Auto Compact：摘要生成前抑制用户问题
- [ ] Reactive Compact：API 错误时立即尝试压缩
- [ ] 熔断器：连续失败 3 次后停止
- [ ] Withhold：错误不立即暴露，尝试内部恢复
- [ ] 重置计数器：成功压缩后重置连续失败计数


path: references/state-machine-loop.md
content: # 状态机 Query Loop 完整实现

本参考文档提供状态机 Query Loop 的完整设计模式和实现细节。

## 核心设计原则

1. **不可变状态** — 每次状态更新创建新对象，不用 `.push()` 等可变操作
2. **单入口循环** — `while(true)` + `continue`，不用递归
3. **显式 transition** — 记录每次循环结束的原因
4. **参数不可变** — 循环外提取 `params`，循环内不修改

## 完整骨架代码

```typescript
// ===================== 状态定义 =====================

type State = {
  messages: Message[]
  toolUseContext: ToolUseContext
  transition: Continue | undefined
  maxOutputTokensRecoveryCount: number
  hasAttemptedReactiveCompact: boolean
  autoCompactTracking?: AutoCompactTrackingState
  turnCount: number
  pendingToolUseSummary?: Promise<ToolUseSummaryMessage | null>
}

type Continue =
  | { reason: 'next_turn' }
  | { reason: 'max_output_tokens_recovery'; attempt: number }
  | { reason: 'max_output_tokens_escalate' }
  | { reason: 'reactive_compact_retry' }
  | { reason: 'collapse_drain_retry'; committed: number }
  | { reason: 'stop_hook_blocking' }
  | { reason: 'token_budget_continuation' }

// ===================== 循环骨架 =====================

export async function* query(
  params: QueryParams,
): AsyncGenerator<StreamEvent, Terminal> {
  const consumedCommandUuids: string[] = []

  try {
    const terminal = yield* queryLoop(params, consumedCommandUuids)
    return terminal
  } finally {
    // 确保清理所有消耗的命令
    for (const uuid of consumedCommandUuids) {
      notifyCommandLifecycle(uuid, 'completed')
    }
  }
}

async function* queryLoop(
  params: QueryParams,
  consumedCommandUuids: string[],
): AsyncGenerator<StreamEvent, Terminal> {
  // === 提取不可变参数 ===
  const {
    systemPrompt,
    userContext,
    systemContext,
    canUseTool,
    fallbackModel,
    querySource,
    maxTurns,
  } = params

  // === 初始化可变状态 ===
  let state: State = {
    messages: params.messages,
    toolUseContext: params.toolUseContext,
    transition: undefined,
    maxOutputTokensRecoveryCount: 0,
    hasAttemptedReactiveCompact: false,
    turnCount: 1,
    pendingToolUseSummary: undefined,
  }

  // === 主循环 ===
  while (true) {
    const {
      messages,
      toolUseContext,
      transition,
      maxOutputTokensRecoveryCount,
      hasAttemptedReactiveCompact,
      turnCount,
    } = state

    // ========== 每个迭代开始 ==========

    // 1. 上下文预处理（压缩、折叠等）
    const { compactionResult, tracking } = await autoCompactIfNeeded(
      messages,
      toolUseContext,
      cacheSafeParams,
      querySource,
      state.autoCompactTracking,
    )

    // 2. API 调用
    const { assistantMessages, toolResults, toolUseBlocks } = 
      await callModelStreaming(messages, toolUseContext)

    // 3. 检查是否需要继续
    if (toolUseBlocks.length === 0) {
      // 无工具调用，检查 stop hooks
      const stopHookResult = yield* handleStopHooks(...)
      if (stopHookResult.preventContinuation) {
        return { reason: 'stop_hook_prevented' }
      }
      if (stopHookResult.blockingErrors.length > 0) {
        // stop hook 要求重试
        state = {
          messages: [...messages, ...assistantMessages, ...stopHookResult.blockingErrors],
          toolUseContext,
          transition: { reason: 'stop_hook_blocking' },
          maxOutputTokensRecoveryCount: 0,
          hasAttemptedReactiveCompact,
          turnCount,
          autoCompactTracking: tracking,
          pendingToolUseSummary: undefined,
        }
        continue
      }
      return { reason: 'completed' }
    }

    // 4. 工具执行
    const toolUpdates = await runTools(toolUseBlocks, ...)
    const toolResults = collectResults(toolUpdates)

    // ========== 继续下一轮 ==========

    state = {
      messages: [...messages, ...assistantMessages, ...toolResults],
      toolUseContext: updatedContext,
      transition: { reason: 'next_turn' },
      maxOutputTokensRecoveryCount: 0,
      hasAttemptedReactiveCompact: false,
      turnCount: turnCount + 1,
      autoCompactTracking: tracking,
      pendingToolUseSummary: nextPendingToolUseSummary,
    }
    continue
  }
}
```

## 各 Transition 的处理逻辑

### `next_turn` — 正常继续

最常见的情况，工具执行完毕，准备下一轮。

```typescript
state = {
  messages: [...messages, ...assistantMessages, ...toolResults],
  toolUseContext: updatedContext,
  transition: { reason: 'next_turn' },
  turnCount: turnCount + 1,
  // 重置恢复计数器
  maxOutputTokensRecoveryCount: 0,
  hasAttemptedReactiveCompact: false,
}
continue
```

### `max_output_tokens_recovery` — 输出被截断

```typescript
const recoveryMessage = createUserMessage({
  content: `Output token limit hit. Resume directly — no apology, no recap. ` +
    `Pick up mid-thought if that is where the cut happened. ` +
    `Break remaining work into smaller pieces.`,
  isMeta: true,
})

state = {
  messages: [...messages, ...assistantMessages, recoveryMessage],
  toolUseContext,
  transition: { reason: 'max_output_tokens_recovery', attempt: count + 1 },
  maxOutputTokensRecoveryCount: count + 1,
  // 注意：不重置 hasAttemptedReactiveCompact
}
continue
```

**为什么最多尝试 3 次**：
```typescript
const MAX_OUTPUT_TOKENS_RECOVERY_LIMIT = 3
if (maxOutputTokensRecoveryCount >= MAX_OUTPUT_TOKENS_RECOVERY_LIMIT) {
  yield lastMessage  // surface the withheld error
  return { reason: 'max_output_tokens' }
}
```

### `max_output_tokens_escalate` — 升级 token 限制

第一次截断时，先尝试升级到更高限制再重试，而不是立即注入恢复消息。

```typescript
// 如果之前用的是 8k 默认值，升级到 64k
if (maxOutputTokensOverride === undefined) {
  state = {
    messages,
    toolUseContext,
    transition: { reason: 'max_output_tokens_escalate' },
    maxOutputTokensOverride: 64_000,  // 升级！
  }
  continue
}
```

### `reactive_compact_retry` — 上下文太长

```typescript
const compacted = await tryReactiveCompact({ hasAttempted, ... })
if (compacted) {
  const postCompactMessages = buildPostCompactMessages(compacted)
  state = {
    messages: postCompactMessages,
    toolUseContext,
    transition: { reason: 'reactive_compact_retry' },
    hasAttemptedReactiveCompact: true,  // 标记已尝试过
    autoCompactTracking: undefined,     // 重置跟踪状态
  }
  continue
}
// 恢复失败，surface 错误
yield lastMessage
return { reason: 'prompt_too_long' }
```

### `collapse_drain_retry` — 先排空折叠再压缩

```typescript
// 优先尝试已有的 collapse（便宜，保持粒度）
const drained = contextCollapse.recoverFromOverflow(messages, querySource)
if (drained.committed > 0) {
  state = {
    messages: drained.messages,
    toolUseContext,
    transition: { reason: 'collapse_drain_retry', committed: drained.committed },
  }
  continue
}
// 没有可排空的折叠，尝试 reactive compact
```

## 状态重置规则

不是所有字段都需要在每次 continue 时重置：

| 字段 | next_turn | compact_retry | stop_hook | token_recovery |
|------|-----------|---------------|-----------|----------------|
| messages | 合并+重置 | 替换 | 合并+重置 | 合并+重置 |
| turnCount | +1 | 不变 | 重置为 1 | 不变 |
| maxOutputTokensRecoveryCount | 重置为 0 | 不变 | 重置为 0 | +1 |
| hasAttemptedReactiveCompact | 重置为 false | 设为 true | 保持 | 保持 |
| autoCompactTracking | 继承/更新 | 重置为 undefined | 继承 | 继承 |

## 测试策略

利用 `transition` 字段做断言，不需要检查消息内容：

```typescript
// ❌ 脆弱的测试：检查消息内容
expect(messages[messages.length - 1]).toContain('Output token limit')

// ✅ 健壮的测试：检查 transition
const terminal = await collectTerminal(query(...))
expect(terminal.transition).toEqual({ reason: 'max_output_tokens_recovery', attempt: 1 })
```

## 常见陷阱

### 1. 不要在循环内重新提取 params

```typescript
// ❌ 错误：每次迭代都重新提取，可能被修改
while (true) {
  const { systemPrompt } = params  // 不要这样做
}

// ✅ 正确：在循环外提取，之后只读
const { systemPrompt } = params
while (true) {
  // 使用 systemPrompt...
}
```

### 2. 合并状态时不要遗漏字段

```typescript
// ❌ 错误：遗漏了 autoCompactTracking
state = { messages: newMessages, toolUseContext, turnCount: n + 1 }

// ✅ 正确：显式列出所有字段
state = {
  messages: newMessages,
  toolUseContext,
  turnCount: n + 1,
  maxOutputTokensRecoveryCount: 0,
  hasAttemptedReactiveCompact: false,
  transition: { reason: 'next_turn' },
  pendingToolUseSummary: undefined,
  autoCompactTracking: tracking,
}
```

### 3. 恢复时保持 hasAttemptedReactiveCompact

如果 compact 已经尝试过且失败，不要重置这个标志，否则会陷入无限循环。


path: references/streaming-executor.md
content: # 流式工具并发执行器

本参考文档描述 `StreamingToolExecutor` 的设计细节。

## 核心问题

传统的 agent 执行模型：
1. 模型完整输出 `tool_use` 块
2. 等待模型输出完毕
3. 按顺序执行所有工具
4. 等待所有工具完成
5. 将结果发回模型

**问题**：步骤 1-2 和步骤 3-5 串行执行，浪费大量等待时间。模型输出可能需要 5-30 秒，而其中大部分时间在等工具结果。

## 核心思路

在模型流式输出的同时就开始执行工具。结果缓冲后按原顺序 yield 给调用方。

## 状态机

每个工具有 4 种状态：

```typescript
type ToolStatus = 'queued' | 'executing' | 'completed' | 'yielded'

type TrackedTool = {
  id: string
  block: ToolUseBlock
  status: ToolStatus
  isConcurrencySafe: boolean
  results: Message[]
  pendingProgress: Message[]  // 进度消息，单独处理
}
```

状态转换图：

```
[queued] ──执行条件满足──→ [executing] ──执行完毕──→ [completed] ──已yield──→ [yielded]
    │
    └──流式 fallback 时→ [completed] (synthetic error results)
```

## 并发控制

### 工具分区

```typescript
function partitionToolCalls(
  toolUseMessages: ToolUseBlock[],
  toolUseContext: ToolUseContext,
): Batch[] {
  return toolUseMessages.reduce((acc, toolUse) => {
    const tool = findToolByName(toolUseContext.options.tools, toolUse.name)
    const isConcurrencySafe = tool?.isConcurrencySafe?.(toolUse.input) ?? false

    if (isConcurrencySafe && acc.at(-1)?.isConcurrencySafe) {
      // 合并到上一个 batch
      acc[acc.length - 1].blocks.push(toolUse)
    } else {
      // 新开一个 batch
      acc.push({ isConcurrencySafe, blocks: [toolUse] })
    }
    return acc
  }, [])
}
```

**效果**：`[Read, Grep, Edit]` → `[{ safe: true, blocks: [Read, Grep] }, { safe: false, blocks: [Edit] }]`

### 执行条件

```typescript
private canExecuteTool(isConcurrencySafe: boolean): boolean {
  const executingTools = this.tools.filter(t => t.status === 'executing')
  return (
    executingTools.length === 0 ||
    (isConcurrencySafe && executingTools.every(t => t.isConcurrencySafe))
  )
}
```

解读：
- 如果没有正在执行的工具 → 可以执行
- 如果当前工具是并发安全的，且所有正在执行的工具都是并发安全的 → 可以执行
- 否则 → 等待

### 队列处理

```typescript
private async processQueue(): Promise<void> {
  for (const tool of this.tools) {
    if (tool.status !== 'queued') continue

    if (this.canExecuteTool(tool.isConcurrencySafe)) {
      await this.executeTool(tool)
    } else if (!tool.isConcurrencySafe) {
      // 非并发安全工具遇到阻碍就停止，因为需要保持顺序
      break
    }
    // 并发安全工具会尝试继续处理队列中的其他工具
  }
}
```

## 与模型流式输出的集成

```typescript
async function* callModelWithStreamingTools(
  messages: Message[],
  toolUseContext: ToolUseContext,
) {
  const executor = new StreamingToolExecutor(tools, canUseTool, toolUseContext)

  for await (const message of deps.callModel({ messages })) {
    // 1. 把模型输出 yield 给调用方（withhold 可恢复错误）
    if (!isRecoverableError(message)) {
      yield message
    }

    // 2. 收集 assistant message
    if (message.type === 'assistant') {
      assistantMessages.push(message)
      for (const toolBlock of message.toolUseBlocks) {
        executor.addTool(toolBlock, message)  // 立即开始执行！
      }
    }

    // 3. yield 已完成工具的结果（不阻塞）
    for (const result of executor.getCompletedResults()) {
      yield result.message
    }
  }

  // 4. 等待剩余工具
  for await (const result of executor.getRemainingResults()) {
    yield result.message
  }
}
```

## 进度消息

工具执行过程中可以产生进度消息（如大文件读取的进度），需要立即 yield，不能等到工具完成：

```typescript
// 工具执行器内
for await (const update of runToolUse(...)) {
  if (update.message.type === 'progress') {
    // 立即加入待 yield 列表
    tool.pendingProgress.push(update.message)
    this.progressAvailableResolve?.()  // 唤醒等待中的 getRemainingResults
  } else {
    messages.push(update.message)
  }
}
```

```typescript
// getCompletedResults
*getCompletedResults() {
  for (const tool of this.tools) {
    // 先 yield 进度消息
    while (tool.pendingProgress.length > 0) {
      yield { message: tool.pendingProgress.shift()! }
    }
    // 再 yield 完成结果
    if (tool.status === 'completed') {
      tool.status = 'yielded'
      yield* tool.results
    }
  }
}
```

## 取消机制

### 流式回退（Streaming Fallback）

当模型输出回退（如触发了 fallback 模型切换），之前流式执行的工具结果都无效：

```typescript
// 调用方检测到 streaming fallback
if (streamingFallbackOccured) {
  executor.discard()  // 丢弃所有工具
  executor = new StreamingToolExecutor(tools, canUseTool, toolUseContext)  // 重置
}
```

```typescript
discard(): void {
  this.discarded = true
  // getRemainingResults 会立即返回，不处理任何工具
}
```

### 兄弟工具错误

Bash 工具出错时，取消所有并行工具：

```typescript
const isErrorResult = message.message.content.some(
  c => c.type === 'tool_result' && c.is_error === true
)

if (isErrorResult && tool.block.name === 'Bash') {
  this.hasErrored = true
  this.siblingAbortController.abort('sibling_error')
}
```

**为什么只取消 Bash**：Bash 命令往往有隐式依赖链（mkdir 失败后 cd 就没意义），而 Read/WebFetch 等是独立的。

### 用户中断

ESC 键触发 `abort` 信号：

```typescript
private getAbortReason(tool: TrackedTool): 'sibling_error' | 'user_interrupted' | 'streaming_fallback' | null {
  if (this.discarded) return 'streaming_fallback'
  if (this.hasErrored) return 'sibling_error'
  if (this.toolUseContext.abortController.signal.aborted) {
    if (this.toolUseContext.abortController.signal.reason === 'interrupt') {
      // 用户输入了新消息
      const behavior = this.getToolInterruptBehavior(tool)
      return behavior === 'cancel' ? 'user_interrupted' : null
    }
    return 'user_interrupted'
  }
  return null
}
```

**注意**：工具有 `interruptBehavior` 属性，`cancel` 行为会被中断，`block` 行为继续执行（用于长时间运行的重要操作）。

## 工具并发安全声明

```typescript
// 每个工具实现 isConcurrencySafe 方法
interface ToolDefinition {
  isConcurrencySafe?: (input: unknown) => boolean
  interruptBehavior?: () => 'cancel' | 'block'
}

// 示例
class ReadTool {
  isConcurrencySafe(_input: ReadInput): boolean {
    return true  // 只读，无副作用
  }
}

class WriteTool {
  isConcurrencySafe(input: WriteInput): boolean {
    // 如果目标是不同的文件，可以并发
    // 如果目标是同一个文件，不能并发
    return !this.wouldOverwrite(input)
  }
}

class BashTool {
  isConcurrencySafe(_input: BashInput): boolean {
    return false  // 有副作用，永远不并发
  }

  interruptBehavior(): 'cancel' | 'block' {
    return 'cancel'  // 可以被 ESC 取消
  }
}
```

## 与传统模式的性能对比

| 阶段 | 传统模式 | 流式执行 |
|------|---------|---------|
| 模型输出 5s | 等待 5s | 等待 5s |
| 工具1 Read 3s | 等上一阶段完成才开始 | 模型输出时已开始 |
| 工具2 Grep 1s | 等上一工具完成 | 与工具1并行 |
| 工具3 Edit 1s | 等上一工具完成 | 等待前两者完成 |
| 发送结果 | 全部完成后一次性发送 | 边完成边发送 |
| **总耗时** | 5+3+1+1 = 10s | max(5, 3) + 1 = 8s |

实际场景中 Read/Grep 通常是并发的，总耗时可降至 6-7s。

## 实现检查清单

- [ ] 工具在 `tool_use` 块到达时立即开始执行
- [ ] 并发安全工具批量并行执行
- [ ] 非并发安全工具串行执行
- [ ] 结果按原始顺序 yield（即使完成时间不同）
- [ ] 进度消息立即 yield，不等待
- [ ] Bash 错误取消兄弟工具
- [ ] 流式回退时 discard 所有工具
- [ ] 用户中断时支持 interruptBehavior


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)