跳到主要内容

持久化执行模型:session / turn / step 与 Workflow

30 秒导读: eve 把一段 agent 对话做成"能跑几天、进程崩了/重新部署了也不丢进度"的东西。秘密是三层嵌套——session(整段对话)装着多个 turn(一次用户消息触发的全部工作),每个 turn 又装着多个 step(一次模型调用 + 它发起的工具调用)。每个 turn 是一个独立的 Workflow 运行,每个 step 是一个 durable checkpoint:做完的 step 永不重跑,崩溃后从上一个完成的 step 续跑;在等审批 / 等 OAuth / 等子 agent 时,workflow 挂起、零计算,输入到了再原地醒来。

这是全 eve 最硬核的一章,解释了为什么 eve 自称 durable(持久化的)。读之前不必读别章;读完你会知道 eve 一句"sessions are durable by default"背后到底发生了什么。配套阅读:文件系统即接口讲 turn 里跑的 agent 是怎么编译出来的,默认 harness讲一个 step 内部的 tool-loop。本章不展开 harness 内部细节。


1. 这是什么(零基础也能懂)

先建立一个心智模型

把 eve 的一段对话想象成一个能随时被冻住、再解冻的程序

普通的聊天后端是"无状态请求-响应":你发一条消息,服务器跑一遍逻辑,返回结果,然后忘记一切;下一条消息靠数据库里的历史从头拼。eve 不是这样——它把"整段对话连同正在跑到一半的工作"当成一个长活的、可持久化的进程:

一句话定义: eve 的一个 session(会话) 是一段持久化的对话——它可以跑上几天、进程重启或重新部署都不丢上下文,而这一切你不用写一行持久化代码。

docs/concepts/execution-model-and-durability.md 开篇就是这句承诺:

"An eve session is a durable conversation. It can run for days and survives process restarts and redeploys without any work on your part."

它解决谁的什么问题

写过 agent 的人都踩过这些坑:

痛点没有持久化执行时会怎样
模型调用跑了 30 秒,服务器这时重启整轮白跑,要么报错、要么从头再来
agent 要等人审批一个危险操作进程得一直占着内存空转,或者你自己存状态、重建
OAuth 登录要跳浏览器,几分钟后才回调同上:挂在那儿耗资源,或丢失现场
你改了 agent 的 prompt 重新部署进行中的对话怎么办?

eve 把这些全部下沉到运行时。你只管写"能力"(工具、指令、channels),eve 替你把循环跑成持久化的。

一句话直觉/类比

把它想成带存档点的游戏:turn 是一个关卡,step 是关卡里的存档点。打到一半断电,重开时不是回到关卡开头,而是回到最近一个存档点——已经打过的存档点直接读取结果,不重打。


2. 顶层全景(它大概怎么转)

三层嵌套是骨架

eve 的执行模型只有三个名词,记住它们的嵌套关系,后面全章都顺了:

session ── 整段持久化对话/任务(长活,可跨天)

├─ turn ── 一条用户消息触发的全部工作(模型/工具/推理,直到产出回复)
│ │
│ ├─ step ── 一个 durable checkpoint(一次模型调用 + 它发起的工具调用)
│ ├─ step
│ └─ step

├─ turn
└─ turn

概念文档 docs/concepts/execution-model-and-durability.md:10-14 把三层定义得很干净:

  • session:the whole durable conversation or task,长活、可跨天/跨周。
  • turn:one user message and all the work it triggers,直到 agent 产出回复。
  • step:a durable checkpoint inside a turn,一次模型调用和它发起的工具调用。

关键映射:概念 → Workflow 原语

这三层不是纯概念,它们直接落到代码上的两个 Workflow 指令。这是本章最该记住的一张表:

概念层对应的代码实体Workflow 指令源码
session长活的 driver(driver loop)"use workflow"workflowEntry(workflow-entry.ts:66)
turn每 turn 一个子 workflow 运行"use workflow"turnWorkflow(turn-workflow.ts:32)
step一个 durable step"use step"turnStep(workflow-steps.ts:108)

一句话:session 是一个长活 workflow(driver),它把每个 turn 派发成一个独立的子 workflow,turn 内部把每个 harness 步骤跑成一个 durable step。

