Appearance
Runtime 事实怎么落表:Run、Node Execution 与 Attempt Identity
前置:数据库并发边界:Transaction、Isolation、Optimistic Concurrency 与 Pool
核心问题:为了 crash/retry/resume,数据库至少要保存哪些 Run、Node、Attempt 事实,execution identity 应该落在哪里?
为什么一个 runs 表不够
UI 希望快速回答:
text
这个 Run 现在 RUNNING / WAITING / FAILED / SUCCEEDED?
下一步是什么?
当前 checkpoint/version 是多少?故障分析则要回答:
text
node-7 执行过几次?
attempt-2 为什么 timeout?
attempt-3 是哪个 worker 执行?
Tool side effect 的 operation_id 是什么?两个问题粒度不同。把全部 attempt history 塞进 runs.json,每次状态变化都重写巨大对象,并发控制也难以表达。
Run Table:保存当前 Materialized State
最小概念字段可以是:
text
runs
- run_id PK
- tenant_id
- status
- state_version
- plan_version
- current_checkpoint_id
- deadline_at
- cancel_requested_at
- created_at / updated_at
- terminal_error_codeRun row 是“当前事实的快速视图”,适合 API 查询和 optimistic concurrency:
sql
UPDATE runs
SET status = ?, state_version = state_version + 1
WHERE run_id = ? AND state_version = ?;更新 0 行意味着调用方拿的是 stale state,而不是应该无条件覆盖。
Node 与 Execution Attempt 要分清
逻辑 Node:
text
node_id = search-code
state = SUCCEEDED但它可能经历:
text
attempt 1 → TIMEOUT
attempt 2 → RATE_LIMITED
attempt 3 → SUCCEEDED因此可使用:
text
node_executions
- run_id
- node_id
- attempt_no
- operation_id
- worker_id / lease_epoch
- status
- started_at / finished_at
- input_ref / output_ref
- normalized_error
- trace_id
UNIQUE(run_id, node_id, attempt_no)
UNIQUE(operation_id)不要覆盖失败 attempt;它们是 retry、cost、incident analysis 的真实证据。
attempt_no 与 operation_id 不是一回事
Retry attempt 可以增加,但某些外部副作用的 logical operation identity 必须稳定。例如远端 API 的 idempotency key 可能跨 retry 共用同一个 operation_id。
text
logical operation
operation_id = op-88
attempt 1 → network timeout
attempt 2 → reconciliation / retry如果每次 attempt 都生成新的业务 operation id,下游无法判断这是一次动作的重试还是两次新动作。
大 Payload 不一定直接塞数据库行
Model raw response、大 Tool output、Artifact 可以进入 object/artifact store,表里保存:
text
content_ref
hash
type
size
schema_version数据库保留 identity、状态和索引友好的事实。否则大 JSON 频繁更新会放大锁、I/O 和备份成本。
与 Checkpoint / Event Log 的关系
text
Run table
→ current materialized view
Node execution table
→ per-node/per-attempt execution facts
Checkpoint
→ 可恢复 snapshot/boundary
Event log
→ append-only history / reconstruction evidence一个系统不一定四种都需要完整实现,但概念不能混成“一张 history JSON”。Shadow Harness 可以先简化存储,接口仍保持这些责任边界。
Failure Modes
Retry 覆盖上一条 Node 记录
后果: 看不到 attempt history,也无法解释成本和失败链。
处理: logical Node 与 attempt record 分开;attempt append,Node materialized status 更新。
API 直接写 status='SUCCEEDED'
后果: 绕过 Runtime invariant 和 execution evidence。
处理: 状态迁移通过 Runtime command/reducer;DB 是 durable representation,不发明第二套状态机。
如何验证
| 场景 | 必须成立 |
|---|---|
| Node timeout 两次后成功 | 3 条 attempt 可审计,Node 最终 SUCCEEDED |
| 两个 writer 用同一旧 state_version 更新 Run | 只有一个成功 |
| 大 Tool Result 10MB | 主状态表不需要每次复制完整 payload |
自测
- 为什么只存
run.status无法回答“某个 Node 第几次 Attempt 做过什么”?Runtime tables 还需要哪些 identity? - 同一 Node 重试三次时,Node Execution 和 Attempt 应怎样分层,才能保留历史又得到当前状态?