Pi agent源码剖析一:启动流程分析
环境搭建
1. 下载pi-agent
1 | git clone https://github.com/earendil-works/pi.git |
2. understanding-anything方便代码理解
安装参考: https://github.com/Lum1104/Understand-Anything
1 | PS D:\workspace\pi> cd C:\Users\zhiminding\.understand-anything-plugin\packages\dashboard |
3. codegraph阅读代码节省token
安装参考: https://github.com/colbymchenry/codegraph
1 | codegraph init |
4. PI-agent服务启动
- 配置自定义模型
1 | // C:\Users\zhiminding\.pi\agent\models.json |
- 安装依赖(不执行 lifecycle scripts)
1 | npm install --ignore-scripts |
- 构建所有包
1 | npm run build |
- 从源码运行 pi(Windows 用 .bat 或 .ps1)
1 | ./pi-test.sh # Linux/macOS |
- 修改代码后,重新运行
pi-test.sh等启动脚本
启动分析
main启动流程
首先从main(packages\coding-agent\src\main.ts)函数开始分析。main是整个代理CLI的入口
1. 环境准备
1.1 处理windwos环境的清理
1.2 包管理/配置命令 处理 pi install、pi config 等子命令,命中则直接返回
1.3 处理命令行参数parseArgs(args) 解析 CLI 参数
1.4 运行模式决定, 确定是 interactive(TUI)、print、json 还是 rpc 模式
2. SessionManager初始化
2.1 创建SettingsManager实例
加载全局+项目级设置,入口代码:
1 | const cwd = process.cwd(); |
相关目录:
| 目录/文件 | 路径(Windows) | 说明 |
|---|---|---|
| agentDir | C:\Users\<user>\.pi\agent\ |
全局代理配置根目录 |
| settings.json | ~/.pi/agent/settings.json |
全局设置 |
| auth.json | ~/.pi/agent/auth.json |
认证凭据 |
| models.json | ~/.pi/agent/models.json |
自定义模型配置 |
| themes/ | ~/.pi/agent/themes/ |
用户自定义主题 |
| tools/ | ~/.pi/agent/tools/ |
自定义工具 |
| prompts/ | ~/.pi/agent/prompts/ |
自定义 prompt 模板 |
| bin/ | ~/.pi/agent/bin/ |
托管二进制(fd, rg) |
| sessions/ | ~/.pi/agent/sessions/ |
会话文件存储根目录 |
| debug log | ~/.pi/agent/pi-debug.log |
调试日志 |
可通过环境变量 PI_CODING_AGENT_DIR 覆盖 agentDir 路径。
2.2 SessionDir 解析
sessionDir 决定会话文件存储位置,解析优先级(从高到低):
1 | const sessionDir = |
默认 sessionDir 计算规则(getDefaultSessionDir()):
- 取当前工作目录 cwd,将路径分隔符替换为
- - 拼接为:
~/.pi/agent/sessions/--<encoded-cwd>--/ - 例如 cwd =
D:\workspace\pi→ sessionDir =~/.pi/agent/sessions/--D-workspace-pi--/
2.3 createSessionManager 详细逻辑
根据不同 CLI 参数走不同分支:
1 | async function createSessionManager(parsed, cwd, sessionDir, settingsManager) |
| CLI 参数 | 行为 | 静态方法 |
|---|---|---|
--no-session / --help / --list-models |
内存模式,不持久化 | SessionManager.inMemory(cwd) |
--fork <session> |
从源会话复制历史到当前项目创建新会话 | SessionManager.forkFrom(sourcePath, cwd, sessionDir) |
--session <id或path> |
打开指定会话(本地直接打开;跨项目则询问是否fork) | SessionManager.open(path, sessionDir) |
--resume |
弹出会话选择器让用户选择 | SessionManager.open(selectedPath, sessionDir) |
--continue |
自动续接最近一次会话(按mtime排序) | SessionManager.continueRecent(cwd, sessionDir) |
--session-id <id> |
打开指定ID的已有会话,或以该ID创建新会话 | SessionManager.open() 或 SessionManager.create() |
| (无参数) | 创建全新会话 | SessionManager.create(cwd, sessionDir) |
–fork 额外校验:
- 不能与
--session、--continue、--resume、--no-session同时使用 - 若指定了
--session-id,检查目标ID不能已存在
–session 的解析流程 (resolveSessionPath()):
- 若参数含
/、\或以.jsonl结尾 → 视为文件路径直接使用 - 否则先在当前项目 sessionDir 中匹配(精确匹配或前缀匹配)
- 若本地未找到,全局搜索所有项目的会话
- 全局找到时标记为
global,需要用户确认是否 fork 到当前项目
2.4 会话文件格式
会话以 .jsonl 文件存储(每行一个 JSON 对象),文件名格式:
1 | <timestamp>_<session-id>.jsonl |
文件结构(首行为 Header,后续为 Entry):
1 | {"type":"session","version":3,"id":"...","timestamp":"...","cwd":"D:/workspace/pi","parentSession":"..."} |
Entry 类型:
| type | 说明 |
|---|---|
message |
用户/助手/系统消息 |
thinking_level_change |
thinking 级别变更记录 |
model_change |
模型切换记录 |
compaction |
上下文压缩摘要(超出 context window 时触发) |
branch_summary |
分支摘要(从旧分支切到新分支时记录上下文) |
custom |
扩展自定义数据(不参与 LLM 上下文) |
custom_message |
扩展自定义消息(参与 LLM 上下文) |
label |
用户书签/标记 |
session_info |
会话元数据(如用户定义的显示名称) |
树形结构: 每个 Entry 有 id 和 parentId,形成 append-only 树。支持分支对话(branch),不会删除/修改历史。
2.5 MissingSessionCwd 处理
当恢复一个旧会话,但会话记录的 cwd 目录已不存在时:
1 | const missingSessionCwdIssue = getMissingSessionCwdIssue(sessionManager, cwd); |
- interactive 模式:弹出选择器,让用户选择是否在当前 cwd 继续
- 其他模式:直接报错退出
1 | // 用户选择继续后,以当前 cwd 覆盖会话的 cwd |
3.6 SessionManager作用
最终会使用newSession()创建一个jsonl文件,并通过sessionManager对这个session文件进行操作
1 | class SessionManager { |
核心工作机制:
fileEntries是完整数据,byId是索引,leafId是游标- 写入:
appendMessage()→ 创建 entry(parentId=leafId) → 更新 leafId → 写文件 - 分支:
branch(entryId)→ 仅修改 leafId 指向历史节点 → 下次 append 自然形成分支 - 读取:
buildSessionContext()→ 从 leafId 沿 parentId 链回溯到根 → 收集路径上的消息
3. 创建 AgentSession Runtime
整体调用链路:
1 | main() |
3.1 createAgentSessionServices — 创建 cwd 绑定的运行时服务
创建"基础设施"(不涉及具体会话)
源文件:packages/coding-agent/src/core/agent-session-services.ts
1 | export async function createAgentSessionServices( |
入参 CreateAgentSessionServicesOptions:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
cwd |
string | (必填) | 当前工作目录 |
agentDir |
string | ~/.pi/agent/ |
全局配置目录 |
authStorage |
AuthStorage | AuthStorage.create(agentDir/auth.json) |
认证凭据管理 |
settingsManager |
SettingsManager | SettingsManager.create(cwd, agentDir) |
设置管理器 |
modelRegistry |
ModelRegistry | ModelRegistry.create(authStorage, agentDir/models.json) |
模型注册表 |
extensionFlagValues |
Map<string, boolean|string> | undefined | CLI 传入的扩展 flag(--flagName) |
resourceLoaderOptions |
object | undefined | 资源加载器选项(见下表) |
resourceLoaderOptions 子选项(来自 main.ts 闭包):
| 选项 | 来源(CLI参数) | 说明 |
|---|---|---|
additionalExtensionPaths |
--extension <path> |
额外扩展路径 |
additionalSkillPaths |
--skill <path> |
额外技能路径 |
additionalPromptTemplatePaths |
--prompt-template <path> |
额外 prompt 模板路径 |
additionalThemePaths |
--theme <path> |
额外主题路径 |
noExtensions |
--no-extensions |
禁用所有扩展 |
noSkills |
--no-skills |
禁用所有技能 |
noPromptTemplates |
--no-prompt-templates |
禁用 prompt 模板 |
noThemes |
--no-themes |
禁用主题 |
noContextFiles |
--no-context-files |
禁用 AGENTS.md 等上下文文件 |
systemPrompt |
--system-prompt <text/path> |
覆盖系统提示词 |
appendSystemPrompt |
--append-system-prompt <text/path> |
追加系统提示词 |
extensionFactories |
代码注入 | 编程式注入的扩展工厂 |
内部执行流程:
- 解析
cwd和agentDir为绝对路径 - 创建
AuthStorage(管理 API Key 和 OAuth token) - 创建
SettingsManager(加载 settings.json) - 创建
ModelRegistry(加载内置模型 + models.json 自定义模型) - 创建
DefaultResourceLoader并调用reload()- 加载扩展(extensions)
- 加载技能(skills)
- 加载 prompt 模板(用于命令使用,如
/review、/pr) - 加载主题
- 加载 AGENTS.md 上下文文件
- 处理扩展注册的 Provider(
pendingProviderRegistrations) - 应用扩展 flag values
返回 AgentSessionServices:
1 | interface AgentSessionServices { |
当用户 /resume 会话替换,切换到另一个项目的会话时,cwd 变了,所有服务要重建(不同项目有不同的扩展、设置、AGENTS.md),所以暴露一个创建service的方法。这个service只服务环境,而不是session级别(createAgentSession)的
3.2 ResourceLoader — 资源加载器
源文件:packages/coding-agent/src/core/resource-loader.ts
DefaultResourceLoader 负责从多个来源加载所有可扩展资源:
加载搜索路径优先级(由高到低):
| 资源类型 | 搜索路径 |
|---|---|
| Extensions | CLI --extension > ~/.pi/agent/extensions/ > .pi/extensions/(项目级) > 包管理器安装的 |
| Skills | CLI --skill > 扩展提供的 > ~/.pi/agent/skills/ > .pi/skills/(项目级) |
| Prompt Templates | CLI --prompt-template > 扩展提供的 > ~/.pi/agent/prompts/ > .pi/prompts/(项目级) |
| Themes | CLI --theme > 扩展提供的 > ~/.pi/agent/themes/ > 内置主题 |
| Context Files | ~/.pi/agent/AGENTS.md > 项目祖先目录链中的 AGENTS.md/CLAUDE.md |
系统提示词(System Prompt)管理:
--system-prompt:完全覆盖默认系统提示词(可以是文本或文件路径)--append-system-prompt:追加到默认系统提示词后面- AGENTS.md 文件内容会被作为上下文注入到系统提示词中
- 扩展也可以通过 hook 修改系统提示词
构造系统提示词流程:packages\coding-agent\src\core\system-prompt.ts
1 | buildSystemPrompt() |
3.3 ModelRegistry — 模型注册表
源文件:packages/coding-agent/src/core/model-registry.ts
1 | ModelRegistry.create(authStorage, modelsPath) |
职责:
- 加载内置模型(
models.generated.ts中 4000+ 条模型定义) - 加载
models.json自定义模型(合并/覆盖内置模型) - 处理扩展注册的 Provider
- 提供 API Key 解析(
getApiKeyAndHeaders(model)) - 判断模型是否有有效认证(
hasConfiguredAuth(model)) - 列出可用模型(
getAvailable()— 仅返回有认证的模型)
API Key 解析优先级:
AuthStorage.getRuntimeApiKey()— CLI--api-key设置的临时 keyauth.json中对应 provider 的配置- 环境变量(如
ANTHROPIC_API_KEY、OPENAI_API_KEY) models.json中 provider 的apiKey字段- OAuth token(通过
/login获取)
3.4 resolveModelScope — 解析模型作用域
源文件:packages/coding-agent/src/core/model-resolver.ts
1 | export async function resolveModelScope( |
调用时机:在 main.ts 的 createRuntime 闭包中:
1 | const modelPatterns = parsed.models ?? settingsManager.getEnabledModels(); |
patterns 来源:
- CLI
--models pattern1,pattern2,... - 或 settings.json 中
enabledModels配置
匹配规则:
- 支持 glob 模式(
*sonnet*、anthropic/*) - 支持精确匹配(
claude-opus-4-7) - 支持
provider/modelId格式 - 支持附加 thinking level(
claude-opus-4-7:high) - 模糊匹配时优先 alias(无日期后缀)> 最新日期版本
返回 ScopedModel[]:
1 | interface ScopedModel { |
用于 TUI 中 Ctrl+P 快速切换模型。
3.5 buildSessionOptions — 构建会话选项
源文件:packages/coding-agent/src/main.ts 第 338-430 行
1 | function buildSessionOptions( |
模型选择优先级:
- CLI
--model <pattern>— 调用resolveCliModel()精确/模糊匹配 - scopedModels 中与 settings 默认模型匹配的那个
- scopedModels 的第一个
- (留空,后续由
createAgentSession内的findInitialModel兜底)
Thinking Level 选择优先级:
- CLI
--thinking <level>(最高优先级) --model pattern:level中解析出的 level- scopedModel 上配置的 level
- (留空,后续由 settings 默认值兜底)
工具配置:
| CLI 参数 | 效果 |
|---|---|
--no-tools |
noTools = "all",禁用所有工具 |
--no-builtin-tools |
noTools = "builtin",仅禁用内置工具 |
--tools read,bash |
仅启用列出的工具 |
3.6 AuthStorage — 认证管理
源文件:packages/coding-agent/src/core/auth-storage.ts
1 | const authStorage = AuthStorage.create(); // 读取 ~/.pi/agent/auth.json |
核心方法:
setRuntimeApiKey(provider, key)— 设置运行时临时 API Key(来自--api-key)getApiKey(provider)— 获取 provider 的 API KeysetOAuthToken(provider, token)— 保存 OAuth token(/login写入)getOAuthToken(provider)— 获取 OAuth token
main.ts 中的使用:
1 | if (parsed.apiKey) { |
3.7 createAgentSessionFromServices — 从服务创建会话
桥接接口,避免开发者直接调用 createAgentSession。这个接口有6个字段,包装一下传参为services的调用。
源文件:packages/coding-agent/src/core/agent-session-services.ts
1 | export async function createAgentSessionFromServices( |
入参 CreateAgentSessionFromServicesOptions:
| 参数 | 说明 |
|---|---|
services |
步骤 3.1 创建的服务集合 |
sessionManager |
会话管理器 |
sessionStartEvent |
会话启动事件(通知扩展) |
model |
选定的模型(可选,会内部兜底) |
thinkingLevel |
thinking 级别(可选) |
scopedModels |
Ctrl+P 可切换的模型列表 |
tools |
工具白名单 |
noTools |
工具禁用模式 |
customTools |
自定义工具定义 |
本质上是将 services 展开后调用 createAgentSession()。
3.8 createAgentSession — SDK 核心函数
源文件:packages/coding-agent/src/core/sdk.ts
这是真正创建 AgentSession 的函数,内部执行:
1. 模型选择流程(若 options.model 未指定):
1 | 如果恢复已有会话 → 尝试从会话历史的 model_change entry 恢复模型 |
2. Thinking Level 确定:
1 | CLI --thinking > 会话历史恢复 > settings 默认值 > DEFAULT_THINKING_LEVEL("medium") |
3. 创建 Agent 实例:
1 | agent = new Agent({ |
Agent 实例的作用(来自 @earendil-works/pi-agent-core 包):
Agent 是底层执行引擎,不关心 UI、文件持久化、扩展——只负责"对话循环":
- state:持有当前 systemPrompt、model、thinkingLevel、tools、messages(对话历史)
- prompt(text):接收用户输入 → 调用 LLM → 执行工具调用 → 循环直到 LLM 停止
- streamFn:实际调 LLM 的函数(通过 modelRegistry 获取 API Key 后调用 streamSimple)
- steer(msg) / followUp(msg):在 agent 运行中注入消息(steering = 当前轮结束后立即注入;followUp = agent 完全停止后才注入)
- abort():中止当前运行
- subscribe(listener):监听生命周期事件(agent_start、message_start、tool_call、agent_end 等)
- convertToLlm:将内部消息格式转为 LLM API 消息格式
- transformContext:发送前让扩展对上下文做最后变换
简单说:Agent = 一个无状态循环(用户消息 → LLM → 工具 → LLM → … → 停止),所有"记住什么、存在哪"都不是它管的。
4. 恢复/初始化会话消息:
1 | if (hasExistingSession) { |
5. 创建 AgentSession:
1 | const session = new AgentSession({ |
6. 返回结果:
1 | interface CreateAgentSessionResult { |
AgentSession 的作用(packages/coding-agent/src/core/agent-session.ts):
AgentSession 是高层业务协调者,在 Agent 之上叠加了所有"编码代理"需要的能力:
| 职责 | 说明 |
|---|---|
| 会话持久化 | 监听 Agent 事件,自动将消息写入 SessionManager(.jsonl 文件) |
| System Prompt 构建 | 组装工具描述 + guidelines + AGENTS.md + skills → 设置 agent.state.systemPrompt |
| 工具管理 | 注册内置工具 + 扩展工具 + 自定义工具,管理启用/禁用状态 |
| 模型切换 | /model 命令切换模型、记录 model_change entry、更新 agent.state.model |
| Thinking Level | 切换 thinking 级别、clamp 到模型能力范围、记录 entry |
| 上下文压缩 | 监控 token 用量,超阈值时触发 compaction(摘要压缩历史消息) |
| Prompt 展开 | 用户输入 → 展开 prompt template → 展开 skill block → 交给 Agent |
| 扩展生命周期 | 创建 ExtensionRunner、分发事件(session_start、turn_start、message_end 等) |
| Bash 执行 | 提供 executeBash() 方法,包装 bash 工具的超时、权限等逻辑 |
| 分支/导航 | 暴露 branch、label 等操作给 UI 层 |
| 导出 | exportToHtml() 将会话导出为 HTML |
Agent vs AgentSession 的关系:
1 | AgentSession(高层:业务逻辑 + 持久化 + 扩展) |
- Agent 不知道文件系统、不知道扩展、不知道 session 文件
- AgentSession 监听 Agent 事件,把每条消息同步写入 .jsonl,管理工具注册,驱动扩展 hook
- 所有运行模式(interactive / print / rpc)共享同一个 AgentSession,只是上面的 UI 层不同
3.9 createAgentSessionRuntime — 顶层包装
源文件:packages/coding-agent/src/core/agent-session-runtime.ts
1 | export async function createAgentSessionRuntime( |
执行流程:
assertSessionCwdExists()— 确保会话的 cwd 存在- 调用
createRuntime(options)— 执行工厂函数(3.1~3.8 全部流程) - 将结果包装为
AgentSessionRuntime实例
AgentSessionRuntime 类:管理当前会话的生命周期,提供会话替换能力:
| 方法 | 触发场景 | 说明 |
|---|---|---|
switchSession(path) |
/resume 选择另一个会话 |
teardown 当前 → 创建新 runtime |
newSession() |
/new 创建新会话 |
teardown 当前 → 创建新 runtime |
fork(entryId) |
/fork 从某个 entry 分支 |
创建分支 → teardown → 新 runtime |
importFromJsonl(path) |
/import 导入外部会话 |
复制文件 → open → 新 runtime |
dispose() |
退出程序 | 发送 session_shutdown 事件 → 释放资源 |
会话替换的统一模式:
1 | async switchSession(path) { |
3.10 默认内置工具
1 | const defaultActiveToolNames: ToolName[] = ["read", "bash", "edit", "write"]; |
| 工具名 | 工厂函数 | 说明 |
|---|---|---|
read |
createReadTool(cwd) |
读取文件内容 |
bash |
createBashTool(cwd) |
执行 shell 命令 |
edit |
createEditTool(cwd) |
编辑文件(search/replace) |
write |
createWriteTool(cwd) |
写入文件 |
grep |
createGrepTool(cwd) |
搜索文件内容(需要格式化输出而非通用bash时,需手动启用) |
find |
createFindTool(cwd) |
查找文件(需要格式化输出而非通用bash时,需手动启用) |
ls |
createLsTool(cwd) |
列出目录(需要格式化输出而非通用bash时,需手动启用) |
工具访问通过 withFileMutationQueue() 包装,确保文件操作的顺序一致性。
3.11 整体数据流总结
1 | ┌─────────────────────────────────────────────────────────────────┐ |
4. 读取输入 & 准备初始消息
对应 main.ts 第 687-704 行:
4.1 readPipedStdin — 读取管道输入
1 | async function readPipedStdin(): Promise<string | undefined> |
- 如果 stdin 是 TTY(交互终端)→ 返回 undefined,不读取
- 如果 stdin 是管道(如
echo "fix bug" | pi)→ 读取全部内容返回 - 副作用:如果读到了管道内容,appMode 从
interactive降级为print(执行一次就退出)
4.2 prepareInitialMessage — 准备首条消息
1 | async function prepareInitialMessage(parsed, autoResizeImages, stdinContent?) |
将各种输入源合并为一条初始消息:
| 输入源 | 示例 | 说明 |
|---|---|---|
-p "message" |
pi -p "fix the bug" |
CLI 直接传入的 prompt 文本 |
@file 参数 |
pi @image.png @code.ts |
文件内容/图片,通过 processFileArguments 处理 |
| stdin 管道 | cat error.log | pi |
readPipedStdin 读到的内容 |
这些内容通过 buildInitialMessage() 拼接为最终的 initialMessage 字符串 + initialImages 图片数组。
4.3 initTheme — 初始化主题
加载主题配色方案(interactive 模式下还会启动主题文件 watcher,支持热更新)。
5. 启动运行模式
对应 main.ts 第 729-773 行,根据 appMode 进入不同的运行循环:
5.1 RPC 模式(appMode === "rpc")
1 | await runRpcMode(runtime); // 永不返回(process.exit 退出) |
- 接管 stdout 为 JSON-RPC 输出通道
- 从 stdin 逐行读取 JSON-RPC 请求,分发到 session 执行
- 用于外部程序(IDE 插件、自动化脚本)集成调用 pi
- 支持的命令:prompt、abort、getState、switchModel、newSession 等
5.2 Interactive 模式(appMode === "interactive")— 最常用
1 | const interactiveMode = new InteractiveMode(runtime, { |
InteractiveMode.run() 内部流程:
-
init()— 初始化 TUI:- 注册信号处理器(Ctrl+C 等)
- 下载
fd和rg工具(如果缺失) - 显示启动信息(版本、模型范围、快捷键提示)
- 创建 TUI 组件树(header、editor、message 列表等)
- 绑定 AgentSession 事件到 UI 组件
-
异步检查(不阻塞启动):
- 版本更新检查(
checkForNewPiVersion) - 已安装包更新检查
- tmux 键盘兼容性检查
- 版本更新检查(
-
处理初始消息(如果有):
1
2
3if (initialMessage) {
await this.session.prompt(initialMessage, { images: initialImages });
} -
主循环(核心交互):
1
2
3
4
5// interactiveMode.run() {
while (true) {
const userInput = await this.getUserInput(); // 等待用户在编辑器中输入
await this.session.prompt(userInput); // 发送给 AgentSession 处理
}getUserInput()阻塞等待用户输入(TUI 编辑器组件)- 用户按 Enter 提交 →
session.prompt()展开模板 → 调 Agent → LLM → 工具循环 → 输出 - 循环永远不会自然结束(通过 Ctrl+C /
/exit触发信号退出)
5.3 Print 模式(appMode === "print" 或 "json")
1 | const exitCode = await runPrintMode(runtime, { |
- 执行完所有消息后立即退出(非交互式)
- 适用于脚本自动化:
pi -p "explain this code" < file.ts text模式直接输出 assistant 回复文本json模式输出结构化 JSON(包含 tool calls、metadata 等)- 返回 exitCode(0=成功,非0=错误)
总结:main() 完整生命周期
1 | main(args) |
完整对话流程
本节从 interactiveMode.run() 开始,详细分析一次完整的用户输入 → LLM 响应 → 工具执行 → 输出的全流程。
总览:调用链路
1 | InteractiveMode.run() [UI 层 — TUI 交互] |
1. 用户输入收集(InteractiveMode)
源文件:packages/coding-agent/src/modes/interactive/interactive-mode.ts
1.1 主循环
1 | // InteractiveMode.run() 的核心循环 |
1.2 getUserInput() — Promise 回调模式
1 | async getUserInput(): Promise<string> { |
这是一个经典的"等待外部触发"模式:
getUserInput()创建一个 Promise,将 resolve 函数暂存为onInputCallback- 当用户在 TUI 编辑器中按 Enter 提交文本时,
setupEditorSubmitHandler中的onSubmit回调触发this.onInputCallback(text) - Promise resolve,主循环继续执行
session.prompt(userInput)
1.3 编辑器提交处理
setupEditorSubmitHandler() 的逻辑分三种情况:
| 场景 | 处理方式 |
|---|---|
内置 UI 命令(/settings、/model、/export 等) |
直接在 InteractiveMode 内处理,不走 session |
| Agent 正在流式输出中 | 调用 session.prompt(text, { streamingBehavior: "steer" }) 排队 |
| 正常输入 | 调用 this.onInputCallback(text) 唤醒主循环 |
2. AgentSession.prompt() — 业务层处理
源文件:packages/coding-agent/src/core/agent-session.ts(第 962 行)
这是从用户文本到 Agent 执行的桥梁,负责一系列预处理。
2.1 完整处理流程
1 | prompt(text, options?) |
2.2 _runAgentPrompt — 调用 Agent 并处理后续
1 | private async _runAgentPrompt(messages: AgentMessage | AgentMessage[]): Promise<void> { |
2.3 _handlePostAgentRun — 后处理循环
Agent 一轮执行完毕后,检查是否需要继续:
1 | private async _handlePostAgentRun(): Promise<boolean> { |
可重试错误类型:网络超时、Rate Limit(429)、服务端错误(5xx)、上下文过长(overflow) 等。
3. Agent.prompt() — 引擎层入口
源文件:packages/agent/src/agent.ts(第 327 行)
Agent 是无状态的对话循环引擎,不关心 UI、持久化、扩展。
3.1 prompt() 方法
1 | async prompt(input: string | AgentMessage | AgentMessage[], images?: ImageContent[]): Promise<void> { |
关键约束:同一时刻只能有一个 activeRun。如果 Agent 正在执行,外部只能通过 steer() 或 followUp() 排队。
3.2 runPromptMessages — 准备上下文并进入循环
1 | private async runPromptMessages(messages: AgentMessage[], options = {}): Promise<void> { |
3.3 createContextSnapshot — 上下文快照
1 | private createContextSnapshot(): AgentContext { |
3.4 createLoopConfig — 循环配置
1 | private createLoopConfig(options = {}): AgentLoopConfig { |
3.5 runWithLifecycle — 生命周期管理
1 | private async runWithLifecycle(executor: (signal: AbortSignal) => Promise<void>): Promise<void> { |
3.6 processEvents — 状态同步 + 事件分发
Agent 收到循环事件后做两件事:
1 | private async processEvents(event: AgentEvent): Promise<void> { |
4. runAgentLoop — 循环核心
源文件:packages/agent/src/agent-loop.ts(第 95 行)
4.1 入口:runAgentLoop
1 | export async function runAgentLoop( |
4.2 runLoop — 双层循环
这是整个 Agent 的核心调度逻辑,采用双层循环设计:
1 | ┌─ 外层循环(Outer Loop)────────────────────────────────────────┐ |
详细伪代码:
1 | async function runLoop(initialContext, newMessages, initialConfig, signal, emit, streamFn) { |
5. streamAssistantResponse — LLM 流式请求
源文件:packages/agent/src/agent-loop.ts(第 275 行)
这是 AgentMessage[] → Message[] 转换 + LLM 调用的地方。
5.1 执行流程
1 | streamAssistantResponse(context, config, signal, emit, streamFn) |
5.2 流式事件传递链
1 | LLM API (HTTP SSE) |
5.3 关键事件类型
| StreamEvent | AgentEvent | UI 效果 |
|---|---|---|
start |
message_start |
创建 assistant 消息组件,开始流式显示 |
text_delta |
message_update |
增量追加文本到消息组件 |
thinking_delta |
message_update |
增量追加思考内容 |
toolcall_start |
message_update |
创建工具执行组件 |
toolcall_delta |
message_update |
更新工具参数显示 |
done |
message_end |
完成消息渲染 |
error |
message_end |
显示错误信息 |
6. executeToolCalls — 工具执行
源文件:packages/agent/src/agent-loop.ts(第 373 行)
6.1 执行策略
根据 config.toolExecution 和工具自身的 executionMode 决定执行方式:
| 条件 | 执行方式 |
|---|---|
config.toolExecution === "sequential" |
串行执行所有工具 |
任一工具标记 executionMode: "sequential" |
串行执行所有工具 |
| 其他情况(默认) | 并行执行所有工具 |
6.2 单个工具调用的完整生命周期
1 | executeToolCalls(context, assistantMessage, config, signal, emit) |
6.3 并行执行细节
并行模式下的执行策略更精细:
1 | // 1. 遍历所有 toolCalls,发出 tool_execution_start |
6.4 terminate 语义
1 | function shouldTerminateToolBatch(finalizedCalls): boolean { |
terminate = true:该批次工具都认为不需要再调 LLM → 内层循环的hasMoreToolCalls = falseterminate = false(默认):工具结果需要反馈给 LLM →hasMoreToolCalls = true→ 继续循环
7. 事件传递与持久化
7.1 AgentSession._handleAgentEvent — 事件中转站
源文件:packages/coding-agent/src/core/agent-session.ts(第 469 行)
AgentSession 通过 agent.subscribe(this._handleAgentEvent) 监听所有 Agent 事件,负责:
1 | _handleAgentEvent(event) |
7.2 InteractiveMode.handleEvent — UI 更新
| 事件类型 | UI 行为 |
|---|---|
agent_start |
清空工具追踪、启动加载动画 |
message_start (assistant) |
创建 AssistantMessageComponent,开始流式渲染 |
message_update |
增量更新文本/思考/工具内容到组件 |
message_end |
完成渲染、处理错误状态 |
tool_execution_start |
创建工具执行进度组件 |
tool_execution_update |
更新进度(如 bash 输出) |
tool_execution_end |
显示工具执行结果 |
agent_end |
停止加载动画、清理状态、准备接收下一次输入 |
auto_retry_start |
显示"正在重试…"提示 |
queue_update |
更新消息队列显示 |
8. Steering 和 FollowUp 机制
8.1 概念对比
| 机制 | 注入时机 | 用途 |
|---|---|---|
| Steering | 当前轮(assistant turn)结束后、下一轮 LLM 调用前 | 用户在 Agent 执行中追加指令 |
| FollowUp | Agent 完全停止(无更多 tool calls)后 | 延迟执行的后续任务 |
8.2 数据流
1 | 用户在流式中输入 "stop writing tests" |
8.3 队列模式(QueueMode)
1 | type QueueMode = "one-at-a-time" | "all"; |
"one-at-a-time"(默认):每次 drain() 只取出一条消息"all":每次 drain() 取出所有积压消息
9. 错误处理与重试
9.1 重试流程
1 | Agent 运行结束 |
9.2 上下文压缩(Compaction)
当 token 用量接近模型的 contextWindow 时触发:
1 | _checkCompaction(lastAssistantMessage) |
10. 完整时序图
一次典型的用户输入完整流程:
1 | 用户按 Enter 时间线 |
11. 关键设计总结
11.1 三层架构分离
| 层次 | 组件 | 职责 | 不关心 |
|---|---|---|---|
| UI 层 | InteractiveMode |
渲染、键盘事件、用户交互 | 模型细节、持久化 |
| 业务层 | AgentSession |
命令解析、扩展、验证、持久化、压缩、重试 | LLM 协议、流式解析 |
| 引擎层 | Agent + runLoop |
纯对话循环、工具执行、消息管理 | 文件系统、UI、扩展系统 |
11.2 事件驱动架构
整个系统通过事件流串联:
- 发布者:
runLoop中的emit()调用 - 中间件:
Agent.processEvents()做状态同步 - 消费者链:Agent listeners → AgentSession._handleAgentEvent → InteractiveMode.handleEvent
11.3 消息队列设计
双队列(steering + followUp)实现了"用户可以在 Agent 执行中追加指令"的能力,而不需要 abort 当前执行:
1 | steering: 用户急需插入的指令 → 当前轮结束后立即处理 |
11.4 不可变快照 + 可变推进
createContextSnapshot()在循环开始前创建上下文快照(独立副本)- 循环内直接 push 到
currentContext.messages(可变推进) Agent._state.messages通过processEvents的message_end逐步同步- 保证循环内的上下文变化不会被外部意外修改