顶层数据流(走一遍,不进代码)

下图是"一条用户消息进来"的高层流向。怎么读这张图: 从上到下是控制流,实线是派发,虚线是"挂起后被唤醒"。

用户消息 (HTTP / channel)


┌──────────────────────────┐
│ workflowEntry (driver) │ ← session 级长活 workflow,"use workflow"
│ 拥有公开投递 hook + 生命周期 │
└──────────────┬───────────┘
│ dispatchTurnStep:启动一个子 workflow

┌──────────────────────────┐
│ turnWorkflow │ ← 每 turn 一个,"use workflow"
│ 跑一个完整逻辑 turn │
└──────────────┬───────────┘
│ 循环调用 turnStep

┌──────────────────────────┐
│ turnStep │ ← 每步一个 durable checkpoint,"use step"
│ 一次模型调用 + 工具调用 │
└──────────────┬───────────┘
│ 每步结尾:序列化 session 快照 → 写进 step 结果

done / park / continue

park 时:workflow 挂起,零计算 ┄┄► 等输入到达,原地醒来

部件一句话职责:

部件干什么在哪个文件
workflowEntrysession 级 driver:拥有公开 delivery hook、跑 driver loop、派发每个 turnexecution/workflow-entry.ts
turnWorkflow一个完整逻辑 turn 的 workflow,循环跑 step、处理子 agent 等待execution/turn-workflow.ts
turnStep一个原子 harness 步骤,跑在 "use step" 边界里execution/workflow-steps.ts
durable-session-storestep 之间持久化 session 快照的读写execution/durable-session-store.ts
TurnExecutionCursor一个 turn 内"当前 durable 状态"的可变游标execution/turn-execution-cursor.ts
turn-control-protocolturn 向 driver 回报 done/park 的控制通道execution/turn-control-protocol.ts

3. 核心原理(逐个机制,由浅入深)

3.1 每个 turn 是一个 Vercel Workflow

它要解决的小问题: 怎么让"一次用户消息触发的全部工作"在中途崩溃后能续上,而不是从头再来?

思路/直觉: 把这段工作交给一个持久化执行引擎去跑。eve 用的是开源的 Workflow SDK(部署在 Vercel 上时就是 Vercel Workflow)。你只要在一个普通 async 函数顶上写一句 "use workflow",它就变成一个可持久化、可恢复的 workflow 函数。

turnWorkflow 就是这么一个函数。注意函数体第二行的指令字面量:

// turn-workflow.ts:31-42 —— 真实源码
/** Runs one complete logical turn, including child-agent waits when supported. */
export async function turnWorkflow(rawInput: unknown): Promise<void> {
"use workflow";

const input = migrateTurnWorkflowInput(rawInput);

if (input.driverCapabilities?.turnInbox !== true) {
return runLegacyTurnWorkflow(input);
}

return runTurnOwnedWorkflow(input);
}

"use workflow"(turn-workflow.ts:33)是给 Workflow DevKit 打包器看的标记:它告诉构建系统"这个函数体要编译成 durable workflow"。

这个 turn 是被谁启动的? 是 driver。dispatchTurnStep(workflow-steps.ts:641)在一个 "use step" 里调用 startWorkflowPreferLatest(turnWorkflowReference, …)——也就是为当前这一 turn 启动一个独立的子 workflow 运行:

// workflow-steps.ts:641-662 —— 真实源码(节选)
export async function dispatchTurnStep(
input: TurnWorkflowDispatchInput,
): Promise<{ readonly runId: string }> {
"use step";

const run = await startWorkflowPreferLatest(
turnWorkflowReference,
[createTurnWorkflowInput(input)],
{ /* attributes … */ },
);

return { runId: run.runId };
}

所以 session 不是一个大 workflow,而是 一个长活 driver workflow + 每 turn 一个子 workflow。这个分层很关键(见 §3.6 的部署模型)。

3.2 每个 step 是一个 durable step,在边界 checkpoint

它要解决的小问题: turn 里有很多次模型调用和工具调用,崩溃可能发生在任何一刻——续跑时怎么知道"哪些已经做完了"?

