50. coding-agent 会话层:把 Agent 变成可恢复的工作会话¶
这个模块干什么¶
AgentSession 包住 packages/agent 的 Agent,在它外面补上持久化、扩展事件、工具注册、队列显示、自动重试、压缩和会话统计。 【packages/coding-agent/src/core/agent-session.ts:303-401】
createAgentSession() 负责真正构造 Agent,恢复已有 transcript,再把它交给 AgentSession。 【packages/coding-agent/src/core/sdk.ts:169-188】 【packages/coding-agent/src/core/sdk.ts:294-390】
SessionManager 不把会话存成一条可变数组,而是把每次变化追加为带 id、parentId 的 JSONL tree entry。 【packages/coding-agent/src/core/session-manager.ts:46-80】 【packages/coding-agent/src/core/session-manager.ts:844-855】
AgentSessionRuntime 再往外包一层,统一处理 /new、resume、fork、import 和 cwd 改变时的整套 runtime 替换。 【packages/coding-agent/src/core/agent-session-runtime.ts:67-95】 【packages/coding-agent/src/core/agent-session-runtime.ts:193-257】
核心文件速查¶
| 文件 | 一句话职责 | 必读优先级 |
|---|---|---|
core/sdk.ts |
构造底层 Agent,恢复模型、thinking、messages,并创建 AgentSession。 【packages/coding-agent/src/core/sdk.ts:169-188】 【packages/coding-agent/src/core/sdk.ts:294-390】 |
⭐⭐⭐ |
core/agent-session.ts |
会话总控:接 Agent 事件、落盘、排队、重试、压缩、工具和扩展。 【packages/coding-agent/src/core/agent-session.ts:303-401】 | ⭐⭐⭐ |
core/session-manager.ts |
管理 append-only JSONL tree、当前 leaf、分支和 context 投影。 【packages/coding-agent/src/core/session-manager.ts:844-887】 【packages/coding-agent/src/core/session-manager.ts:1260-1303】 | ⭐⭐⭐ |
core/agent-session-runtime.ts |
持有当前 session 与 cwd-bound services,执行整套会话替换。 【packages/coding-agent/src/core/agent-session-runtime.ts:67-95】 | ⭐⭐⭐ |
core/agent-session-services.ts |
创建随 cwd 变化的 settings、resources、model runtime,并收集非致命诊断。 【packages/coding-agent/src/core/agent-session-services.ts:25-45】 【packages/coding-agent/src/core/agent-session-services.ts:129-190】 | ⭐⭐ |
core/messages.ts |
定义 coding-agent 自定义消息,并投影为 LLM 可见消息。 【packages/coding-agent/src/core/messages.ts:26-77】 【packages/coding-agent/src/core/messages.ts:140-195】 | ⭐⭐⭐ |
core/session-cwd.ts |
检查 session header 中保存的 cwd 是否仍存在,并给出结构化错误。 【packages/coding-agent/src/core/session-cwd.ts:14-59】 | ⭐⭐ |
core/event-bus.ts |
给 extensions/resources 提供按 channel 发布订阅的进程内总线。 【packages/coding-agent/src/core/event-bus.ts:3-32】 | ⭐ |
core/usage-totals.ts |
汇总 assistant、工具和摘要调用的 token/cost。 【packages/coding-agent/src/core/usage-totals.ts:4-28】 【packages/coding-agent/src/core/usage-totals.ts:36-69】 | ⭐⭐ |
core/cache-stats.ts |
从相邻 assistant usage 推断 prompt cache miss 与浪费成本。 【packages/coding-agent/src/core/cache-stats.ts:50-90】 【packages/coding-agent/src/core/cache-stats.ts:134-164】 | ⭐ |
core/timings.ts |
在 PI_TIMING=1 时记录启动阶段耗时;不参与会话语义。 【packages/coding-agent/src/core/timings.ts:1-22】 【packages/coding-agent/src/core/timings.ts:45-50】 |
⭐ |
精读¶
1. 先读 sdk.ts:AgentSession 不是 Agent 的子类¶
会话入口的简化签名是:
async function createAgentSession(
options?: CreateAgentSessionOptions,
): Promise<CreateAgentSessionResult>
createAgentSession() 先确定 cwd、agentDir、ModelRuntime、SettingsManager 和 SessionManager;没有显式 ResourceLoader 时才创建并 reload 默认 loader。 【packages/coding-agent/src/core/sdk.ts:169-185】
恢复不是直接读最后一条消息:它调用 sessionManager.buildSessionContext(),同时拿到压缩后的 messages、当前 thinking level 和当前 model。 【packages/coding-agent/src/core/sdk.ts:187-190】 【packages/coding-agent/src/core/session-manager.ts:461-470】
如果 session 里记录的 model 仍能从 ModelRuntime 找到且已有认证,就恢复它;否则再走初始模型选择,并保留 modelFallbackMessage。 【packages/coding-agent/src/core/sdk.ts:192-221】
thinking level 优先级是:显式 option、session 中的 thinking entry、settings 默认值,最后再按当前 model 能力 clamp。 【packages/coding-agent/src/core/sdk.ts:224-243】
默认激活的内建工具只有 read、bash、edit、write;allowlist、noTools 和 denylist 在创建 session 前先算成 initialActiveToolNames。 【packages/coding-agent/src/core/sdk.ts:245-251】
底层 Agent 的初始 systemPrompt 是空字符串、tools 是空数组,因为真正的工具 registry 和 prompt 要等 AgentSession 绑定 resources/extensions 后构造。 【packages/coding-agent/src/core/sdk.ts:294-301】 【packages/coding-agent/src/core/agent-session.ts:397-400】
Agent.convertToLlm 指向 coding-agent 的 convertToLlm() 包装器;当 blockImages 开启时,user/toolResult 中的 image block 会被替换成文本占位。 【packages/coding-agent/src/core/sdk.ts:255-290】
Agent.streamFn 不直接调用某个 provider SDK,而是进 ModelRuntime.streamSimple(),并在这里合并 timeout、provider retry、attribution headers 和 provider 扩展 hooks。 【packages/coding-agent/src/core/sdk.ts:302-330】
transformContext 交给 extension runner,steering/follow-up mode、transport、thinking budgets 也在构造 Agent 时从 settings 注入。 【packages/coding-agent/src/core/sdk.ts:349-360】
有历史时直接把 existingSession.messages 放回 agent.state.messages;新会话则先追加初始 model change 和 thinking change,保证之后可恢复。 【packages/coding-agent/src/core/sdk.ts:362-374】
最后才 new AgentSession({ agent, sessionManager, ... }),所以两层职责清楚:Agent 跑 loop,AgentSession 管产品级会话。 【packages/coding-agent/src/core/sdk.ts:376-390】
2. agent-session.ts 构造期:先接事件,再建 runtime¶
AgentSessionConfig 明确要求现成的 Agent、SessionManager、SettingsManager、ResourceLoader 和 ModelRuntime。 【packages/coding-agent/src/core/agent-session.ts:196-225】
构造函数立即做三件事:订阅 Agent 事件、安装 before/after tool hooks、安装 next-turn refresh。 【packages/coding-agent/src/core/agent-session.ts:375-400】
tool hooks 每次执行时读取当前 _extensionRunner,因此 reload extensions 后不用重新给 Agent 装 hook。 【packages/coding-agent/src/core/agent-session.ts:460-488】
beforeToolCall 把已验证参数转换为 tool_call 扩展事件;扩展抛出的非 Error 也会被包装成阻止执行的错误。 【packages/coding-agent/src/core/agent-session.ts:468-487】
afterToolCall 可替换 content、details、isError 和 usage,再交回底层 agent loop 形成最终 tool result。 【packages/coding-agent/src/core/agent-session.ts:490-517】
next-turn refresh 会把最新 system prompt、tools、model、thinking level 放进下一次 loop snapshot,避免多轮工具调用继续使用旧配置。 【packages/coding-agent/src/core/agent-session.ts:520-540】
3. 事件接线:扩展先看,外部 listener 后看,最后落盘¶
AgentSessionEvent 复用绝大多数 AgentEvent,但给 agent_end 增加 willRetry,并新增 settled、queue、compaction、retry、bash update 等事件。 【packages/coding-agent/src/core/agent-session.ts:138-181】
对外 subscribe() 只是把 listener 放进数组;取消订阅按引用删除对应项。 【packages/coding-agent/src/core/agent-session.ts:795-810】
_handleAgentEvent() 收到 queued user message 的 message_start 时,先从 steering/follow-up 展示队列删除,再发 queue_update。 【packages/coding-agent/src/core/agent-session.ts:594-616】
普通事件的固定顺序是:await _emitExtensionEvent(event),同步 _emit() 给 session listeners,然后在 message_end 分支持久化。 【packages/coding-agent/src/core/agent-session.ts:618-665】
这意味着扩展的 message_end handler 可以在落盘前返回 replacement;实现会原地改写 Agent 已保存的 message 对象,让 state、后续事件和持久化保持同一引用内容。 【packages/coding-agent/src/core/agent-session.ts:695-709】 【packages/coding-agent/src/core/agent-session.ts:747-765】
普通 user、assistant、toolResult 通过 appendMessage() 保存;custom message 走 appendCustomMessageEntry();bash、compaction、branch summary 在各自专用路径保存。 【packages/coding-agent/src/core/agent-session.ts:624-647】
assistant 成功时会清掉 overflow recovery 标记,并在此前发生过 retry 时立刻发成功的 auto_retry_end。 【packages/coding-agent/src/core/agent-session.ts:645-663】
底层 agent_end 不是 session 真正空闲点:_runAgentPrompt() 之后还可能自动 retry、自动 compact 或继续处理新排队消息。 【packages/coding-agent/src/core/agent-session.ts:1061-1103】
只有这些 post-run 工作都结束,finally 才清 system prompt override、冲刷 pending bash messages,并发 agent_settled。 【packages/coding-agent/src/core/agent-session.ts:1061-1072】
waitForIdle() 等的是 session 自己的 settled promise,而不是只看底层 Agent.isStreaming。 【packages/coding-agent/src/core/agent-session.ts:562-589】 【packages/coding-agent/src/core/agent-session.ts:1540-1553】
4. prompt():输入进入 Agent 前还有一整条 preflight¶
简化签名:
async prompt(text: string, options?: PromptOptions): Promise<void>
扩展 command 最先尝试执行;随后发 input event,允许扩展 handled 或 transform 输入。 【packages/coding-agent/src/core/agent-session.ts:1114-1149】
再往后才展开 skill command 和 prompt template;因此 input hook 看到的是用户原始文本,不是展开后的长文本。 【packages/coding-agent/src/core/agent-session.ts:1151-1156】
正在运行时必须显式指定 streamingBehavior,然后分别进入 _queueSteer() 或 _queueFollowUp()。 【packages/coding-agent/src/core/agent-session.ts:1158-1172】
非 streaming 路径先 flush 独立 bash 记录,再校验 model 和 auth。 【packages/coding-agent/src/core/agent-session.ts:1174-1195】
提交新 prompt 前还会检查上一个 assistant 是否需要压缩;这个入口会包含 aborted response,避免取消后留下超大 context。 【packages/coding-agent/src/core/agent-session.ts:1197-1202】
最终 messages 顺序是 user message、nextTurn custom messages、before_agent_start 注入的 custom messages。 【packages/coding-agent/src/core/agent-session.ts:1204-1244】
扩展还可为本轮临时替换 system prompt;override 在 _runAgentPrompt() finally 中清除。 【packages/coding-agent/src/core/agent-session.ts:1245-1265】 【packages/coding-agent/src/core/agent-session.ts:1061-1072】
steering 和 follow-up 在 UI 侧各自保存文本副本,同时把完整 user message 交给 Agent.steer() / Agent.followUp()。 【packages/coding-agent/src/core/agent-session.ts:1371-1400】
5. session-manager.ts:JSONL 是 append-only tree,不是聊天数组快照¶
header 保存 version、session id、timestamp、cwd 和可选 parent session。 【packages/coding-agent/src/core/session-manager.ts:30-44】
其余 entry 都有 id、parentId、timestamp;message、model change、thinking change、compaction、branch summary、custom、label、session info 都是独立节点。 【packages/coding-agent/src/core/session-manager.ts:46-153】
SessionManager 内部同时维护 fileEntries、byId、label maps 和当前 leafId。 【packages/coding-agent/src/core/session-manager.ts:855-887】
打开已有文件时会 load、验证 header、迁移旧版本、重建索引;非空但无效的显式文件会报错而不是覆盖。 【packages/coding-agent/src/core/session-manager.ts:895-928】
新 session 先只在内存建立 header,并把 flushed 设为 false。 【packages/coding-agent/src/core/session-manager.ts:930-955】
关键落盘策略在 _persist():第一条 assistant 出现前,不创建普通新会话文件;assistant 到来后一次性写入 header 和此前所有 entries,后续才逐行 append。 【packages/coding-agent/src/core/session-manager.ts:1015-1042】
这样只有用户输入、尚无 assistant 回复的临时会话不会留下大量空壳文件。 【packages/coding-agent/src/core/session-manager.ts:1018-1039】
_appendEntry() 永远把新 entry 接到当前 leaf,再把 leaf 前移。 【packages/coding-agent/src/core/session-manager.ts:1044-1049】
branch(id) 只移动 leaf,不改旧 entry;下一次 append 自然形成新分支。 【packages/coding-agent/src/core/session-manager.ts:1354-1365】
getBranch() 从 leaf 沿 parentId 回溯再 reverse,得到当前活动路径。 【packages/coding-agent/src/core/session-manager.ts:1255-1270】
buildSessionContext() 先从整条活动路径恢复 model/thinking,再把 compaction-aware entries 投影成 AgentMessage[]。 【packages/coding-agent/src/core/session-manager.ts:362-377】 【packages/coding-agent/src/core/session-manager.ts:461-470】
遇到最新 compaction 时,context 只保留 compaction entry、本次指定的 kept 段和 compaction 之后的新 entries;更老原文仍留在 JSONL tree。 【packages/coding-agent/src/core/session-manager.ts:410-454】
6. agent-session-runtime.ts 与 services:session 替换是整套重建¶
AgentSessionServices 只包含 cwd、agentDir、model runtime、settings、resource loader 和 diagnostics;它故意不包含 AgentSession。 【packages/coding-agent/src/core/agent-session-services.ts:66-79】
createAgentSessionServices() 规范化 cwd/agentDir,创建或复用 model/settings,reload resources,注册扩展提供的 providers,再返回诊断。 【packages/coding-agent/src/core/agent-session-services.ts:129-190】
createAgentSessionFromServices() 只是把这些已建好的 services 和 session options 转交 createAgentSession()。 【packages/coding-agent/src/core/agent-session-services.ts:193-219】
这样 cwd 改变时可以重建 cwd-bound resources/settings,而不必把进程级固定输入散落在每个切换分支。 【packages/coding-agent/src/core/agent-session-services.ts:30-45】
AgentSessionRuntime 保存当前 session、services、diagnostics 和一个可重复调用的 createRuntime factory。 【packages/coding-agent/src/core/agent-session-runtime.ts:74-95】
切换前先发可取消的 session_before_switch;通过后打开目标 SessionManager、检查 cwd、teardown 旧 session,再创建并 apply 新 runtime。 【packages/coding-agent/src/core/agent-session-runtime.ts:133-148】 【packages/coding-agent/src/core/agent-session-runtime.ts:193-220】
teardown 的顺序是 await session_shutdown、同步清 host UI、session.dispose()。 【packages/coding-agent/src/core/agent-session-runtime.ts:167-175】
因此新 runtime 创建失败时,旧 session 已经失效;错误由调用方负责展示。 【packages/coding-agent/src/core/agent-session-runtime.ts:67-72】
newSession() 根据当前 manager 是否持久化,创建 file-backed 或 in-memory manager,并保留同一个 cwd。 【packages/coding-agent/src/core/agent-session-runtime.ts:223-257】
fork 对持久化 session 会生成新文件;对 in-memory session 则在同一个 manager 中重置或创建 branch。 【packages/coding-agent/src/core/agent-session-runtime.ts:259-349】
import 会先解析路径、复制进 session dir、打开 JSONL、检查 cwd,再替换 runtime。 【packages/coding-agent/src/core/agent-session-runtime.ts:351-393】
7. session-cwd.ts:不要悄悄把旧项目会话跑到新目录¶
getMissingSessionCwdIssue() 只对有 session file 且保存 cwd 不存在的情况返回 issue;in-memory session 不触发。 【packages/coding-agent/src/core/session-cwd.ts:14-33】
issue 同时保留 session file、旧 cwd 和 fallback cwd,供错误文本或交互提示使用。 【packages/coding-agent/src/core/session-cwd.ts:3-7】 【packages/coding-agent/src/core/session-cwd.ts:35-42】
assertSessionCwdExists() 将它升级成带结构化 issue 字段的 MissingSessionCwdError。 【packages/coding-agent/src/core/session-cwd.ts:44-59】
初始 runtime、resume 和 import 都在创建 session 前调用这层检查。 【packages/coding-agent/src/core/agent-session-runtime.ts:206-217】 【packages/coding-agent/src/core/agent-session-runtime.ts:380-390】 【packages/coding-agent/src/core/agent-session-runtime.ts:411-428】
8. messages.ts:持久 transcript 与 LLM context 不是同一种类型¶
coding-agent 声明合并了四类消息:bashExecution、custom、branchSummary、compactionSummary。 【packages/coding-agent/src/core/messages.ts:26-77】
bashExecution 可用 excludeFromContext 留在 session/history 但不发给模型。 【packages/coding-agent/src/core/messages.ts:29-40】 【packages/coding-agent/src/core/messages.ts:148-161】
branch/compaction summary 会包进固定 <summary> 标记,转换为 user message 重新进入 LLM context。 【packages/coding-agent/src/core/messages.ts:11-24】 【packages/coding-agent/src/core/messages.ts:170-183】
原生 user、assistant、toolResult 直接透传;所有自定义投影都集中在 convertToLlm()。 【packages/coding-agent/src/core/messages.ts:140-195】
9. 小型支撑模块:事件、账单、缓存和启动计时¶
createEventBus() 用 Node EventEmitter 实现字符串 channel;on() 返回 unsubscribe。 【packages/coding-agent/src/core/event-bus.ts:12-28】
handler 被包成 async safe handler,异常只写 stderr,不会从 emit() 冒回发布方。 【packages/coding-agent/src/core/event-bus.ts:18-27】
DefaultResourceLoader 接受外部 event bus,否则为当前 loader 创建一个,供扩展资源共享。 【packages/coding-agent/src/core/resource-loader.ts:125-142】 【packages/coding-agent/src/core/resource-loader.ts:217-226】
usage-totals.ts 直接累加 input/output/cacheRead/cacheWrite/cost;成本 breakdown 把 assistant 按 provider/model 分组,把工具和摘要放入 Tools/summaries。 【packages/coding-agent/src/core/usage-totals.ts:12-28】 【packages/coding-agent/src/core/usage-totals.ts:36-69】
getSessionStats() 会遍历全部 entries,所以已经被压缩移出当前 context 的历史费用仍被计入。 【packages/coding-agent/src/core/agent-session.ts:3099-3153】
cache miss 以相邻 assistant request 为基线,忽略 1024 tokens 以内的噪声,并按实际 paid rate 与 cache-read rate 差额估算浪费。 【packages/coding-agent/src/core/cache-stats.ts:10-23】 【packages/coding-agent/src/core/cache-stats.ts:56-89】
compaction 或 branch summary 会重置 cache 扫描基线,因为下一轮 context 确实已经变化。 【packages/coding-agent/src/core/cache-stats.ts:104-130】
timings.ts 只在模块加载时发现 PI_TIMING === "1" 才工作;time() 记录相邻标记的增量,printTimings() 按 namespace 打印。 【packages/coding-agent/src/core/timings.ts:1-32】 【packages/coding-agent/src/core/timings.ts:34-50】
数据流¶
下面追踪一次“恢复旧 session,发 prompt,模型调用工具,最终落盘”的完整链路:
createAgentSession(options)
-> SessionManager.open/create
-> sessionManager.buildSessionContext()
-> new Agent({ convertToLlm, streamFn, transformContext, queues })
-> agent.state.messages = restored messages
-> new AgentSession({ agent, sessionManager, resources, ... })
-> agent.subscribe(_handleAgentEvent)
-> install tool hooks / next-turn refresh
-> build tool registry + system prompt
session.prompt(text)
-> extension command/input preflight
-> skill/template expansion
-> auth + pre-prompt compaction check
-> before_agent_start
-> Agent.prompt(messages)
-> Agent/agent-loop emits message/tool/turn events
-> AgentSession._handleAgentEvent(event)
-> extension event
-> session listener event
-> message_end: SessionManager.appendMessage(...)
-> _appendEntry(parentId = current leaf)
-> first assistant: flush whole JSONL
-> later entries: append one JSON line
-> post-run retry / compaction / queued continuation
-> flush pending bash messages
-> agent_settled
Agent 的构造、历史恢复和 AgentSession 创建发生在同一个入口。 【packages/coding-agent/src/core/sdk.ts:169-188】 【packages/coding-agent/src/core/sdk.ts:294-390】
AgentSession 在任何 prompt 之前已经接好 event listener 和 tool hooks。 【packages/coding-agent/src/core/agent-session.ts:375-400】
输入 preflight 到 _runAgentPrompt() 的顺序由 prompt() 固定。 【packages/coding-agent/src/core/agent-session.ts:1114-1265】
事件经过 extension、session listener、persistence 的次序在 _handleAgentEvent() 中固定。 【packages/coding-agent/src/core/agent-session.ts:594-665】
JSONL 首次 flush 与后续 append 的分界由第一条 assistant message 决定。 【packages/coding-agent/src/core/session-manager.ts:1015-1042】
真正的空闲事件要等 retry、compaction 和 queued continuation 全部处理完。 【packages/coding-agent/src/core/agent-session.ts:1061-1103】
自测¶
-
为什么
createAgentSession()先把Agent.systemPrompt和 tools 留空,再由AgentSession构造? 【packages/coding-agent/src/core/sdk.ts:294-301】 【packages/coding-agent/src/core/agent-session.ts:2547-2599】 -
message_end到来后,扩展 replacement、外部 listener 和 JSONL 持久化的先后顺序是什么? 【packages/coding-agent/src/core/agent-session.ts:618-665】 【packages/coding-agent/src/core/agent-session.ts:747-765】 -
为什么底层
agent_end已经出现时,AgentSession.isIdle仍可能是 false? 【packages/coding-agent/src/core/agent-session.ts:1061-1103】 -
SessionManager.branch()为什么不需要复制或删除旧消息就能形成新分支? 【packages/coding-agent/src/core/session-manager.ts:1044-1049】 【packages/coding-agent/src/core/session-manager.ts:1354-1365】 -
compaction 之后,为什么历史费用仍能统计,而旧消息又不会全部进入下一次 LLM context? 【packages/coding-agent/src/core/agent-session.ts:3099-3153】 【packages/coding-agent/src/core/session-manager.ts:410-470】