概述
本文档以 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() 返回
[