思路/直觉: 把每一步包成一个 durable step。step 一旦成功返回,它的返回值就被 Workflow 引擎记录(checkpoint)下来;之后无论崩多少次,这个 step 都不再重跑,引擎直接回放记录的结果

turnStep 就是这个 step,函数体首行是 "use step":

// workflow-steps.ts:105-113 —— 真实源码(节选)
/**
* Runs one atomic harness step inside a durable `"use step"` boundary.
*/
export async function turnStep(rawInput: TurnStepInput): Promise<DurableStepResult> {
"use step";

let input = rawInput;

let durableSession = await readDurableSession(input.sessionState);
// …
}

step 的"原子性"体现在它的返回值。 turnStep 跑完会返回一个 DurableStepResult,它的 action 字段只可能是几种:continue(还要再来一步)、done(turn 完成)、park(要挂起等输入)、dispatch-workflow-runtime-actions(派发运行时动作)。看 workflow-steps.ts:79-101 的类型定义——每种结果都同时带上 serializedContextsessionState:

// workflow-steps.ts:79-101 —— 真实源码(节选,类型)
export type DurableStepResult =
| {
readonly action: "continue" | "done";
// …
readonly serializedContext: Record<string, unknown>;
readonly sessionState: DurableSessionState;
}
| { readonly action: "park"; /* … */
readonly serializedContext: Record<string, unknown>;
readonly sessionState: DurableSessionState;
}
| { readonly action: "dispatch-workflow-runtime-actions"; /* … */ };

这就是 checkpoint 的本质: step 在边界上把"下一步要用的全部状态"——序列化后的运行时上下文 + session 快照——塞进自己的返回值。返回值被引擎持久化,于是状态也被持久化了。turnStep 的尾部(workflow-steps.ts:339-343)正是在做这件事:

// workflow-steps.ts:339-343 —— 真实源码
const rekeyed = reconcileSessionContinuationToken(ctx, stepResult.session);
const nextSerializedContext = serializeContext(ctx); // ← 序列化运行时上下文
stepResult = { ...stepResult, session: rekeyed };

const nextState = createDurableSessionState({ session: stepResult.session }); // ← 投影 session 快照

serializeContext(ctx) 把上下文序列化,createDurableSessionState({ session }) 把活的 HarnessSession 投影成可上线的快照。两者一起进 step 结果——Workflow step 的结果,就是 eve 持久化 session program memory 的原子边界durable-session-store.ts:1-9 文件头注释把这点说死了:

"Session-mutating steps return the current snapshot inside DurableSessionState; Workflow step results are the atomic persistence boundary for session program memory."

3.3 序列化:session 怎么被存下来、又拼回来

它要解决的小问题: 活的 HarnessSession 带着模型引用、工具实现、编译产物——这些没法序列化不该序列化(每次部署都会变)。那 step 边界到底存什么?

思路/直觉: 只存会变的数据,不存能重建的东西。 把 session 拆成两半:

  • 要持久化的:对话历史、authored state、sandbox 状态、上次 prompt 快照、compaction 计数……
  • 每 turn 重建的:模型引用、工具列表、compaction 阈值、system prompt——这些每个 turn 都从当前部署的 bundle.turnAgent 现配。

这套"投影 / 重建"由 execution/session.ts 里一对函数完成:

方向函数作用源码
存(活 → 快照)projectToDurableSession丢掉运行时字段,只留可持久化数据session.ts:146
取(快照 → 活)hydrateDurableSession用当前 turnAgent 重建运行时字段session.ts:205

durable-session-store.ts:61-72 的注释精确列出了"哪些被丢、为什么":

"Omits agent.modelReference, agent.tools, agent.compactionModelReference, and the compaction thresholds — those are rebuilt every turn from bundle.turnAgent by hydrateDurableSession."

为什么这么设计? 因为这正是"重新部署后,进行中的对话自动用上新 prompt/模型/工具"的实现基础(execution-model-and-durability.md:20):快照里只有历史和状态,模型与指令每个 turn 现配——所以你换了部署,下一个 turn 就用新配置,而对话历史原封不动。

序列化的保真度: 读写靠 devalue 编解码,所以 session 里的富类型(URL 形式的 FilePart.dataBufferDateMapSet)能结构化往返,而不只是 JSON 那点能力(durable-session-store.ts:124-126)。

