概述

本文档以 ASCII 控制流图的形式展示 agent-session.ts 中 prompting 系统的完整执行路径。每个图对应一个核心方法或子系统,用箭头和分支标注所有可能的执行路径、提前返回点、错误抛出点。

目标:一眼看清消息从用户输入到 LLM 调用的完整路径。


1. prompt() 顶层 try 块控制流

这是 prompt() 方法的大 try 块,包含 7 个处理段、4 个 early return 点、3 个 preflightResult(true) 和 1 个 preflightResult(false)

prompt(text, options)
    │
    ├─ 初始化: expandPromptTemplates, preflightResult, messages = undefined
    │
    ▼
try {
    │
    ├── [段1] 扩展命令? (expandPromptTemplates && text.startsWith("/"))
    │       │
    │       ├── true → _tryExecuteExtensionCommand(text)
    │       │           │
    │       │           ├── handled=true → preflightResult(true) + return  ← Return 1
    │       │           │
    │       │           └── handled=false → 继续
    │       │
    │       └── false → 继续
    │
    ├── [段2] input 事件? (hasHandlers("input"))
    │       │
    │       ├── true → emitInput(text, images, source, streamingBehavior)
    │       │           │
    │       │           ├── handled → preflightResult(true) + return    ← Return 2
    │       │           │
    │       │           ├── transform → 更新 currentText/currentImages → 继续
    │       │           │
    │       │           └── continue → 不修改 → 继续
    │       │
    │       └── false → 跳过, text/images 不变
    │
    ├── [段3] 技能/模板展开 (expandPromptTemplates)
    │       │
    │       ├── _expandSkillCommand()          /skill:name → <skill> XML 块
    │       └── expandPromptTemplate()          {{template}} → 预设内容
    │
    ├── [段4] 流式中? (isStreaming)
    │       │
    │       ├── true → streamingBehavior 存在?
    │       │           │
    │       │           ├── 无 → throw Error("必须指定 streamingBehavior")  ← 异常
    │       │           │
    │       │           ├── "followUp" → _queueFollowUp() → preflightResult(true) + return  ← Return 3
    │       │           │
    │       │           └── "steer" → _queueSteer() → preflightResult(true) + return       ← Return 3
    │       │
    │       └── false → 继续
    │
    ├── [段5] 认证检查
    │       │
    │       ├─ _flushPendingBashMessages()   ← 刷新上一轮的 bash 结果
    │       │
    │       ├── model 存在?
    │       │   ├── 无 → throw formatNoModelSelectedMessage()  ← 异常
    │       │   └── 有 → 继续
    │       │
    │       └── hasConfiguredAuth(model)?
    │           ├── false + isOAuth → throw "OAuth 过期, 请重新登录"  ← 异常
    │           ├── false + API key → throw formatNoApiKeyFoundMessage()  ← 异常
    │           └── true → 继续
    │
    ├── [段6] 预压缩
    │       │
    │       ├─ _findLastAssistantMessage()
    │       │   │
    │       │   ├── 无 lastAssistant → 跳过
    │       │   └── 有 lastAssistant → _checkCompaction(msg, skipAbortedCheck=false)
    │       │       │
    │       │       ├── 需要压缩 → try { agent.continue() 循环 } finally { _flushPendingBashMessages() }
    │       │       └── 不需要 → 跳过
    │       │
    │       └── 继续
    │
    └── [段7] 消息构建 + before_agent_start
            │
            ├── 1. 构建 user message
            │      { role: "user", content: [text, ...images], timestamp }
            │
            ├── 2. 注入 pendingNextTurnMessages (asides)
            │      扩展注入的上下文信息, 不单独触发 LLM 调用
            │
            ├── 3. emitBeforeAgentStart()
            │      │
            │      ├── result.messages → 扩展注入 custom messages
            │      └── result.systemPrompt → 覆盖 | 回退到 _baseSystemPrompt
            │
            └── end try  (messages 赋值完成)
    │
} catch (error) {
    │
    └── preflightResult(false) + throw error   ← 所有上述 throw 汇聚于此
    
    │
    ▼
  messages === undefined?
    │
    ├── true → return (防呆, 理论上不会发生)
    │
    └── false → preflightResult(true) + _runAgentPrompt(messages)  ← 唯一执行 LLM 调用的路径

preflightResult 5 个调用点汇总

调用点 1: [段1] 扩展命令找到并执行         → preflightResult(true) + return
调用点 2: [段2] input 事件 handled       → preflightResult(true) + return
调用点 3: [段4] 消息已排队 (steer/followUp) → preflightResult(true) + return
调用点 4: [catch] try 块内任何异常        → preflightResult(false) + throw
调用点 5: [try 成功] 所有检查通过         → preflightResult(true) + _runAgentPrompt()

2. input 事件链 — ExtensionRunner.emitInput()

这是扩展系统对用户输入的拦截处理机制。支持多个扩展的 handler 链式调用,每个可以独立选择拦截、修改或放行。

emitInput(text, images, source, streamingBehavior)
    │
    ▼
  遍历 this.extensions (所有已注册扩展)
    │
    └── 对每个扩展, 遍历 ext.handlers.get("input") (该扩展的所有 input handler)
            │
            ▼
          handler(event, ctx)  ← 每个 handler 是一个 async 函数
            │
            ├── 执行成功
            │   │
            │   ├── result.action === "handled"
            │   │   └── return { action: "handled" }  ← 短路, 不再遍历后续 handler
            │   │
            │   ├── result.action === "transform"
            │   │   ├── currentText = result.text
            │   │   ├── currentImages = result.images ?? currentImages
            │   │   └── 继续下一个 handler (可叠加)
            │   │
            │   └── result === undefined 或 continue
            │       └── 继续下一个 handler
            │
            └── 执行异常
                └── emitError() → 记录错误 → 继续下一个 handler (隔离)
    │
    ▼
  所有 handler 遍历完毕
    │
    ├── currentText !== text || currentImages !== images
    │   └── return { action: "transform", text, images }  ← 至少一个扩展做了修改
    │
    └── 无任何修改
        └── return { action: "continue" }

多扩展叠加 transform 示例

输入: "hello"

扩展 A handler:   transform → text = "A<" + text          → "A<hello"
扩展 B handler:   transform → text = text + ">B"          → "A<hello>B"
扩展 C handler:   continue  → 不处理

最终 result:     { action: "transform", text: "A<hello>B" }

handled 短路示例

输入: "hello"

扩展 A handler:   handled  → return { action: "handled" }
扩展 B handler:   不会被执行 (被短路)
扩展 C handler:   不会被执行 (被短路)

prompt() 收到 handled → preflightResult(true) + return

3. _runAgentPrompt() 运行循环

_runAgentPrompt(messages)
    │
    ▼
try {
    │
    ├─ agent.prompt(messages)    ← 首次 LLM 调用 + 工具链
    │
    └─ while (_handlePostAgentRun())  ← 后处理链, 返回 true 就继续
           │
           └── agent.continue()  ← 继续 agent 循环
                   │
                   └──→ 回到 _handlePostAgentRun() 检查
    │
} finally {
    │
    └─ _flushPendingBashMessages()  ← 无论如何, 刷新 bash 结果

执行路径

entry: _runAgentPrompt()
    │
    ▼
agent.prompt()
    │
    ▼ (agent 完成: 报错 / LLM 响应完毕 / 工具链结束)
    │
    ▼
_handlePostAgentRun()  ──── true ────→ agent.continue()
    │                                       │
    │                                       ▼
    false                              _handlePostAgentRun()
    │                                       │
    ▼                                       │
 循环结束                               true/false 分支 继续循环
    │
    ▼
_flushPendingBashMessages()  ← finally
    │
    ▼
done

4. _handlePostAgentRun() 三阶段责任链

_handlePostAgentRun()
    │
    ├─ 读取 _lastAssistantMessage
    │   │
    │   ├── undefined → return false (无可处理的消息)
    │   │
    │   └── 有 msg → 立即重置 _lastAssistantMessage = undefined (防重复)
    │
    ├─ [阶段1] 重试检查
    │   │
    │   ├── isRetryableError(msg)?
    │   │   │
    │   │   ├── true → _prepareRetry(msg)
    │   │   │           │
    │   │   │           ├── 未超过 maxRetries → 指数退避 sleep(delay)
    │   │   │           │                          │
    │   │   │           │                          ├── sleep 被中止 → return false
    │   │   │           │                          └── sleep 完成 → return true (调用者继续)
    │   │   │           │
    │   │   │           └── 超过 maxRetries → return false
    │   │   │
    │   │   └── false → 继续
    │   │
    │   ├── stopReason === "error" && retryAttempt > 0
    │   │   └── emit auto_retry_end(success=false) + reset counter
    │   │
    │   ├── [阶段2] 压缩检查
    │   │   │
    │   │   ├── _checkCompaction(msg, skipAbortedCheck=true)
    │   │   │   │
    │   │   │   ├── compaction disabled           → false
    │   │   │   ├── msg was aborted               → false (被 skipAbortedCheck 过滤)
    │   │   │   ├── msg from different model      → false (模型已切换)
    │   │   │   ├── msg from before compaction     → false (防重复触发)
    │   │   │   ├── context overflow               → _runAutoCompaction("overflow")
    │   │   │   │                                      │
    │   │   │   │                                      ├── 首次 overflow → 移错误消息 + 压缩 + agent.continue()
    │   │   │   │                                      └── 二次 overflow → emit 失败事件, return false
    │   │   │   │
    │   │   │   └── context threshold 超阈值       → _runAutoCompaction("threshold") → return true
    │   │   │
    │   │   └── 不需要压缩 → 继续
    │   │
    │   └── [阶段3] 队列检查
    │       │
    │       └── agent.hasQueuedMessages()
    │           │
    │           ├── true → return true (扩展在 agent_end handler 中排队了消息)
    │           │
    │           └── false → return false (循环结束)
    │
    ▼ 返回结果
    true  → 调用者继续 agent.continue()
    false → 调用者退出循环

责任链的优先级

                   ┌─────────────────┐
                   │  入口: 有 msg?  │
                   └────────┬────────┘
                            │
                     ┌──────▼──────┐
                     │  重试优先   │  ← 优先处理错误恢复
                     │  retryable? │
                     └──────┬──────┘
                            │
                     ┌──────▼──────┐
                     │  压缩次之   │  ← 上下文管理
                     │  need compact? │
                     └──────┬──────┘
                            │
                     ┌──────▼──────┐
                     │  队列最后   │  ← 扩展注入的消息
                     │  queued?    │
                     └──────┬──────┘
                            │
                     ┌──────▼──────┐
                     │  无事可做   │
                     │  return false │
                     └─────────────┘

5. streaming 分支 — steer vs followUp

prompt() 内 isStreaming === true
    │
    ├── streamingBehavior 未指定 → throw Error("Specify streamingBehavior")
    │
    └── streamingBehavior 已指定
            │
            ├── "followUp"
            │   └── _queueFollowUp(text, images)
            │       │
            │       ├── _followUpMessages.push(text)  ← UI 追踪
            │       ├── _emitQueueUpdate()
            │       └── agent.followUp({ role: "user", content })  ← agent-core 排队
            │
            └── "steer"
                └── _queueSteer(text, images)
                    │
                    ├── _steeringMessages.push(text)  ← UI 追踪
                    ├── _emitQueueUpdate()
                    └── agent.steer({ role: "user", content })  ← agent-core 排队

steer vs followUp 在 agent-core 中的区别

LLM 正在生成文本的中间
    │
    ├── steer() 被调用
    │   └── agent-core: 中断当前 LLM 响应
    │       ├── 丢弃正在生成的文本
    │       └── 立即开始处理新消息
    │
    └── followUp() 被调用
        └── agent-core: 不中断
            ├── 等当前 LLM 生成完毕
            ├── 执行工具调用链 (如有)
            └── LLM 空闲时才处理新消息

队列消费检测 (在 _handleAgentEvent 中)

agent-core 消费了排队消息 → 触发 message_start 事件
    │
    ▼
_handleAgentEvent()
    │
    ├── event.type === "message_start" && message.role === "user"
    │   │
    │   ├── _overflowRecoveryAttempted = false  (重置 overflow 标志)
    │   │
    │   └── messageText = _getUserMessageText(event.message)
    │       │
    │       ├── 在 _steeringMessages 中找到?
    │       │   ├── true → 从 _steeringMessages 移除 + _emitQueueUpdate()
    │       │   └── false → 继续
    │       │
    │       ├── 在 _followUpMessages 中找到?
    │       │   ├── true → 从 _followUpMessages 移除 + _emitQueueUpdate()
    │       │   └── false → 消息不是来自队列 (正常用户输入)
    │       │
    │       └── 移除顺序: 先 steering, 再 followUp

6. sendCustomMessage() 五种投递路径

sendCustomMessage({ customType, content, display, details }, options?)
    │
    ▼
  构建 appMessage = { role: "custom", customType, content, display, details, timestamp }
    │
    ├── deliverAs === "nextTurn"
    │   └── _pendingNextTurnMessages.push(appMessage)
    │       下次 prompt() 时作为 aside 注入
    │
    ├── 正在 streaming
    │   │
    │   ├── deliverAs === "followUp"
    │   │   └── agent.followUp(appMessage)  ← 等待队列
    │   │
    │   └── 其他 (默认 steer)
    │       └── agent.steer(appMessage)  ← 立即中断
    │
    ├── 非 streaming + triggerTurn === true
    │   └── _runAgentPrompt(appMessage)  ← 立即触发 LLM 调用
    │
    └── 非 streaming + 不 trigger
        ├── agent.state.messages.push(appMessage)      ← 追加到 agent state
        ├── sessionManager.appendCustomMessageEntry()   ← 持久化到会话
        ├── emit message_start                         ← 通知 UI
        └── emit message_end                           ← 不触发 LLM

五种投递路径决策树

sendCustomMessage 入口
    │
    ├── deliverAs === "nextTurn"?
    │   └── YES → 缓存到 _pendingNextTurnMessages, 不触发任何事件
    │
    └── NO
        │
        ├── isStreaming?
        │   ├── YES → agent.steer() / agent.followUp()
        │   └── NO
        │       │
        │       ├── triggerTurn?
        │       │   ├── YES → _runAgentPrompt(appMessage)  ← 新 LLM 调用
        │       │   └── NO
        │       │       └── 追加到 state + session + emit events, 不触发 LLM

7. 扩展命令路由 — _tryExecuteExtensionCommand()

_tryExecuteExtensionCommand(text)
    │
    ├─ 解析命令名
    │   ├── text.indexOf(" ") === -1 → commandName = text.slice(1)       (/help → "help")
    │   └── text.indexOf(" ") !== -1 → commandName = text.slice(1, pos)  (/model gpt → "model")
    │
    ├─ 查找命令
    │   │
    │   ├── _extensionRunner.getCommand(commandName) === undefined
    │   │   └── return false (不是扩展命令, 由后续步骤处理)
    │   │
    │   └── 命令找到
    │       │
    │       ├─ ctx = _extensionRunner.createCommandContext()
    │       │
    │       ├─ try { await command.handler(args, ctx) }
    │       │   └── return true (命令执行成功)
    │       │
    │       └─ catch (err)
    │           └── emitError → return true (错误已处理, 仍返回 true)
    │
    └── 返回值
        ├── true  → prompt() 不继续, 直接 return
        └── false → prompt() 继续到下一个段

执行位置特例

prompt() 中
    │
    ├── [段1] 扩展命令: 在 try 块最前面, 优先于 input 事件
    │   即使 isStreaming === true 也立即执行
    │   失败时 emitError 但不 throw, 不阻塞 prompt()
    │
    └── steer() / followUp() 中
        └── _throwIfExtensionCommand(): 扩展命令不能排队, 直接抛错

8. 认证检查决策树

Step 5 认证检查
    │
    ├─ _flushPendingBashMessages()  ← 先刷新上一轮 bash 结果
    │
    ├─ this.model 存在?
    │   │
    │   ├── false → throw formatNoModelSelectedMessage()
    │   │           └── "No model selected. Run '/model <name>' to select a model."
    │   │
    │   └── true → 继续
    │
    └─ hasConfiguredAuth(this.model)
        │
        ├── true → 通过, 继续到 Step 6
        │
        └── false
            │
            ├── 是 OAuth 模型?
            │   │
            │   ├── OAuth → throw
            │   │   "Authentication failed for \"{provider}\". "
            │   │   "Credentials may have expired or network is unavailable. "
            │   │   "Run '/login {provider}' to re-authenticate."
            │   │
            │   └── API key → throw formatNoApiKeyFoundMessage(provider)
            │       "No API key found for {provider}. "
            │       "Run '/set api-key {provider} <key>' to configure, "
            │       "or set the {ENV_VAR} environment variable."
            │
            └── 所有 throw 被 try/catch 捕获 → preflightResult(false)

9. 技能展开格式

_expandSkillCommand("/skill:debugging crash in login")
    │
    ├─ 解析: skillName = "debugging", args = "crash in login"
    │
    ├─ 在 resourceLoader.getSkills() 中查找
    │   │
    │   ├── 未找到 → 返回原始 text, 不修改
    │   │
    │   └── 找到 skill 文件
    │       │
    │       ├─ readFileSync(skill.filePath, "utf-8")  读取技能内容
    │       ├─ stripFrontmatter(content).trim()        移除 frontmatter
    │       │
    │       └─ 构建输出
    │           │
    │           └── <skill name="debugging" location="/path/to/skill.md">
    │               References are relative to /base/dir.
    │
    │               ## Systematic Debugging
    │               ...
    │               </skill>
    │
    │               crash in login
    │
    └── 有 args? → 追加到 skill block 后面
        无 args? → 只返回 skill block

10. 消息构建顺序 (Step 7)

messages = []   ← 清空
    │
    ├─ 1. user message
    │   { role: "user", content: [{ type: "text", text: expandedText }, ...images] }
    │
    ├─ 2. pendingNextTurnMessages (扩展 asides)
    │   for (msg of _pendingNextTurnMessages) { messages.push(msg) }
    │   _pendingNextTurnMessages = []  ← 清空
    │
    ├─ 3. emitBeforeAgentStart() → 扩展注入
    │   │
    │   ├── result.messages 存在?
    │   │   └── for (msg of result.messages) { push custom message }
    │   │
    │   └── result.systemPrompt 存在?
    │       ├── YES → this.agent.state.systemPrompt = result.systemPrompt
    │       └── NO  → this.agent.state.systemPrompt = this._baseSystemPrompt (回退)
    │
    └─ 最终消息数组顺序:
       [user, aside1, aside2, ..., custom1, custom2, ...]

11. bash 结果延迟刷新

recordBashResult() 分流

executeBash(command)
    │
    ▼
recordBashResult(command, result, options?)
    │
    ├── isStreaming?
    │   │
    │   ├── true → _pendingBashMessages.push(bashMessage)  ← 排队等待
    │   │
    │   └── false → 立即注入
    │       ├── agent.state.messages.push(bashMessage)
    │       └── sessionManager.appendMessage(bashMessage)
    │
    └── bashMessage 结构:
        { role: "bashExecution", command, output, exitCode, cancelled, truncated, ... }

_flushPendingBashMessages() 的 3 个触发时机

时机 1: prompt() Step 5 开头
    └── _flushPendingBashMessages()
        在新用户消息发送前, 刷新上一轮 LLM 调用产生的 bash 结果

时机 2: prompt() Step 6 预压缩的嵌套 finally
    └── _flushPendingBashMessages()
        预压缩过程中可能产生 bash 结果, 确保被刷新

时机 3: _runAgentPrompt() 的 finally 块
    └── _flushPendingBashMessages()
        每次 agent run 结束后统一刷新, 最关键的触发点

_flushPendingBashMessages()
    │
    ├── _pendingBashMessages.length === 0 → return
    │
    └── 遍历 _pendingBashMessages
        ├── agent.state.messages.push(msg)     ← 注入 agent state
        └── sessionManager.appendMessage(msg)  ← 持久化到会话
        │
        └── _pendingBashMessages = []  ← 清空

为什么需要延迟刷新

LLM 调用过程中                 Bash 结果
    │                           │
    ├─ tool_use: bash          ├─ 立即加入 agent state
    │   "run ls -la"           │   ↓
    │                           │   破坏消息顺序:
    ├─ tool_result: "file1..." │   [tool_use, bashResult, tool_result]
    │                           │   应为: [tool_use, tool_result, bashResult]
    ├─ tool_use: edit          │
    │                           │   原因: bash 结果由扩展或异步路径产生,
    ├─ tool_result: "done"     │   不经过 agent-core 的 tool 执行序列
    │                           │
    └─ agent_end               └─ 延迟到 turn 结束后统一刷新
                                  [tool_use, tool_result, tool_use, tool_result, bashResult]
                                  正确顺序在 agent state 的末尾

12. abort() 中止链

abort()
    │
    ├── 1. abortRetry()
    │       └── _retryAbortController.abort()
    │           中断指数退避的 sleep, 防止重试在 abort 后继续
    │
    ├── 2. agent.abort()
    │       └── 中断 agent-core 的当前操作
    │           ├── 停止 LLM 流式生成
    │           ├── 中断正在执行的工具调用
    │           └── 设置 state.isStreaming = false
    │
    └── 3. agent.waitForIdle()
            └── 等待 agent 完全变为空闲状态
                确保后续操作 (compact, switchSession) 不竞争

总图:prompt() 到 agent LLM 调用的完整路径

用户输入: "write a test for login"
    │
    ▼
prompt()
    │
    ├── expandPromptTemplates? → "/" 开头?
    │   └── NO → 继续
    │
    ├── hasHandlers("input")?
    │   ├── YES → emitInput → continue → 继续
    │
    ├── _expandSkillCommand → 无 "/skill:" → 不变
    ├── expandPromptTemplate → 无 "{{}}" → 不变
    │
    ├── isStreaming?
    │   └── NO → 继续
    │
    ├── _flushPendingBashMessages()
    ├── model? → YES
    ├── hasConfiguredAuth? → YES
    │
    ├── _findLastAssistantMessage() → undefined → 跳过
    │
    ├── 构建消息数组
    │   [{ role: "user", content: [{ type: "text", text: "write a test..." }] }]
    │
    ├── emitBeforeAgentStart → 扩展无注入
    │
    ├── preflightResult(true)
    │
    ▼
_runAgentPrompt(messages)
    │
    ▼
agent.prompt(messages)
    │
    ├── [LLM 生成思考过程]
    ├── [LLM 调用 tool: read_file]
    ├── [tool_result: 返回文件内容]
    ├── [LLM 生成代码]
    └── [LLM 返回 stop]
    │
    ▼
_handlePostAgentRun()
    │
    ├── isRetryableError? → NO
    ├── checkCompaction? → NO (上下文充足)
    ├── hasQueuedMessages? → NO
    │
    └── return false (循环结束)
    │
    ▼
_flushPendingBashMessages() (finally)
    │
    ▼
prompt() 返回

[