跳转至

50. coding-agent 会话层:把 Agent 变成可恢复的工作会话

这个模块干什么

AgentSession 包住 packages/agentAgent,在它外面补上持久化、扩展事件、工具注册、队列显示、自动重试、压缩和会话统计。 【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 不把会话存成一条可变数组,而是把每次变化追加为带 idparentId 的 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.tsAgentSession 不是 Agent 的子类

会话入口的简化签名是:

async function createAgentSession(
  options?: CreateAgentSessionOptions,
): Promise<CreateAgentSessionResult>

createAgentSession() 先确定 cwdagentDirModelRuntimeSettingsManagerSessionManager;没有显式 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】

默认激活的内建工具只有 readbasheditwrite;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 明确要求现成的 AgentSessionManagerSettingsManagerResourceLoaderModelRuntime。 【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 都有 idparentId、timestamp;message、model change、thinking change、compaction、branch summary、custom、label、session info 都是独立节点。 【packages/coding-agent/src/core/session-manager.ts:46-153】

SessionManager 内部同时维护 fileEntriesbyId、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 声明合并了四类消息:bashExecutioncustombranchSummarycompactionSummary。 【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】

自测

  1. 为什么 createAgentSession() 先把 Agent.systemPrompt 和 tools 留空,再由 AgentSession 构造? 【packages/coding-agent/src/core/sdk.ts:294-301】 【packages/coding-agent/src/core/agent-session.ts:2547-2599】

  2. 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】

  3. 为什么底层 agent_end 已经出现时,AgentSession.isIdle 仍可能是 false? 【packages/coding-agent/src/core/agent-session.ts:1061-1103】

  4. SessionManager.branch() 为什么不需要复制或删除旧消息就能形成新分支? 【packages/coding-agent/src/core/session-manager.ts:1044-1049】 【packages/coding-agent/src/core/session-manager.ts:1354-1365】

  5. compaction 之后,为什么历史费用仍能统计,而旧消息又不会全部进入下一次 LLM context? 【packages/coding-agent/src/core/agent-session.ts:3099-3153】 【packages/coding-agent/src/core/session-manager.ts:410-470】