DurableSessionState 和它内嵌的 DurableSessionSnapshot 都带一个 version 字段(durable-session-store.ts:52-5998-102)。这让被钉在旧部署的 driver 能搬运新 step 写出的形状:加可选字段是前向兼容的(devalue 保留未知 POJO 字段),破坏性改形状才 bump version + 加迁移器。

3.4 崩溃 / 重部署后:从上一个完成的 step 续跑,而非重放

它要解决的小问题: 进程崩了、超时了、或重新部署打断了一个跑到一半的 turn——怎么恢复?

思路/直觉: 这正是 §3.2 那套 checkpoint 机制的回报。Workflow 引擎记得每个完成的 step 的返回值;恢复时,它对已完成的 step 回放记录的结果(不重跑),只从第一个没完成的 step开始真正执行。

概念文档 execution-model-and-durability.md:42-43 说得最直白:

"Crash the process, hit a timeout, or redeploy mid-turn, and the run picks up from the last completed step rather than replaying the whole turn. Completed steps never re-run; eve replays the recorded result."

代码层面,这意味着:每次 turnStep 返回的 { action, serializedContext, sessionState } 就是恢复点;turnWorkflow 的循环(turn-workflow.ts:74)while (true) { const result = await turnStep(...) } 在恢复时,之前完成的那几次 turnStep 调用直接回放结果,循环从断点处继续。

一个必须自己负责的边界(重要的坑): 被中途打断的那个 step 会重跑。所以非幂等的副作用(扣款、发邮件)要么做成幂等,要么用审批门控住:

"A step interrupted mid-execution re-runs, so make non-idempotent side effects like charges or emails idempotent, or gate them with approval."(execution-model-and-durability.md:43)

这就是为什么 step 的粒度是"一次模型调用 + 它的工具调用":粒度太大,重跑会重复昂贵副作用;粒度太细,checkpoint 开销爆炸。eve 选在这个边界上 checkpoint,是工程上的甜点。

3.5 parked work:等待时挂起,零计算

它要解决的小问题: agent 经常要——等人点"批准"、等用户做完 OAuth、等一个跑很久的子 agent。这一等可能是几分钟到几天。难道要让一个进程一直占着内存空转?

思路/直觉: 不。workflow 挂起,持有零计算;输入到了再原地醒来。这是 durable execution 相对"自己存状态 + 轮询"的最大优势。

概念文档 execution-model-and-durability.md:50-51:

"At those points the turn parks durably. The workflow suspends and holds no compute until the input it's waiting on arrives (a click, a callback, a child completing), even if that's much later. When it does, the conversation picks up exactly where it left off."

代码里 park 怎么发生? turnStep 跑完 harness,发现"没有下一步,但 turn 还没 done"(即 stepResult.next === null),就返回一个 action: "park" 的结果(workflow-steps.ts:360-384):

// workflow-steps.ts:360-384 —— 真实源码(节选)
if (stepResult.next === null) {
writer.releaseLock();
// …(若是 workflow 运行时动作中断,走 dispatch 分支)…
return {
action: "park",
...derivePendingState(stepResult.session), // ← 带上"在等什么":auth/input/runtime-action
serializedContext: nextSerializedContext,
sessionState: nextState,
};
}

derivePendingState(workflow-steps.ts:398-418)算出这次 park 在等哪类输入:待授权(hasPendingAuthorization)、待输入批次(hasPendingInputBatch)、或待运行时动作键(pendingRuntimeActionKeys)。turnWorkflow 拿到 park 结果后,先校验"这种模式允不允许等"(task 模式不能等后续输入,turn-workflow.ts:122-128),然后调 cursor.finish(...) 把 park 上报给 driver 并 return——turn 子 workflow 结束,driver 那边挂起等待

子 agent / HITL 时 turn 反而不结束: 有一类等待(等子 agent 结果、等 dispatch 出去的 runtime action)是在 turn 内部原地等的——runTurnOwnedWorkflowwaitForRuntimeActionResults(turn-workflow.ts:153)在一个私有 inbox 上 await iterator.next(),turn 不退出、保持活着跨越这次等待(turn-workflow.ts:74-120)。两种等待的区别:park 让整个 turn 退场交回 driver;in-line wait 让 turn 自己挂在 inbox 上。无论哪种,底层都是 workflow 挂起、零计算。

