Skip to content

Latest commit

 

History

History
122 lines (85 loc) · 6.14 KB

File metadata and controls

122 lines (85 loc) · 6.14 KB

第 4 章:Session V2 —— 把 Agent Loop 改造成可恢复的 Session Drain

1. 先区分三个词:Session、Drain、Provider Turn

  • Session:持久化的对话与执行对象,有稳定 ID、Location、agent、model 和历史。
  • Session Drain:某个进程为这个 Session 执行的一段本地 continuation;它没有 durable identity,也不是 transcript boundary。
  • Provider Turn:一次 llm.stream(request) 调用以及它投影出的 assistant / tool 事件。

传统 loop 往往把三者压成一个 while (true)。V2 把它们分开,目的就是允许输入先落盘、执行失败后可诊断、UI 断线后从事件重新同步。

2. Durable input:先接纳,再运行

用户发来的 prompt 会先进入 SessionInput。它至少区分两种投递语义:

类型 语义 何时进入 Session History
steer 指导当前运行,尽快在安全边界吸收 当前 provider turn 或工具结算后
queue 排队等待,不打断当前连续执行 Session 即将 idle 时

这就是 AGENTS.md 中“prompt admission 与 model execution 分开”的具体含义:HTTP handler 成功写入 inbox,并不等于当前线程已经执行模型。

stateDiagram-v2
    [*] --> Admitted: POST prompt
    Admitted --> Promoted: safe provider-turn boundary
    Promoted --> ProviderTurn: build request
    ProviderTurn --> ToolSettlement: model emits tool call
    ToolSettlement --> ProviderTurn: local tools settled
    ProviderTurn --> Idle: no continuation
    ProviderTurn --> Admitted: new steer accepted
    Idle --> Promoted: queued input becomes eligible
    Admitted --> [*]: durable record remains after process exit
Loading

3. SessionExecutionSessionRunner 的分工

packages/core/src/session/execution.ts 的 Service 提供:

  • active():列出当前进程拥有的 active session;
  • resume(sessionID):空闲时启动或加入 execution;
  • wake(sessionID):记录新工作并触发 advisory wake,重复 wake 可合并;
  • interrupt(sessionID):中断当前进程拥有的执行,空闲时 no-op。

SessionRunner 则只提供 run({ sessionID, force }):从已记录的历史中 drain eligible durable work,执行 provider turn,并在需要时继续。

这是一种“控制平面 / 执行平面”分离:Execution 管调度和拥有关系,Runner 管一轮业务执行。

4. Runner 的真实骨架

packages/core/src/session/runner/llm.ts 的注释和实现给出了清晰阶段:

1. 从 SessionStore 读取 session
2. 确认当前 Location 仍匹配
3. 选择 agent,初始化 Context Epoch
4. 在 cutoff 前提升 steer / queue 输入
5. 读取 projected history
6. 物化可见工具定义
7. 生成 canonical LLM request
8. 必要时 compact / recover overflow
9. 执行一个 llm.stream(request)
10. 逐事件持久化 assistant / reasoning / tool / usage
11. 等待所有 local tool fibers settlement
12. 重新加载 history,决定是否继续下一 turn

关键点是第 9 步:一个 provider turn 就是一次显式的 llm.stream(request) 工具执行和 continuation 保留在 runner 里,但不把所有内容藏在 provider SDK 的隐式 loop 中。

5. 为什么要“durably record tool call before side effects”

如果模型要求执行 write,系统先写入 tool call 的 pending 状态,再启动实际文件写入。这样:

  • 进程在副作用开始后崩溃,数据库仍能说明“曾经准备执行什么”;
  • UI 可以显示 pending / running / completed / error,而不是凭空出现结果;
  • 恢复或诊断代码能区分“模型没发 call”“call 已记录但没结算”“call 已完成”。

这也解释了为什么 V2 不推荐一个纯内存的 Promise.all(toolCalls) 作为唯一真相。

6. 一次 provider turn 的关键状态

runTurnAttempt 内可以看到这些状态:

  • promotion:本轮是否先提升 steer / queue;
  • currentStep:agent step limit;
  • system:当前 Context Epoch 的 baseline;
  • entries:根据 baseline sequence 选出的 history;
  • toolMaterialization:当前 permission 和 step limit 下可见的工具;
  • publisher:把 LLMEvent 转成 SessionEvent / durable projection;
  • toolFibers:并发执行 local tool,但在继续前等待结算。

这些状态组合出一个受控的 provider request,而不是直接把数据库所有记录塞进模型。

7. 中断和失败的设计取舍

V2 当前把一些问题显式保留为后续工作:durable ownership、崩溃后的自动 provider retry、最终 status、重复 tool call 的限制等。这个“未完成清单”本身也是设计证据:项目刻意不把本地 execution 当成已经解决的分布式执行。

因此要正确理解当前实现:

  • 它已经把输入和大部分输出持久化;
  • 它已经让一个 Session 在当前进程中有明确 coordinator;
  • 它还没有把跨节点 ownership 和 crash recovery 伪装成完成品。

8. 和 Pi-Agent Loop 的差别

Pi 的教学重点是 stopReason 驱动的内存 loop;OpenCode 的 V2 则把 loop 拆为:

admission → promotion → provider turn → tool settlement → continuation

stopReason 仍有对应语义,但系统的“下一步做什么”还受到 durable input、Context Epoch、permission、compaction、agent step limit 和 Location 的共同影响。

本章小结

Session V2 的核心创新不是换了一个 loop 函数,而是把“用户输入已被接收”“当前进程在执行”“模型正在请求”“工具已经结算”“Session 历史已经投影”拆成可观察的阶段。

源码锚点