File size: 27,254 Bytes
f468102 96f34e3 f468102 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 | # 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, 权限模式 |
|