3.6 continuationToken:resume handle 与单 owner 语义

它要解决的小问题: 一段 session 挂起后,下一条消息怎么找到它、并把它叫醒?多个并发请求同时想叫醒同一段 session,会乱套吗?

思路/直觉: 用一个 continuationToken(续接令牌) 作为 session 当前那个 workflow hook 的 resume handle(恢复句柄)。它不是消息队列地址,而是"叫醒当前这个挂起点"的钥匙。

概念文档 execution-model-and-durability.md:54-55 划清了这条线:

"eve does not maintain a durable FIFO queue of user messages for a session. The continuationToken is a resume handle for the session's current workflow hook, not a general message-queue address."

单 owner 语义(关键安全/正确性约束): 一个 continuation token 只能被一个活跃 session 拥有。 一个带 token 启动的 session,会先提交 park hook,再处理它的第一个 turn;如果另一个 run 已经拥有那个 token,竞争者会被判失败(execution-model-and-durability.md:56-57):

"Only one active session can own a continuation token. … eve commits the park hook before processing its first turn and fails a competing session if another run already owns that token."

代码侧,投递就是去 resumeHook(continuationToken, payload)(workflow-runtime.ts:159-180deliver):

// workflow-runtime.ts:159-180 —— 真实源码(节选)
async deliver(input: DeliverInput): Promise<{ sessionId: string }> {
const hookPayload = { auth: input.auth, kind: "deliver", payloads: [input.payload], requestId: input.requestId };
try {
const hook = normalizeWorkflowHook(await resumeHook(input.continuationToken, hookPayload));
return { sessionId: hook.runId };
} catch (error) {
if (HookNotFoundError.is(error)) { // ← "没有这个 hook" = 没有活跃 session 在等
throw new RuntimeNoActiveSessionError(input.continuationToken);
}
throw error;
}
}

HookNotFoundError 被规整成 eve 自己的 RuntimeNoActiveSessionError——这是"resume-or-start"的预期信号:没人在这个 token 上等,就该走"新开 session"而非"叫醒"。

driver 侧怎么用 token 决定醒来后干什么? workflowEntry 的 driver loop(workflow-entry.ts:188-282)在 park 后,先 deliveryHook.rekey(action.sessionState.continuationToken)(workflow-entry.ts:214)把投递 hook 重新绑定到这次 park 的 token,再等下一条 deliver。token 的两个角色——resume handle(channel 持有)sessionId/runId(runtime 持有的 stream-and-inspect 句柄)——在 sessions-runs-and-streaming.md:11-16 被刻意分开,混用是最常见的错误。

3.7 durable-session-store:状态住在哪儿

它要解决的小问题: §3.2-3.3 说 step 把快照塞进返回值——但具体"读"和"写"长什么样?有没有旧 session 的兼容路径?

新路径:状态直接随 step 结果走。 createDurableSessionState(durable-session-store.ts:189-201)把活 session 投影成快照,内嵌进 DurableSessionState.snapshot;这个 state 随 step 结果被引擎持久化。读时 readDurableSession(durable-session-store.ts:132-135)看见 state.snapshot 直接就地解出来,不需要额外的 step 边界:

// durable-session-store.ts:132-135 —— 真实源码(节选)
export async function readDurableSession(state: DurableSessionState): Promise<DurableSession> {
if (state.snapshot !== undefined) {
return migrateDurableSessionSnapshot(state.snapshot).session;
}
// …否则走 legacy 流尾部回退…
}

旧路径(legacy fallback): 老的在途 session 可能只带一个小 state 句柄、没有内嵌 snapshot。这时 readDurableSession 回退去读 legacy 的 "eve.session" 流尾部(startIndex: -1,durable-session-store.ts:137-174),还带一个 10 秒超时(DURABLE_SESSION_READ_TIMEOUT_MS,durable-session-store.ts:34)兜底。这是为了让"改造前就在跑"的 session 不被打断。

