- 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 断线后从事件重新同步。
用户发来的 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
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 管一轮业务执行。
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 中。
如果模型要求执行 write,系统先写入 tool call 的 pending 状态,再启动实际文件写入。这样:
- 进程在副作用开始后崩溃,数据库仍能说明“曾经准备执行什么”;
- UI 可以显示 pending / running / completed / error,而不是凭空出现结果;
- 恢复或诊断代码能区分“模型没发 call”“call 已记录但没结算”“call 已完成”。
这也解释了为什么 V2 不推荐一个纯内存的 Promise.all(toolCalls) 作为唯一真相。
在 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,而不是直接把数据库所有记录塞进模型。
V2 当前把一些问题显式保留为后续工作:durable ownership、崩溃后的自动 provider retry、最终 status、重复 tool call 的限制等。这个“未完成清单”本身也是设计证据:项目刻意不把本地 execution 当成已经解决的分布式执行。
因此要正确理解当前实现:
- 它已经把输入和大部分输出持久化;
- 它已经让一个 Session 在当前进程中有明确 coordinator;
- 它还没有把跨节点 ownership 和 crash recovery 伪装成完成品。
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 历史已经投影”拆成可观察的阶段。