codev / docs /architecture /agent-loop.md
chenbhao's picture
chore: rename VersperClaw to Codev, update org to chenbhao
96f34e3
|
Raw
History Blame Contribute Delete
27.3 kB
# Agent 循环与查询引擎深度分析
> 本文基于 `src/query.ts`、`src/QueryEngine.ts`、`src/query/*`、`src/services/tools/*` 等核心模块,
> 详细阐述 Codev (Claude Code) 的 Agent 循环架构、模型调用管道、工具调度机制、恢复策略与继续决策逻辑。
> 文中列出具体文件路径、函数名称与行号,供开发者快速定位代码。
---
## 目录
1. [主循环架构 (queryLoop)](#1-主循环架构-queryloop)
2. [模型调用管道](#2-模型调用管道)
3. [工具调度与流式执行](#3-工具调度与流式执行)
4. [Stop Hooks (后处理管道)](#4-stop-hooks-后处理管道)
5. [继续决策](#5-继续决策)
6. [恢复机制](#6-恢复机制)
7. [QueryEngine.ts 的角色](#7-queryenginets-的角色)
8. [工具执行引擎](#8-工具执行引擎)
9. [参考文件索引](#9-参考文件索引)
---
## 1. 主循环架构 (queryLoop)
### 1.1 概述
`queryLoop()` 是整个 Agent 的核心,它是一个 `AsyncGenerator`,运行在 `query()` 函数内部的 `while (true)` 无限循环中。每次迭代代表一个 **turn**(回合),包括:输入处理、模型调用、工具执行、后处理钩子、继续决策。
```
┌─────────────────────────────────────────────────────────────────┐
│ queryLoop (AsyncGenerator) │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Pre-model │──>│ Model │──>│ Tool │──>│ Stop │──> │
│ │ Context │ │Invocation│ │Dispatch │ │ Hooks │ │
│ │ Shaping │ │(Stream) │ │& Exec │ │(Post-turn)│ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ │ │ │ │ │
│ │ │ │ │ │
│ └──────────────┴──────────────┴──────────────┘ │
│ │ │
│ ┌─────▼──────┐ │
│ │ Continuation│ │
│ │ Decision │──> continue (loop again) │
│ └─────┬──────┘ │
│ │ stop │
│ ▼ │
│ return Terminal │
└─────────────────────────────────────────────────────────────────┘
```
**文件**: `src/query.ts`
- `query()` 函数: 第 219-239 行 — 公共入口,封装 queryLoop 并处理 consumedCommandUuids
- `queryLoop()` 函数: 第 241-1729 行 — 主循环体
- `QueryParams` 类型: 第 181-199 行 — 循环的输入参数
- `State` 类型: 第 204-217 行 — 跨迭代的可变状态
### 1.2 五种核心阶段
每一个 turn 循环包含以下阶段:
#### 阶段 A: Pre-model Context Shaping (第 365-548 行)
在调用模型之前,对消息列表进行一系列上下文压缩和优化:
1. **Tool Result Budget** (第 376-394 行): `applyToolResultBudget()` 限制每个消息中 tool_result 的总大小,防止工具输出膨胀。
2. **Snip Compact** (第 400-410 行): `snipCompactIfNeeded()` — 被 `HISTORY_SNIP` 特性门控,裁剪历史消息中的冗余内容。
3. **Microcompact** (第 413-426 行): `deps.microcompact()` — 对连续工具结果进行微压缩,减小上下文体积。
4. **Context Collapse** (第 440-447 行): `applyCollapsesIfNeeded()` — 被 `CONTEXT_COLLAPSE` 门控,对历史消息进行投影式折叠。
5. **Auto-compact** (第 454-543 行): `deps.autocompact()` — 全自动上下文压缩,当 Token 数超过阈值时触发。
6. **Blocking Limit Check** (第 628-648 行): 计算是否已达到硬性阻塞限制,阻止 API 调用并返回 `blocking_limit`
#### 阶段 B: Model Invocation (第 652-863 行)
见第 2 节详细分析。
#### 阶段 C: Tool Dispatch & Execution (第 1360-1409 行)
- 如果启用 `StreamingToolExecutor`,使用 `getRemainingResults()` 处理流式工具结果
- 否则使用 `runTools()` 执行工具(通过 `toolOrchestration.ts`
- 工具结果被收集到 `toolResults` 数组中
#### 阶段 D: Stop Hooks (第 1267-1306 行)
`handleStopHooks()` 处理后处理管道(见第 4 节)。
#### 阶段 E: Continuation Decision (第 1308-1357 行)
根据 token 预算、stop hooks 结果等决定是否继续循环。
### 1.3 状态管理
`queryLoop` 使用 `State` 类型(第 204-217 行)管理跨迭代的可变状态:
```typescript
type State = {
messages: Message[]
toolUseContext: ToolUseContext
autoCompactTracking: AutoCompactTrackingState | undefined
maxOutputTokensRecoveryCount: number
hasAttemptedReactiveCompact: boolean
maxOutputTokensOverride: number | undefined
pendingToolUseSummary: Promise<ToolUseSummaryMessage | null> | undefined
stopHookActive: boolean | undefined
turnCount: number
transition: Continue | undefined
}
```
每次迭代开始时解构 state(第 311-321 行),在所有继续点(continue sites)通过 `state = { ... }`(第 1099-1116 行、第 1207-1221 行等)整体替换。
`transition` 字段记录上次迭代的继续原因,用于测试断言恢复路径是否正确触发。
---
## 2. 模型调用管道
### 2.1 callModel 实现
模型调用通过 `deps.callModel()`(第 659 行)进行,其类型为 `typeof queryModelWithStreaming``src/query/deps.ts` 第 23 行)。
`queryDeps.ts` (第 21-30 行) 定义了四种依赖:
- `callModel`: `typeof queryModelWithStreaming` — 流式 API 调用
- `microcompact`: `typeof microcompactMessages` — 微压缩
- `autocompact`: `typeof autoCompactIfNeeded` — 自动压缩
- `uuid`: `() => string` — UUID 生成
`productionDeps()` (第 33-39 行) 提供生产环境实现。
### 2.2 流式事件处理
模型输出的流式事件在 `for await (const message of deps.callModel({...}))` 循环中处理(第 659-863 行):
| 事件类型 | 处理位置 | 说明 |
|---------|---------|------|
| `text_delta` | 由 claude.ts 封装 | 文本增量 |
| `tool_use` | 第 829-845 行 | 提取 tool_use 块,推入 toolUseBlocks |
| `content_block` | 第 748-787 行 | 处理 content block,backfill tool_use input |
| `message_stop` | 第 866-892 行 | 处理缓存的微压缩边界消息 |
关键逻辑:
- **Backfill tool_use input** (第 748-787 行): 当工具定义包含 `backfillObservableInput` 时,对 tool_use 块进行输入回填(如展开文件路径)。
- **Withhold 机制** (第 799-825 行): 可恢复的错误(prompt-too-long、max-output-tokens、media-size-error)在流中被扣留(withhold),不 yield 给调用方,直到恢复机制确认无法恢复后才暴露。
- **Streaming Fallback** (第 712-741 行): 当发生流式模型回退时,清空 previous assistant messages 和 tool results,创建新的 StreamingToolExecutor。
### 2.3 max_output_tokens 恢复机制
代码位置: 第 164 行、第 1188-1256 行
```
恢复步骤:
1. 第1次: 设置 maxOutputTokensOverride = ESCALATED_MAX_TOKENS (64K) 重试
(第 1194-1221 行, 仅当 capEnabled 且第一次)
2. 第2-4次: 注入恢复消息 "Output token limit hit. Resume directly..."
(第 1223-1252 行, 最多 3 次 = MAX_OUTPUT_TOKENS_RECOVERY_LIMIT)
3. 超出限制: 暴露扣留的错误消息并返回 (第 1254-1256 行)
```
如果用户设置了 `CLAUDE_CODE_MAX_OUTPUT_TOKENS` 环境变量,8K→64K 的自动升级会被跳过(第 1202 行)。
### 2.4 缓存控制
- `skipCacheWrite` 参数(第 192 行)— 传递给 API 调用选项(第 697 行),控制是否跳过缓存写入。
- `pendingCacheEdits`(第 423-425 行)— `CACHED_MICROCOMPACT` 特性门控,在 API 响应后使用实际 API 报告的 `cache_deleted_input_tokens` 生成边界消息(第 870-892 行)。
---
## 3. 工具调度与流式执行
### 3.1 StreamingToolExecutor 架构
**文件**: `src/services/tools/StreamingToolExecutor.ts`
`StreamingToolExecutor` 是一个类(第 40-519 行),实现工具的流式执行调度。它在模型仍输出内容时就开始执行已到达的工具。
```
模型流式输出工具调用
StreamingToolExecutor.addTool()
├── 并发安全工具(concurrency-safe)→ 并行执行
└── 非并发安全工具 → 独占执行
收集结果 → getCompletedResults() / getRemainingResults()
```
#### 核心数据结构
```typescript
type TrackedTool = {
id: string
block: ToolUseBlock
assistantMessage: AssistantMessage
status: ToolStatus // 'queued' | 'executing' | 'completed' | 'yielded'
isConcurrencySafe: boolean
promise?: Promise<void>
results?: Message[]
pendingProgress: Message[]
contextModifiers?: Array<(context: ToolUseContext) => ToolUseContext>
}
```
#### 并发控制
- **`addTool()`** (第 76-124 行): 将工具加入队列,立即触发 `processQueue()`
- **`canExecuteTool()`** (第 129-135 行): 决定是否可以执行:
- 如果没有正在执行的工具 → 总是可以
- 如果工具是并发安全的且所有正在执行的工具也是并发安全的 → 可以
- 否则 → 阻塞
- **`processQueue()`** (第 140-150 行): 遍历队列,对每个 queued 工具检查执行条件。
- **`getCompletedResults()`** (第 412-440 行): 非阻塞收集已完成的结果,保持顺序(非并发工具会阻断后续工具的 yield)。
- **`getRemainingResults()`** (第 453-490 行): 等待所有工具完成,带进度唤醒。
#### Bash 错误级联
当 Bash 工具失败时(第 359-363 行),`hasErrored` 被置为 `true`,兄弟工具通过 `siblingAbortController` 被取消。这防止了在 `mkdir` 失败后继续执行依赖的命令。
#### 进度通知
- **BashProgress**: Bash 工具执行的 stdout/stderr 增量更新
- **AgentProgress**: 子 agent 执行进度
- **MCPProgress**: MCP 工具执行的进度
进度消息通过 `pendingProgress` 队列立即 yield(第 368-374 行),并通过 `progressAvailableResolve` 信号唤醒 `getRemainingResults()`
### 3.2 非流式工具执行 (runTools)
**文件**: `src/services/tools/toolOrchestration.ts`
`StreamingToolExecutor` 未启用时,使用 `runTools()` 函数(第 19-82 行)。
#### 工具批处理分区
`partitionToolCalls()`(第 91-116 行)将工具调用分区为批次:
- **并发安全批次**: 多个工具并行执行(通过 `runToolsConcurrently()`
- **非并发安全批次**: 单个工具串行执行(通过 `runToolsSerially()`
最大并发数由 `CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY` 环境变量控制,默认为 10(第 8-12 行)。
---
## 4. Stop Hooks (后处理管道)
**文件**: `src/query/stopHooks.ts`
### 4.1 概要
`handleStopHooks()` 函数(第 65-473 行)在每一 turn 的模型响应结束后运行。它是一个 `AsyncGenerator``yield` 进度/附件消息,最终返回 `StopHookResult`
```typescript
type StopHookResult = {
blockingErrors: Message[] // 阻断性错误(钩子注入的消息)
preventContinuation: boolean // 是否阻止继续循环
}
```
### 4.2 执行流程
```
1. 保存 cacheSafeParams (第 96-98 行)
2. 模板作业分类 (第 108-132 行)
3. Prompt 建议 (第 139 行) [fire-and-forget]
4. Memory 提取 (第 141-153 行) [fire-and-forget]
5. Auto-dream (第 154-156 行) [fire-and-forget]
6. MCP 清理 (第 164-173 行)
7. executeStopHooks() (第 180 行) — 主钩子执行
8. Teammate hooks (第 335-453 行)
```
### 4.3 Stop Hooks 执行
`executeStopHooks()`(第 180 行)返回一个 generator,产生进度消息和阻断错误。
钩子结果处理(第 192-295 行):
- **进度消息** (第 201-215 行): 收集每个钩子的 `command``promptText`
- **阻断错误** (第 257-267 行): 创建 `createUserMessage({ isMeta: true })` 作为阻断消息
- **继续阻止** (第 269-280 行): 生成 `hook_stopped_continuation` 附件
- **中止检测** (第 283-294 行): 如果被中止,返回 `{ preventContinuation: true }`
### 4.4 后台任务
| 任务 | 位置 | 说明 |
|------|------|------|
| `executePromptSuggestion()` | 第 139 行 | 生成提示建议(仅非 bare 模式) |
| `executeExtractMemories()` | 第 141-153 行 | 提取记忆(`EXTRACT_MEMORIES` 门控) |
| `executeAutoDream()` | 第 154-156 行 | 自动记忆整理 |
| `cleanupComputerUseAfterTurn()` | 第 164-173 行 | MCP 计算机使用清理(`CHICAGO_MCP` 门控) |
### 4.5 模板作业分类
当设置了 `CLAUDE_JOB_DIR` 环境变量时(第 110 行),`jobClassifierModule!.classifyAndWriteState()`(第 121 行)在每次 turn 后分类作业状态。加 60 秒超时(第 127-131 行)。
### 4.6 Teammate Hooks
在 teammate 模式下(第 335 行):
1. **TaskCompleted hooks** (第 345-400 行): 对每个 `in_progress` 且属于当前 teammate 的任务执行 `executeTaskCompletedHooks()`
2. **TeammateIdle hooks** (第 402-441 行): 执行 `executeTeammateIdleHooks()`
这两个钩子都支持 `preventContinuation``blockingErrors`
### 4.7 阻断错误与继续抑制
- `blockingErrors`: 由钩子注入的系统消息,作为当前 turn 的继续输入(第 1282-1306 行)
- `preventContinuation`: 立即结束循环返回 `stop_hook_prevented`(第 1278 行)
- **错误免入死亡螺旋** (第 1260-1264 行): 当 lastMessage 是 API 错误时,跳过 stop hooks
---
## 5. 继续决策
**文件**: `src/query/tokenBudget.ts`
### 5.1 checkTokenBudget
```typescript
function checkTokenBudget(
tracker: BudgetTracker,
agentId: string | undefined,
budget: number | null, // getCurrentTurnTokenBudget()
globalTurnTokens: number, // getTurnOutputTokens()
): TokenBudgetDecision
```
第 45-93 行。
### 5.2 决策逻辑
```
1. 跳过条件: agentId 存在 OR budget 为 null/0 → stop (第 51-53 行)
2. 计算使用率 pct = turnTokens / budget * 100 (第 56 行)
3. 收益递减检测: 连续 3+ 次继续且每次增量 < 500 tokens (第 59-62 行)
4. 如果 pct < 90% 且非收益递减 → continue (第 66-75 行)
5. 否则 → stop (第 78-92 行)
```
### 5.3 90% 阈值
`COMPLETION_THRESHOLD = 0.9`(第 3 行): 当 token 消耗达到预算的 90% 时触发继续。
### 5.4 收益递减检测
`DIMINISHING_THRESHOLD = 500`(第 4 行): 当连续 3+ 次继续且每次增量 < 500 tokens,认为模型收益递减,提前停止。
### 5.5 集成到 queryLoop
`queryLoop` 中(`src/query.ts` 第 1308-1355 行):
```typescript
if (feature('TOKEN_BUDGET')) {
const decision = checkTokenBudget(budgetTracker!, ...)
if (decision.action === 'continue') {
// 注入 nudgemessage
// incrementBudgetContinuationCount()
// 设置 state.transition = { reason: 'token_budget_continuation' }
// continue (继续循环)
}
// 否则记录 completionEvent
}
// return { reason: 'completed' }
```
### 5.6 任务预算 (taskBudget)
`taskBudget``src/query.ts` 第 197-198 行)是 API 端的 `task_budget`(output_config.task_budget, beta task-budgets-2026-03-13)。与 `tokenBudget` +500k 自动继续的不同。
- 在每次压缩后计算 `taskBudgetRemaining`(第 508-515 行、第 1138-1146 行)
- 传递给 API 调用(第 699-706 行)
---
## 6. 恢复机制
### 6.1 max_output_tokens 恢复
**文件**: `src/query.ts`
| 恢复阶段 | 触发条件 | 行为 | 行号 |
|---------|---------|------|------|
| 8K→64K 升级 | 首次命中上限,capEnabled 且无用户自定义 | 设置 maxOutputTokensOverride=64K,重试 | 第 1194-1221 行 |
| 恢复消息注入 | 已升级或 cap 关闭 | 注入 "Output token limit hit" 消息 | 第 1223-1252 行 |
| 限制耗尽 | 超过 3 次 | 暴露扣留的错误 | 第 1254-1256 行 |
`MAX_OUTPUT_TOKENS_RECOVERY_LIMIT = 3`(第 164 行)。
### 6.2 Context Collapse 压缩
**文件**: `src/query.ts`
当 API 返回 prompt-too-long 错误时(第 1085-1118 行):
1. 首先尝试 `contextCollapse.recoverFromOverflow()`(第 1094 行) — 从已分阶段(staged)的折叠队列中释放
2. 如果已经尝试过 `collapse_drain_retry`(第 1092 行),则跳过直接走 reactive compact
### 6.3 Reactive Compact
**文件**: `src/query.ts`
当 prompt-too-long 错误和 reactive compact 都启用时(第 1119-1175 行):
1. 调用 `reactiveCompact.tryReactiveCompact()`(第 1120 行)
2. 成功→构建压缩消息,设置 `hasAttemptedReactiveCompact = true`
3. 失败→暴露错误消息
### 6.4 错误恢复策略总结
```
API 413 (prompt too long)
├── Context Collapse drain (第 1094 行)
│ └── 失败→ fall through
├── Reactive Compact (第 1120 行)
│ └── 失败→ 返回 prompt_too_long
└── (如果两者都不可用) → 返回 blocking_limit
max_output_tokens
├── 8K→64K escalate (第 1194 行)
├── 恢复消息注入 x3 (第 1223 行)
└── 暴露错误 (第 1254 行)
model fallback (第 893-953 行)
└── 切换到备用模型,清理并重试
一般错误 (第 955-997 行)
└── yield 错误消息,返回 model_error
```
---
## 7. QueryEngine.ts 的角色
**文件**: `src/QueryEngine.ts`
### 7.1 概述
`QueryEngine` 类(第 184-1177 行)封装了查询生命周期和会话状态,是 `ask()` 函数的核心引擎。它提取了在 headless/SDK 和 REPL 间共享的逻辑。
### 7.2 与 query.ts 的关系
```
ask() 函数 QueryEngine.submitMessage() 方法
│ │
│ 创建 QueryEngine 实例 │
│ (第 1249-1285 行) │
│ │
└─── query() ────────────────────────────┘
│ AsyncGenerator
yield 消息流 (assistant/user/attachment/stream_event/...)
```
- `ask()`(第 1186-1295 行)是一个便利包装器,创建 `QueryEngine` 实例并调用 `submitMessage()`
- `QueryEngine.submitMessage()`(第 209-1156 行)处理完整的查询生命周期:
1. 构建 `ProcessUserInputContext`(第 335-395 行)
2. 处理用户输入(第 410-428 行)
3. 记录 transcript(第 450-463 行)
4. 调用 `query()`(第 675-686 行)
5. 处理 query 产出的所有消息类型(第 757-969 行)
6. 生成最终 result(第 1082-1155 行)
### 7.3 关键职责
- **消息持久化**: `recordTranscript()`(第 717-732 行)
- **权限跟踪**: `wrappedCanUseTool()` 包装(第 244-271 行)记录权限拒绝
- **SDK 消息规范化**: `normalizeMessage()`(第 769、783、787 行)
- **预算检查**: USD 预算(第 972-1002 行)和结构化输出重试限制(第 1005-1048 行)
- **Snip 回放**: `snipReplay` 回调(第 905-914 行)在 SDK 模式下处理 snip 边界
### 7.4 submitMessage 的消息处理
`submitMessage()``for await` 循环处理 10+ 种消息类型:
| 消息类型 | 处理 | 行号 |
|---------|------|------|
| `assistant` | push 到 mutableMessages,yield 规范化 | 第 761-769 行 |
| `user` | push,yield 规范化,turnCount++ | 第 753-787 行 |
| `progress` | push,记录 transcript | 第 771-783 行 |
| `stream_event` | 累积 usage,跟踪 stop_reason | 第 788-827 行 |
| `attachment` | 处理结构化输出、max_turns、queued_command | 第 829-893 行 |
| `system` | 处理 compact_boundary、api_error、snip | 第 897-958 行 |
| `tool_use_summary` | yield 工具使用摘要 | 第 959-969 行 |
---
## 8. 工具执行引擎
**文件**: `src/services/tools/toolExecution.ts`
### 8.1 工具执行九步生命周期
`runToolUse()` 函数(第 337-490 行)实现工具的完整执行生命周期:
```
1. Tool Lookup & Validation
│ findToolByName() (第 345 行)
│ 别名回退 (第 350-355 行)
2. Abort Check
│ abortController.signal.aborted (第 415 行)
3. Input Validation (Zod)
│ tool.inputSchema.safeParse() (第 615 行)
4. Custom Validation
│ tool.validateInput() (第 683 行)
5. Pre-Tool Hooks
│ runPreToolUseHooks() (第 800 行)
6. Permission Check & User Confirmation
│ resolveHookPermissionDecision() (第 921 行)
│ canUseTool() (第 927 行)
7. Tool Execution
│ tool.call() (第 1207 行)
8. Result Processing
│ tool.mapToolResultToToolResultBlockParam() (第 1292 行)
│ processToolResultBlock() (第 1415 行)
9. Post-Tool Hooks
│ runPostToolUseHooks() (第 1483 行)
│ runPostToolUseFailureHooks() (第 1700 行)
```
### 8.2 详细步骤解析
#### 步骤 1-2: 工具查找与中止检查
`runToolUse()`(第 337-490 行):
```typescript
// 1. 查找工具 (第 345-355 行)
let tool = findToolByName(toolUseContext.options.tools, toolName)
if (!tool) {
// 通过别名回退 (第 350-355 行)
}
// 2. 中止检查 (第 415-453 行)
if (toolUseContext.abortController.signal.aborted) {
// yield "cancelled" 消息
}
```
#### 步骤 3-4: 输入验证
`checkPermissionsAndCallTool()`(第 599-1745 行):
**Zod Schema 验证**(第 615-679 行):
- `tool.inputSchema.safeParse(input)` 使用 Zod 验证模型输入
- 失败时生成格式化的 Zod 错误并附加 schema-not-sent 提示(第 578-597 行)
- `buildSchemaNotSentHint()`: 检测到延迟工具(deferred tool)的 schema 没有被发送到 API 时,提示模型使用 ToolSearch 重新加载
**自定义验证**(第 683-733 行):
- 每个工具可以定义自己的 `validateInput()` 方法
- 验证失败返回 `isValidCall.result === false`
#### 步骤 5: Pre-Tool Hooks
**文件**: `src/services/tools/toolHooks.ts`
`runPreToolUseHooks()`(第 435-650 行):
返回多类型结果:
| 结果类型 | 说明 |
|---------|------|
| `message` | 进度消息或附件消息 |
| `hookPermissionResult` | 钩子做出的权限决定 (allow/deny/ask) |
| `hookUpdatedInput` | 钩子修改后的输入 (passthrough) |
| `preventContinuation` | 阻止继续 |
| `stopReason` | 停止原因 |
| `additionalContext` | 额外的上下文消息 |
| `stop` | 立即停止 |
#### 步骤 6: 权限检查
**文件**: `src/services/tools/toolHooks.ts`
`resolveHookPermissionDecision()`(第 332-433 行):
- **Hook allow**: 仍然检查 settings.json 的 deny/ask 规则(第 373-385 行)
- **Hook deny**: 直接拒绝(第 408-411 行)
- **无钩子决定**: 走正常权限流程,可能包含 forceDecision(第 413-432 行)
- **需用户交互**: 钩子批准后如果 `requiresUserInteraction()` 或 `requireCanUseTool`,仍调用 `canUseTool()`(第 356-370 行)
`canUseTool` 在 `src/hooks/useCanUseTool.tsx` 中实现(React hook),处理 interactive/coordinator/swarm 三种权限模式。
#### 步骤 7: 工具执行
`tool.call()`(第 1207 行):
- 使用处理后的输入调用工具
- 通过 `onToolProgress` 回调报告进度
- 使用 `toolAbortController`(第 301 行)实现 per-tool 取消
Bash 错误级联(第 359-363 行):
```typescript
if (tool.block.name === BASH_TOOL_NAME) {
this.hasErrored = true
this.siblingAbortController.abort('sibling_error')
}
```
#### 步骤 8: 结果处理
- `tool.mapToolResultToToolResultBlockParam()`(第 1292 行)映射工具结果
- `processPreMappedToolResultBlock()` / `processToolResultBlock()`(第 1409-1415 行)进行后处理
- `applyToolResultBudget()`(第 379 行)限制工具结果大小
#### 步骤 9: Post-Tool Hooks
**文件**: `src/services/tools/toolHooks.ts`
`runPostToolUseHooks()`(第 39-191 行):
- 对 MCP 工具,支持 `updatedMCPToolOutput`
- 支持 `blockingError`、`preventContinuation`、`additionalContext`
`runPostToolUseFailureHooks()`(第 193-319 行):
- 工具失败时执行
- 同样支持 `blockingError`、`preventContinuation`、`additionalContext`
### 8.3 错误分类与处理
`classifyToolError()`(第 150-171 行)将错误分类为安全的 telemetry 字符串:
- `TelemetrySafeError`: 使用预审的 telemetryMessage
- Node.js `errno` 错误: 记录 `ENOENT` 等代码
- 已知错误类型: 使用构造函数名称
- 未知错误: 降级为 `"Error"`
### 8.4 工具遥测
工具执行的每个阶段都会发出遥测事件:
| 事件 | 触发时机 | 代码位置 |
|------|---------|---------|
| `tengu_tool_use_error` | 工具不存在 | 第 372 行 |
| `tengu_tool_use_cancelled` | 工具被取消 | 第 416 行 |
| `tengu_tool_use_progress` | 进度更新 | 第 523 行 |
| `tengu_tool_use_can_use_tool_rejected` | 权限拒绝 | 第 1001 行 |
| `tengu_tool_use_can_use_tool_allowed` | 权限批准 | 第 1105 行 |
| `tengu_tool_use_success` | 工具执行成功 | 第 1331 行 |
| `tool_decision` (OTel) | 权限决策 | 第 962 行 |
| `tool_result` (OTel) | 工具结果 | 第 1381 行 |
---
## 9. 参考文件索引
| 文件 | 路径 | 核心内容 |
|------|------|---------|
| 主查询循环 | `src/query.ts` | `query()`, `queryLoop()`, 完整 Agent 循环 |
| 查询配置 | `src/query/config.ts` | `buildQueryConfig()`, `QueryConfig` 类型 |
| 查询依赖 | `src/query/deps.ts` | `QueryDeps`, `productionDeps()` |
| 停止钩子 | `src/query/stopHooks.ts` | `handleStopHooks()`, StopHookResult |
| Token 预算 | `src/query/tokenBudget.ts` | `checkTokenBudget()`, `BudgetTracker` |
| 转换类型 | `src/query/transitions.ts` | `Terminal`, `Continue` 类型 |
| 查询引擎 | `src/QueryEngine.ts` | `QueryEngine` 类, `ask()` 函数 |
| 工具流式执行器 | `src/services/tools/StreamingToolExecutor.ts` | `StreamingToolExecutor` 类 |
| 工具执行 | `src/services/tools/toolExecution.ts` | `runToolUse()`, `checkPermissionsAndCallTool()` |
| 工具编排 | `src/services/tools/toolOrchestration.ts` | `runTools()`, `partitionToolCalls()` |
| 工具钩子 | `src/services/tools/toolHooks.ts` | `runPreToolUseHooks()`, `runPostToolUseHooks()`, `resolveHookPermissionDecision()` |
| 权限检查 | `src/hooks/useCanUseTool.tsx` | `useCanUseTool()` React hook, 权限模式 |