DurableSessionState 本身(durable-session-store.ts:52-59)除了 snapshot,还携带几个不取步骤边界就能用的小投影:continuationTokenhasProxyInputRequests(一个闭合契约的短路:没有活跃后代子 agent 时,driver 可跳过每次投递的代理路由 step)、emissionState(让 workflow-body 的框架 step 给协议事件盖 { turnId, sequence, stepIndex } 而不必读全 session)。

3.8 本地 world / Vercel Workflow / 自托管 world:同一份 workflow 代码,三个底座

它要解决的小问题: "每个 turn 是一个 Workflow"——但这个 Workflow 引擎到底跑在哪?本地 dev 和生产 Vercel 一样吗?能不上 Vercel 吗?

思路/直觉: Workflow SDK 不绑死 Vercel"use workflow" / "use step" 写出来的同一份代码,跑在一个可替换的 world(世界) 实现上。三种底座:

底座用在哪状态存哪 / 特性
本地 world(SDK 默认)本地开发、自部署的 eve start把 workflow 运行持久化到磁盘,通常在 .workflow-data 下;经同一套 Nitro 托管的 workflow 路由派发
Vercel Workflow部署到 Vercel同一份 workflow 代码跑在 Vercel Workflow 上,额外加平台特性:latest 生产部署路由、dashboard 运行元数据
自托管 world 包高级自托管agent.ts 里用 experimental.workflow.world 选一个安装好的 world 包(如 @workflow/world-postgres)来背 workflow 状态/队列/hook/流

概念文档 execution-model-and-durability.md:18 把前两者讲清楚:

"In local development and in a self-deployed eve start process, eve uses the SDK's local world by default; that world persists workflow runs on disk, normally under .workflow-data … On Vercel, the same workflow code runs against Vercel Workflow instead, which adds platform features such as latest production deployment routing and dashboard run metadata."

自托管 world 怎么选: 在根 agent.ts 里声明(execution-model-and-durability.md:24-39):

// agent/agent.ts —— 真实文档示例
import { defineAgent } from "eve";

export default defineAgent({
model: "anthropic/claude-opus-4.8",
experimental: {
workflow: {
world: "@workflow/world-postgres",
},
},
});

