Agent Runtime 中 Command、Event、Replay、Resume 与状态层的关系
把 L3 对话中形成的 Command/Event/Effect、Replay/Resume、ActiveTurn/Event Log/Checkpoint/Read Model、Thread/Run/Interaction/Config 等关系收敛成一套可恢复 Agent Runtime 心智模型。
[!info] related notes
- 所属 MOC: agent-runtime-moc、bodysense-moc
- 相关概念:
- 易混淆概念: agent-replay-modes
- 相关资源:
Agent Runtime 中 Command、Event、Replay、Resume 与状态层的关系
覆盖范围
这篇笔记不重新解释 SSE、Reducer、Checkpoint 等单个概念,而是解决 L3 学习中最容易混淆的一组关系:
Command、Event、Effect分别是什么;Replay为什么不能重新执行 Command;Resume为什么不是 Replay;ActiveTurnState、Runtime Event Log、Checkpoint、Read Model分别拥有哪类状态;Interaction ID、Semantic Thread ID、Public Run ID、Configuration ID为什么不是同一个 identity;interrupted、completed、failed、cancelled的 authority 与终止语义。
这组关系来自 BodySense Consultation L3 的实际学习场景,但可以复用于其他 production Agent Runtime。
一张总图:从用户意图到可重放 UI
User Intent
│
↓
Command
SubmitAnswer / CancelRun
│
↓
┌────────────────────── Server Runtime ──────────────────────┐
│ │
│ Semantic Thread / Agent Execution │
│ │ │
│ ├──── Checkpoint │
│ │ “未完成执行从哪里继续?” │
│ │ │
│ ├──── Commands / Tools / real side effects │
│ │ │
│ └──── Durable Runtime Events │
│ “已经发生了什么?” │
│ │
│ Durable Interaction / Messages / Read Model │
│ “现在稳定展示什么?” │
└───────────────────────────┬────────────────────────────────┘
│
┌──────────┴──────────┐
↓ ↓
Live SSE after_seq Replay
└──────────┬──────────┘
↓
Network bytes
↓
unknown
↓
Runtime Validation
↓
Trusted StreamEvent
↓
Pure Reducer
↓
ActiveTurnState
“当前正在发生什么?”
↓
UI
完成后:
ActiveTurn ── reconcile ──> Durable Read Model
└─> reset ActiveTurn
这张图是 L3 最重要的总心智模型:实时 transport 可以断,执行 identity 和 durable facts 不能跟着消失;Live 与 Replay 最终都必须收敛到同一个 projection。
Command、Event、Effect:不要把“请求改变现实”和“现实已经改变”混在一起
Command
Command 表达:
我希望系统改变现实。
例如:
SubmitInteractionAnswer(I7, "Yes")
CancelRun(R7)
SendNotification(N3)
Command 需要经过服务端当前状态、权限、幂等和时效性验证。
Event
Event 表达:
某个变化已经成为事实。
例如:
interaction.answered(I7)
run.resumed
run.cancelled
notification.sent(N3)
Effect
前端 Reducer 可以在纯状态转换之外声明客户端协调型 Effect,例如:
reconcileConversation
invalidate durable query
upsert query cache
但不要让 Replayable public Event 重新触发现实世界 Side Effect,例如:
send email
execute tool
resume checkpoint
cancel server run
完整关系
User Intent
↓
Command
↓
Server State Machine
↓
Real Side Effect / Durable Transition
↓
Event
↓
Reducer
↓
Projection
↓
Client Coordination Effect
核心记忆:
Command 请求创造事实;Event 描述事实已经成立;Reducer/Effect 负责根据事实让客户端收敛。
Replay vs Resume:重建过去,不重新创造过去
这两个词很像,但语义完全不同。
Replay
Replay 的目标是:
Event Log
↓
validation
↓
same reducer
↓
reconstruct projection
它回答:
历史上发生过什么,现在 UI 应恢复成什么样?
例如历史已有:
tool.call T7
tool.result T7 success
notification.sent N3
interaction.answered I7
run.resumed
Replay 只能把这些事实重新投影出来,不能再次调用工具、再次发送通知、再次提交答案或再次 Resume。
Resume
Resume 的目标是:
unfinished Semantic Thread
+
Checkpoint
+
validated Resume Input
↓
continue real execution
它回答:
这个尚未结束的 Agent execution 应该从哪里继续?
所以:
Replay = reconstruct history/projection
Resume = continue unfinished computation
核心记忆:
Replay 重建过去,不重新创造过去。
四种 State Owner:同样都“保存状态”,回答的问题却不同
┌────────────────────────────────────────────┐
│ ActiveTurnState │
│ 当前 Run 的 transient/live projection │
│ │
│ 回答:当前正在发生什么? │
└────────────────────────────────────────────┘
┌────────────────────────────────────────────┐
│ Runtime Event Log │
│ append-only durable runtime facts │
│ │
│ 回答:执行过程中发生过什么? │
└────────────────────────────────────────────┘
┌────────────────────────────────────────────┐
│ Checkpoint │
│ framework/runtime internal continuation │
│ │
│ 回答:未完成 execution 从哪里继续? │
└────────────────────────────────────────────┘
┌────────────────────────────────────────────┐
│ Durable Read Model │
│ finalized messages / thread / workspace │
│ │
│ 回答:现在稳定页面应该展示什么? │
└────────────────────────────────────────────┘
因此:
ActiveTurn ≠ Event Log ≠ Checkpoint ≠ Read Model
普通刷新优先读取 Read Model;若存在 active/interrupted Run,再通过 Event Log catch-up 恢复当前 Runtime Projection。真正 Resume Agent 则依赖 Checkpoint,而不是让前端解析 Event Log 猜执行内部状态。
Event Log append-only,不代表 Projection 也 append-only
历史可以保存:
seq=3 tool.call T7 running
seq=8 tool.result T7 completed
两条 Event 都应该保留,因为它们描述历史。
但 ActiveTurn 中:
toolCallsById[T7] = completed
应该只有一个当前实体。
因此:
History / Event Log
→ append
Current Projection
→ reduce / keyed upsert / converge
Text delta 常常是 append;Tool、Citation、Interaction 等更常使用 stable identity 做 upsert 或小状态机。
Identity Stack:每个 ID 回答不同的问题
一个 Consultation 可能同时存在:
Conversation C1
↓
Semantic / Checkpoint Thread T7
├── Public Run R10
│ ↓
│ Interaction I7
│ ↓
│ interrupted
│
└── Public Run R11
↑ resumes R10
↓
completed
Pinned Agent Configuration = C15
Interaction ID
I7
回答:
用户这次答案是在满足哪个等待点?
Tool Call ID
ToolCall T3
回答:
这次 Agent Tool Invocation 是谁?
如果 ask_user 起源于 Tool Call,它可以关联 Interaction,但二者生命周期不同,因此不应默认共用 identity。
Semantic / Checkpoint Thread ID
Thread T7
回答:
哪一条 Agent execution history / checkpoint chain 被继续?
Public Run ID
R10 / R11
回答:
API / Event Log 对外如何标识本次 execution segment?
Resume 可以保持同一个 Public Run,也可以创建明确关联的 resumed segment;关键不是选哪一种,而是语义必须稳定并可审计。
Configuration ID
C15
回答:
这条 execution 当初由哪套行为配置定义?
同一 Semantic Thread Resume 时默认继续 pinned C15,而不是因为 production pointer 已经升级到 C16 就偷偷换配置。
Resume:先判断还能不能继续,再保持原执行身份
假设:
09:00 Thread T7 starts with Config C15
09:02 interaction.required → interrupted
10:00 production pointer C15 → C16
11:00 user answers
Resume 有两个独立问题:
Current Business Eligibility
→ 旧 execution 现在还允许继续吗?
Execution Identity
→ 如果继续,应使用哪套 config?
正确关系:
仍允许继续
→ resume T7 with pinned C15
不再允许继续
→ explicit cancel/supersede
→ start new analysis if needed
不要用“重新授权当前世界”作为借口偷偷切换到 C16。
answered ≠ resumed:Durable Workflow 必须允许中间态修复
真实故障:
I7 pending
↓
user submits answer
↓
answer persisted successfully
↓
process crashes before resume
此时:
Interaction I7 = answered ✅
Semantic Thread T7 = still interrupted ❗
因此:
interaction.answered
≠
run.resumed
系统重启后应识别:
answered but unconsumed
然后在确认仍允许 Resume 后:
resume T7 exactly once
而不是重新问用户或重复保存 answer。
这就是 durable workflow 的 repair semantics。
Double Submit:幂等不只是 UI 去重
用户可能因 HTTP response 丢失重复提交:
SubmitAnswer(I7, Yes)
SubmitAnswer(I7, Yes)
目标不是仅仅 disable button,而是服务端保证:
one durable answer
+
at most one semantic resume
如果第二次答案不同:
Yes → No
必须有明确 conflict/revision policy,不能 accidental last-write-wins,否则 execution 实际消费的答案和数据库最终显示的答案可能互相矛盾。
Interaction Answer 不是自动的 Domain Fact
Interaction Answer
= 用户确实这样回答过
但它只是 domain input/observation candidate。
例如:
“是,我右肩会痛”
还可能经过:
normalization
source/provenance
timestamp
conflict handling
domain validation
才能进入 BodyState 的 accepted Observation/Fact。
所以:
Interaction Answer
↓
Domain Input / Observation
↓
BodyState Policy
↓
Accepted Fact
不要把 UI answer 自动升级成最终领域真值。
Transport Lifetime 与 Run Lifetime
SSE disconnected
≠ run.failed
AbortController.abort()
≠ run.cancelled
浏览器断线或主动停止 reader,只能证明 transport 状态发生变化。
真正取消 Run 应是正式 Command:
CancelRun(R7)
↓
server state / authority validation
↓
cancellation propagation where possible
↓
run.cancelled
其中 run.cancelled 是权威 terminal fact。
Terminal State:唯一且不可逆
典型 terminal:
completed
failed
cancelled
而:
interrupted
不是 terminal,因为它仍可 Resume。
一旦 Durable Event Log 已经确认:
seq=20 run.completed
之后到达的 Cancel 应被视为 too late / already terminal,而不是:
completed → cancelled
同理,terminal 后迟到的 text/tool event 也不能重新打开 Run。
一条 BodySense Consultation 完整路径
User starts consultation
↓
Run / Thread starts
↓
message.text.delta
↓
tool.call T1
↓
--- network lost ---
↓
server persists tool.result T1
server persists interaction.required I7
↓
after_seq catch-up
↓
same validator + same reducer
↓
ActiveTurn:
T1 = completed
status = interrupted
pendingInteraction = I7
↓
user submits Answer(I7)
↓
interaction.answered durable
↓
resume eligible?
↓ yes
resume same Semantic Thread / pinned config
↓
run.resumed
↓
streaming
↓
run.completed / failed / cancelled
↓
reconcile durable Read Model
↓
reset ActiveTurn
这条路径把 L3 中原本容易分散理解的 Trust Boundary、Reducer、Recovery、HITL、Durability、Identity 与 Cancellation 放进同一个运行时闭环。
源码审查时的快速问题
看到一段 Runtime 代码时,可以按顺序问:
- 这里接收的是
unknown、Command 还是已验证 Event? - 这段代码是在“请求改变现实”,还是“记录/投影已经发生的事实”?
- 这个 Event 如果 Replay,会不会错误地重新执行现实副作用?
- 当前状态属于 ActiveTurn、Event Log、Checkpoint 还是 Read Model?
- Resume 的 identity 是 Interaction、Thread、Public Run 还是 Config?
answered、resumed、completed是否被错误合并成一个状态?- retry / double submit 是否能收敛为一次语义执行?
- transport failure 是否被错误升级成 business/runtime terminal fact?
- terminal state 是否唯一且不可逆?
- Live 与 Replay 是否最终进入同一 validator / reducer 语义?
最终记忆
Command
= 请求现实改变
Event
= 现实已经改变
Replay
= 重建历史 projection
Resume
= 继续未完成 execution
ActiveTurn
= 当前正在发生什么
Event Log
= 发生过什么
Checkpoint
= 从哪里继续
Read Model
= 现在稳定展示什么
如果这八句话能稳定区分,L3 大部分“看起来相似”的 runtime 概念就不会再混在一起。