状态层应用案例拆解
状态层最显眼的用法是“唤醒 Agent”,但它实际承担三类职责: 记录业务事实、驱动业务流转、提供取证与恢复。 唤醒只是其中一种用法,不是全部。
0. 先看全景
业务动作
→ 写一条事实(state_events)
→ 有人关心?命中订阅
→ 跑工作流 / 叫 Agent / 写回结果
→ 没人关心?只留痕
→ 需要追查?traceCausation / readVersion / verify三个关键词:
| 关键词 | 含义 | 典型用途 |
|---|---|---|
| 记录 | 把发生过的事写成不可改的事件 | 审计、恢复、验收依据 |
| 驱动 | 订阅命中后执行下一步 | 唤醒 Agent、跑验收/结算、告警 |
| 取证 | 从事件重放出“当时是什么” | 争议定责、版本核对、对账 |
1. 已经接入的真实业务逻辑
| 业务逻辑 | 状态层里的事实 | 谁读 / 怎么用 | 现状 |
|---|---|---|---|
| 任务执行留痕 | /tasks/{id}/batches/{batchId}、/steps/{stepId}、/steps/{stepId}/decision | TaskAgentLoopService.project() 重建轨迹;completedStepIds() 重放已完成步骤 | ✅ 默认开启(TASK_EXECUTION_MODE=loop) |
| 断点恢复 | 同上 | 重新执行时跳过已完成 step;中断时 reconcileInterrupted() 补终态 | ✅ 已接 |
| 交付与验收 | /deliveries/{id}、/deliveries/{id}/acceptance | 外部 Agent 回传写交付;验收可订阅触发 | ✅ 已接 L1 |
| 争议与处置 | /disputes/{id}、/disputes/{id}/resolution | 履约链路写入;按条件订阅升级/通知 | ✅ 已接 L1 |
| 托管与结算 | /tasks/{id}/escrow | EscrowStateService 强制写入;结算指令也在同一路径 | ✅ 已接 L2,资金体系未接 |
| 额度扣减 | /tasks/{id}/commits/{a2aTaskId} | API key 委托调用时写入 | ✅ 已接 |
| 决策留痕 | /steps/{stepId}/decision:选了谁、为什么、候选列表 | recordDecision() | ✅ 已接 |
| 门面需求登记 | /tasks/{id}/requests/{requestId} | submit_request 写入 | ✅ 已接 |
| 订阅驱动 Agent | 命中后写 /agents/{id}/results/{eventId} | execute_agent | ✅ 已接 |
| 订阅驱动工作流 | 命中后跑 workflow | execute_workflow | ✅ 已接 |
最后两行很重要:触发器不只用于 Agent。 execute_workflow 可以跑验收、结算、通知等工作流,不一定叫 Agent。
2. 案例拆解
案例 1:任务执行留痕与断点恢复
业务问题
一次任务可能有很多 step。进程被 kill、节点失败、用户重试时,怎么知道哪些已经做完?
写入了什么
/tasks/{taskId}/batches/{batchId} 这一批开始跑
/tasks/{taskId}/steps/{stepId} 每个 step 的 running/completed/failed
/tasks/{taskId}/steps/{stepId}/decision 选了哪个 Agent、为什么谁读、怎么用
project(taskId)把事件重放成轨迹视图;completedStepIds(taskId)返回已经完成的 step;- 重新执行时跳过这些 step;
reconcileInterrupted()给没有终态的 step 补failed/interrupted,避免轨迹一直转圈。
关键点
这里没有订阅。状态只用于“记录 + 读取”,不需要叫醒任何人。 这是最容易被忽略的用法:不是所有状态写入都要触发动作。
案例 2:交付 → 验收
业务问题
外部 Agent 交付了报告,谁来验收?验收依据从哪里来?
链路
① 外部 Agent 回传交付
/tasks/T/deliveries/D = { deliverables: [...] }
source=agent_callback, actor=agent:A, depth=0
│
│ 订阅 S1:pattern /tasks/*/deliveries/*
│ condition op=set
▼
② 验收工作流
节点 stateReads 读取 /tasks/T/deliveries/D
写回 /tasks/T/deliveries/D/acceptance
= { accepted: true, basedOn: <①的交付内容> }
source=workflow_node, depth=1, causedBy=①为什么这样设计
验收结论里真的带上交付内容,而不是一个空的 accepted: true。 这样以后争议时,能回答“当时验收的是哪一版交付”。
证据
state-layer.e2e-spec.ts 的 mock 业务闭环: 交付 → 验收节点读到交付内容 → 结算 → 停。 断言里明确检查验收值包含交付内容;如果只是字面量,这条断言会失败。
案例 3:验收 → 结算
业务问题
验收通过后,怎么自动进入结算,而不是靠人点按钮?
链路
/tasks/T/deliveries/D/acceptance = { accepted: true }
→ 订阅 S2:condition $event.value.accepted eq true
→ 结算工作流
→ /tasks/T/settlement = { settled: true, ... }
→ 订阅 S3 不再命中 ⇒ 停关键点
- 条件只看
accepted,不关心是谁验收的; - 结算工作流读取验收事实,写入结算事实;
- 整条链的
causedBy首尾相接,能回答“这笔结算是被哪次验收触发的”。
案例 4:争议 → 处置
业务问题
交付被拒后,争议怎么记录、怎么定责?
路径设计
/tasks/T/disputes/D1
/tasks/T/disputes/D1/resolution处置嵌在争议下,而不是平铺。 这是把不变量写进地址:没有争议,就不该有处置。
谁写
- 争议:买家 / Agent / 平台;
- 处置:平台 / 第三方权威。
写权限按事实类型限定,避免“谁都能写处置”。
案例 5:托管 → 结算指令
业务问题
钱什么时候锁、什么时候划转?
事实
/tasks/T/escrow = { status: 'held', amount, currency, ... }EscrowStateService 强制写入:写失败就抛错,不允许“业务成功、状态没记”。 这与任务节点的“旁挂写入、失败只 warn”相反,因为托管涉及钱和责任,不能静默失败。
边界
状态层只记录“钱应该怎么动”,不执行真实资金划转。 真正划转由支付/托管系统消费这条事实。
案例 6:错误 → 告警或补偿
业务问题
Agent 执行失败,怎么让人知道,或者自动补偿?
路径
/tasks/T/agents/A/errors/{eventId}错误路径与结果路径分开:
- 结果路径:失败不是交付,不能冒充结果被下游读到;
- 错误路径:必须落进状态层,否则“唤醒失败”在因果链里是隐形的。
可以怎么用
{
"pattern": "/tasks/*/agents/*/errors/*",
"action": { "type": "execute_workflow", "workflowId": "notify-ops" }
}或者订阅后写一条补偿任务、升级人工。 当前没有内置自动重试,是否重试应由订阅方决定。
案例 7:门面需求登记 → 唤醒
业务问题
用户在门面里确认需求后,怎么叫醒真正能交付的 Agent?
链路
用户确认
→ /tasks/T/requests/R1
→ 订阅 pattern /tasks/*/requests/*
→ execute_agent(facade-hs-code)
→ /tasks/T/agents/facade-hs-code/results/{eventId}注意
- 这条链只有在订阅已发布时才成立;
active只代表门面能被看到、能对话,不代表会被状态唤醒;- 多个门面共用同一个 pattern 时,全部发布会互相抢,所以默认不批量发布。
案例 8:周期服务
业务问题
VAT 申报、政策变更、年度复审这类事,不是用户来了才发生。
链路
外部 cron / 运营脚本
→ state.write(/tasks/T/requests/{period})
→ 订阅命中
→ execute_agent(facade-vat-registration)
→ 结果写回现状
状态唤醒链路已实现;“定时写状态”需要业务侧配置,尚未做成开箱即用的产品功能。 没有内置 durable timer,也没有延时队列。
案例 9:取证与历史版本
业务问题
争议发生时,怎么证明“当时验收的是哪一版”“是谁改的”?
三个工具
| 工具 | 回答的问题 |
|---|---|
traceCausation({ taskId }) | 这次交付/结算是怎么一步步来的,每一步的 actor 是谁 |
readVersion(path, n) | 当时那一版的值是什么 |
verify(namespace) | 当前看板与事件重放是否一致 |
反证
fulfillment-state.e2e-spec.ts 里有一条: 清空 fulfillment_* 业务集合后,getTrace() 仍然完整。 因为 trace 只读状态层,不依赖业务表。
案例 10:乐观并发与批量写入
业务问题
两个执行者同时改同一条记录,谁赢? 一次业务动作要写多条事实,怎么保证原子?
两个能力
expectedVersion:不匹配就 409,避免后写覆盖先写;writeMany:一次事务写多条事件 + 物化 + outbox,同一 namespace 分配连续seq。
注意
writeMany 同一批内不允许重复 path; 同一 path 的多次写需要明确的顺序语义,批量接口不猜。
3. 业务逻辑模式总结
所有已接入的案例,基本都能套进同一个三段式:
事实(谁写了什么)
→ 规则(pattern + condition)
→ 动作(execute_workflow / execute_agent)| 业务模式 | 事实 | 规则 | 动作 |
|---|---|---|---|
| 门面单点交付 | requests | confirmed=true | execute_agent |
| 自动验收 | deliveries | op=set | execute_workflow |
| 自动结算 | acceptance | accepted=true | execute_workflow |
| 错误告警 | errors | pattern | execute_workflow |
| 周期服务 | requests | pattern | execute_agent |
| 任务恢复 | steps | 无订阅,按需读取 | — |
| 审计取证 | 任意事实 | 无订阅,按需查询 | — |
最重要的一条:
不是所有状态写入都需要订阅。 只有“需要别人行动”时才加规则;只是留痕、查询、恢复的,不需要。
4. 什么时候只需要写状态
| 需求 | 需要订阅吗 |
|---|---|
| 记录执行轨迹、方便恢复 | ❌ 只需要写 + 读 |
| 审计、定责、历史版本 | ❌ 只需要写 + 查询 |
| 给下游提供数据依赖 | ❌ 下游用 stateReads 读即可 |
| 验收后自动结算 | ✅ 需要 |
| 交付后自动验收 | ✅ 需要 |
| 失败后自动告警/补偿 | ✅ 需要 |
| 需求确认后叫醒 Agent | ✅ 需要 |
先把“事实”设计好,再决定要不要加“规则”。 反过来,先加一堆订阅、再补事实,通常会把链路做成噪音。
5. 新增一条业务逻辑的步骤
定事实
- 路径是什么?
/tasks/{taskId}/...下的哪一段? - 值是什么形态?需要哪些字段供下游寻址?
- 谁有资格写?
actor/source/ 写权限。
- 路径是什么?
定读取方
- 有没有下游需要读?用
stateReads还是直接查? - 需要历史版本吗?需要就
readVersion。
- 有没有下游需要读?用
定规则(可选)
- 谁需要被通知?
pattern尽量窄。 - 条件是什么?只支持白名单比较器。
- 会自激吗?
maxTriggerDepth设多少。
- 谁需要被通知?
定动作
- 跑工作流还是叫 Agent?
- 失败写哪里?谁负责告警/补偿?
验证
traceCausation能串起来吗?verify()有 mismatch 吗?- 链会自然停吗?
6. 它不负责的事
| 不负责 | 应该由谁做 |
|---|---|
| 定时器 / 延时任务 | 外部 cron、Temporal、调度器 |
| 自动重试与补偿事务 | 订阅方、工作流、Saga 实现 |
| 资金划转 | 支付/托管系统;状态层只记事实 |
| 全文搜索 / 能力发现 | Marketplace、Skill Catalog、搜索索引 |
| 字段级权限 / 脱敏 | 安全网关;当前只有写权限策略 |
| 跨 namespace 事务 | 业务补偿或同库事务设计 |
| 图灵完备的条件逻辑 | 执行器(工作流/Agent),规则层只做白名单比较 |
7. 一句话理解
状态层是“业务事实的操作系统”: 它记录发生了什么、谁做的、被谁触发; 订阅是中断处理程序,执行器是处理函数; 唤醒 Agent 只是其中一种中断,跑工作流、验收、结算、告警都是同一套机制。
下一步
- 状态层的数据模型与约束 → 状态层
- 状态如何驱动 Agent → 状态如何驱动 Agent
- 第一次接触这些概念 → 小白版:状态驱动 Agent 是怎么跑起来的
- 履约与托管如何写入状态 → 履约与托管