一个会咬人的版本约束: 选的 world 包必须匹配 eve 内置的 @workflow/* 版本线(当前是 5.0.0-beta 线);要显式 pin,否则版本不匹配会在 run replay 时ZodError: invalid_union(execution-model-and-durability.md:39)。

"latest 部署路由"是什么、为什么只在 Vercel 生产生效? §3.1 提过 driver 被钉在调用 start() 的那个部署,而子 turn workflow 跑在 latest。这套"latest 路由"靠 startWorkflowPreferLatest(workflow-runtime.ts:197-219)实现,它只在 VERCEL_ENV === "production" 时启用(shouldRouteToLatestDeployment,workflow-runtime.ts:229-231):

// workflow-runtime.ts:229-231 —— 真实源码
function shouldRouteToLatestDeployment(): boolean {
return process.env.VERCEL_ENV === "production";
}

预览 / CLI 部署没有 git 分支引用,无法解析 "latest",于是把 workflow 运行钉在自己那个不可变部署上——这对预览来说恰好也是正确的隔离语义(workflow-runtime.ts:221-235 注释)。本地 world 不实现 latest 路由,startWorkflowPreferLatest 在它上面会优雅回退到普通 start()(workflow-runtime.ts:208-218)。

这就是为什么 §3.1 强调 workflowEntryturnWorkflow 的 workflow id 不带包版本戳(STABLE_WORKFLOW_NAMES,workflow-runtime.ts:54-86):稳定 id 才能让"被钉在旧部署的 driver"用 deploymentId: "latest" 找到新部署上的同名 turn workflow,即使 eve 版本变了。


4. 一个 turn 的完整生命周期(把上面拼起来)

把 §3 的机制按时间顺序串一遍。这是 driver 与一个 turn 子 workflow 之间的一来一回:

driver(workflowEntry) turn 子 workflow(turnWorkflow)
───────────────────── ──────────────────────────────
dispatchAndAwaitTurn
│ dispatchTurnStep ───────────────► 启动子 workflow,"use workflow"
│ │
│ ▼ while(true):
│ turnStep("use step") ──┐
│ │ 读快照 → 跑模型/工具 │ 每步:
│ │ → 序列化上下文+快照 │ checkpoint
│ ▼ │
│ action=continue? ── 是 ──► 再来一步 ┘
│ │否
│ ┌────────┼─────────┐
│ done park dispatch-runtime-actions
│ │ │ │(in-line 等结果,
│ ◄── turn-result ─────────┘ │ turn 不退场)
│ ◄── turn-result(park) ────────────┘

park:rekey 到 continuationToken,挂起等下一条 deliver(零计算)

┊ 下一条用户消息到达 → resumeHook 叫醒

dispatchAndAwaitTurn(下一 turn)…

几个落地引用,方便你跳进源码核对:

  • driver 派发 + 等结果:dispatchAndAwaitTurn(execution/turn-dispatch.ts),内部 dispatchTurnStep + control.waitForAction()
  • turn 循环:runTurnOwnedWorkflowwhile (true) { await turnStep(...) }(turn-workflow.ts:74-143)。
  • turn 把状态游标交给 TurnExecutionCursor(turn-execution-cursor.ts:24):adopt 吸纳每步的状态转移并按需上报 continuation-token 变化(turn-execution-cursor.ts:56-64),finish 把终态(done/park)作为 turn 结果发出(turn-execution-cursor.ts:81-96)。
  • turn → driver 的控制通道:sendTurnControlStep(turn-control-protocol.ts:33-40),载荷类型见 TurnControlPayload(turn-control-protocol.ts:15-30)。
  • driver 看见 park → rekey → 等 deliver:workflow-entry.ts:188-282

5. 巧妙之处(可借鉴的技术)

  • 概念三层 ↔ 两条 workflow 指令的干净映射。 session=driver workflow,turn=子 workflow,step=durable step,只靠 "use workflow" / "use step" 两个字面量(turn-workflow.ts:33workflow-steps.ts:109)。读者只要记住这两个词,就能在源码里定位"持久化边界在哪"。

  • step 结果就是持久化边界。 不另设存储层:把"序列化上下文 + session 快照"塞进 step 的返回值,让 Workflow 引擎顺带把状态也 checkpoint 了(durable-session-store.ts:1-9 注释 + workflow-steps.ts:339-343)。少一层,少一处不一致。

  • 只存会变的、重建能算的。 projectToDurableSession / hydrateDurableSession(session.ts:146205)把模型/工具/阈值每 turn 现配——既缩小了快照,又免费得到了"重部署后进行中对话自动用新配置"的能力(execution-model-and-durability.md:20)。

  • park = 零计算等待。 不是占内存轮询,而是让 workflow 挂起;derivePendingState(workflow-steps.ts:398-418)把"在等什么"精确编码进 park 结果,driver 据此决定醒来后怎么接(workflow-entry.ts:216-243)。

  • 稳定 workflow id 支撑跨部署路由。 id 去掉版本戳(STABLE_WORKFLOW_NAMES,workflow-runtime.ts:54-57),让被钉在旧部署的长活 driver 能把每个 turn 路由到 latest 部署(startWorkflowPreferLatest,workflow-runtime.ts:197-219)。这是"长活会话 + 持续部署"能共存的关键。

  • 单 owner + commit-park-before-first-turn。 用 continuation token 的独占所有权(execution-model-and-durability.md:56-57)挡掉并发 session 抢同一个 hook,把"谁能叫醒这段对话"做成确定的。


6. 边界与局限(诚实)

  • 不是消息队列。 eve 维护 session 的持久 FIFO 用户消息队列;continuationToken 是 resume handle,不是队列地址(execution-model-and-durability.md:54-55)。并发往同一 session 猛发消息,不会像有序聊天队列那样表现:turn 活跃时 hook 可能接收额外 deliver,但运行时只在特定 workflow 边界 drain,多条就绪时是 best-effort 折叠进下一 turn(execution-model-and-durability.md:58-61)。要确定性,就一次发一个 turn、等 session.waiting 再发下一条(sessions-runs-and-streaming.md:89)。

  • 被打断的 step 会重跑。 完成的 step 不重跑,但中途被打断的那个 step 会重跑——非幂等副作用(扣款、发邮件)必须自己做幂等或用审批门控(execution-model-and-durability.md:43)。这是 durable execution 的标准代价,eve 没有替你消除。

  • task 模式不能等后续输入。 会话模式可以 park 等下一条消息,但 task 模式 park 等输入会直接抛错(TASK_MODE_WAIT_ERROR_MESSAGE,turn-workflow.ts:27128)。

  • 自托管 world 的版本耦合。 选的 world 包必须 pin 到 eve 内置的 @workflow/* 版本线,否则 run replay 时才暴雷(ZodError: invalid_union)——一个迟到、难定位的失败(execution-model-and-durability.md:39)。

  • 你不直接写 workflow 代码。 start() / resumeHook() 等是 eve 运行时层的实现细节,channels/tools/hooks 碰不到(execution-model-and-durability.md:46-47)。好处是简单,代价是这层不可定制——你只能通过 ctx.sessiondefineState 间接接触 session 数据。


7. 横向对比

同 shelf 的 agent 框架里,持久化执行各有取舍:

  • eve(本章) 把持久化下沉到 Workflow SDK / Vercel Workflow,每 turn 一个子 workflow + 每 step 一个 durable checkpoint,park 时零计算。开发者完全不碰 workflow 原语。
  • 许多 agent 框架(如经典的 tool-loop runner)把"持久化"留给应用层:自己存历史、自己重建状态、等待时要么占进程要么自己实现轮询。eve 的差异在于把"崩溃续跑 / 零计算等待 / 重部署自动换配置"做成了运行时默认,而不是留给使用者拼。

本章只讲执行底座;turn 内部的 agent 循环、内置工具与 compaction 见 默认 harness,前门(channels/connections)与 continuation token 的归属见 channels、connections 与安全模型


8. 代码地图(导航索引)

主题文件路径符号名
session 级 driver(长活 workflow)packages/eve/src/execution/workflow-entry.tsworkflowEntry / runDriverLoop
每 turn 的子 workflowpackages/eve/src/execution/turn-workflow.tsturnWorkflow / runTurnOwnedWorkflow
in-line 等子 agent / 运行时动作结果packages/eve/src/execution/turn-workflow.tswaitForRuntimeActionResults
一个原子 durable steppackages/eve/src/execution/workflow-steps.tsturnStep
step 结果类型(continue/done/park/dispatch)packages/eve/src/execution/workflow-steps.tsDurableStepResult
算出"park 在等什么"packages/eve/src/execution/workflow-steps.tsderivePendingState
派发一个 turn 子 workflowpackages/eve/src/execution/workflow-steps.tsdispatchTurnStep
启动 workflow(优先 latest 部署)packages/eve/src/execution/workflow-runtime.tsstartWorkflowPreferLatest
latest 路由开关(仅 Vercel 生产)packages/eve/src/execution/workflow-runtime.tsshouldRouteToLatestDeployment
稳定(无版本戳)workflow idpackages/eve/src/execution/workflow-runtime.tsSTABLE_WORKFLOW_NAMES / turnWorkflowReference
投递叫醒 session(resumeHook)packages/eve/src/execution/workflow-runtime.tscreateWorkflowRuntime (deliver)
读 durable session 快照packages/eve/src/execution/durable-session-store.tsreadDurableSession
写 durable session 快照packages/eve/src/execution/durable-session-store.tscreateDurableSessionState
state 形状 + 版本packages/eve/src/execution/durable-session-store.tsDurableSessionState / DURABLE_SESSION_VERSION
活 session → 快照投影packages/eve/src/execution/session.tsprojectToDurableSession
快照 → 活 session 重建packages/eve/src/execution/session.tshydrateDurableSession
一个 turn 的可变状态游标packages/eve/src/execution/turn-execution-cursor.tsTurnExecutionCursor
turn → driver 控制通道packages/eve/src/execution/turn-control-protocol.tssendTurnControlStep / TurnControlPayload
driver 派发并等 turn 结果packages/eve/src/execution/turn-dispatch.tsdispatchAndAwaitTurn
持久化执行 / 续跑 / park 概念docs/concepts/execution-model-and-durability.md
continuationToken vs sessionId 契约docs/concepts/sessions-runs-and-streaming.md"The two handles"