Skip to content

状态层应用案例拆解 ​

状态层最显眼的用法是“唤醒 Agent”,但它实际承担三类职责: 记录业务事实、驱动业务流转、提供取证与恢复。 唤醒只是其中一种用法,不是全部。


0. 先看全景 ​

text
业务动作
  → 写一条事实(state_events)
      → 有人关心?命中订阅
          → 跑工作流 / 叫 Agent / 写回结果
      → 没人关心?只留痕
  → 需要追查?traceCausation / readVersion / verify

三个关键词:

关键词含义典型用途
记录把发生过的事写成不可改的事件审计、恢复、验收依据
驱动订阅命中后执行下一步唤醒 Agent、跑验收/结算、告警
取证从事件重放出“当时是什么”争议定责、版本核对、对账

1. 已经接入的真实业务逻辑 ​

业务逻辑状态层里的事实谁读 / 怎么用现状
任务执行留痕/tasks/{id}/batches/{batchId}、/steps/{stepId}、/steps/{stepId}/decisionTaskAgentLoopService.project() 重建轨迹;completedStepIds() 重放已完成步骤✅ 默认开启(TASK_EXECUTION_MODE=loop)
断点恢复同上重新执行时跳过已完成 step;中断时 reconcileInterrupted() 补终态✅ 已接
交付与验收/deliveries/{id}、/deliveries/{id}/acceptance外部 Agent 回传写交付;验收可订阅触发✅ 已接 L1
争议与处置/disputes/{id}、/disputes/{id}/resolution履约链路写入;按条件订阅升级/通知✅ 已接 L1
托管与结算/tasks/{id}/escrowEscrowStateService 强制写入;结算指令也在同一路径✅ 已接 L2,资金体系未接
额度扣减/tasks/{id}/commits/{a2aTaskId}API key 委托调用时写入✅ 已接
决策留痕/steps/{stepId}/decision:选了谁、为什么、候选列表recordDecision()✅ 已接
门面需求登记/tasks/{id}/requests/{requestId}submit_request 写入✅ 已接
订阅驱动 Agent命中后写 /agents/{id}/results/{eventId}execute_agent✅ 已接
订阅驱动工作流命中后跑 workflowexecute_workflow✅ 已接

最后两行很重要:触发器不只用于 Agent。 execute_workflow 可以跑验收、结算、通知等工作流,不一定叫 Agent。


2. 案例拆解 ​

案例 1:任务执行留痕与断点恢复 ​

业务问题

一次任务可能有很多 step。进程被 kill、节点失败、用户重试时,怎么知道哪些已经做完?

写入了什么

text
/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 交付了报告,谁来验收?验收依据从哪里来?

链路

text
① 外部 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:验收 → 结算 ​

业务问题

验收通过后,怎么自动进入结算,而不是靠人点按钮?

链路

text
/tasks/T/deliveries/D/acceptance = { accepted: true }
  → 订阅 S2:condition $event.value.accepted eq true
  → 结算工作流
  → /tasks/T/settlement = { settled: true, ... }
  → 订阅 S3 不再命中 ⇒ 停

关键点

  • 条件只看 accepted,不关心是谁验收的;
  • 结算工作流读取验收事实,写入结算事实;
  • 整条链的 causedBy 首尾相接,能回答“这笔结算是被哪次验收触发的”。

案例 4:争议 → 处置 ​

业务问题

交付被拒后,争议怎么记录、怎么定责?

路径设计

text
/tasks/T/disputes/D1
/tasks/T/disputes/D1/resolution

处置嵌在争议下,而不是平铺。 这是把不变量写进地址:没有争议,就不该有处置。

谁写

  • 争议:买家 / Agent / 平台;
  • 处置:平台 / 第三方权威。

写权限按事实类型限定,避免“谁都能写处置”。


案例 5:托管 → 结算指令 ​

业务问题

钱什么时候锁、什么时候划转?

事实

text
/tasks/T/escrow = { status: 'held', amount, currency, ... }

EscrowStateService 强制写入:写失败就抛错,不允许“业务成功、状态没记”。 这与任务节点的“旁挂写入、失败只 warn”相反,因为托管涉及钱和责任,不能静默失败。

边界

状态层只记录“钱应该怎么动”,不执行真实资金划转。 真正划转由支付/托管系统消费这条事实。


案例 6:错误 → 告警或补偿 ​

业务问题

Agent 执行失败,怎么让人知道,或者自动补偿?

路径

text
/tasks/T/agents/A/errors/{eventId}

错误路径与结果路径分开:

  • 结果路径:失败不是交付,不能冒充结果被下游读到;
  • 错误路径:必须落进状态层,否则“唤醒失败”在因果链里是隐形的。

可以怎么用

jsonc
{
  "pattern": "/tasks/*/agents/*/errors/*",
  "action": { "type": "execute_workflow", "workflowId": "notify-ops" }
}

或者订阅后写一条补偿任务、升级人工。 当前没有内置自动重试,是否重试应由订阅方决定。


案例 7:门面需求登记 → 唤醒 ​

业务问题

用户在门面里确认需求后,怎么叫醒真正能交付的 Agent?

链路

text
用户确认
  → /tasks/T/requests/R1
  → 订阅 pattern /tasks/*/requests/*
  → execute_agent(facade-hs-code)
  → /tasks/T/agents/facade-hs-code/results/{eventId}

注意

  • 这条链只有在订阅已发布时才成立;
  • active 只代表门面能被看到、能对话,不代表会被状态唤醒;
  • 多个门面共用同一个 pattern 时,全部发布会互相抢,所以默认不批量发布。

案例 8:周期服务 ​

业务问题

VAT 申报、政策变更、年度复审这类事,不是用户来了才发生。

链路

text
外部 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. 业务逻辑模式总结 ​

所有已接入的案例,基本都能套进同一个三段式:

text
事实(谁写了什么)
  → 规则(pattern + condition)
  → 动作(execute_workflow / execute_agent)
业务模式事实规则动作
门面单点交付requestsconfirmed=trueexecute_agent
自动验收deliveriesop=setexecute_workflow
自动结算acceptanceaccepted=trueexecute_workflow
错误告警errorspatternexecute_workflow
周期服务requestspatternexecute_agent
任务恢复steps无订阅,按需读取—
审计取证任意事实无订阅,按需查询—

最重要的一条:

不是所有状态写入都需要订阅。 只有“需要别人行动”时才加规则;只是留痕、查询、恢复的,不需要。


4. 什么时候只需要写状态 ​

需求需要订阅吗
记录执行轨迹、方便恢复❌ 只需要写 + 读
审计、定责、历史版本❌ 只需要写 + 查询
给下游提供数据依赖❌ 下游用 stateReads 读即可
验收后自动结算✅ 需要
交付后自动验收✅ 需要
失败后自动告警/补偿✅ 需要
需求确认后叫醒 Agent✅ 需要

先把“事实”设计好,再决定要不要加“规则”。 反过来,先加一堆订阅、再补事实,通常会把链路做成噪音。


5. 新增一条业务逻辑的步骤 ​

  1. 定事实

    • 路径是什么?/tasks/{taskId}/... 下的哪一段?
    • 值是什么形态?需要哪些字段供下游寻址?
    • 谁有资格写?actor / source / 写权限。
  2. 定读取方

    • 有没有下游需要读?用 stateReads 还是直接查?
    • 需要历史版本吗?需要就 readVersion。
  3. 定规则(可选)

    • 谁需要被通知?pattern 尽量窄。
    • 条件是什么?只支持白名单比较器。
    • 会自激吗?maxTriggerDepth 设多少。
  4. 定动作

    • 跑工作流还是叫 Agent?
    • 失败写哪里?谁负责告警/补偿?
  5. 验证

    • traceCausation 能串起来吗?
    • verify() 有 mismatch 吗?
    • 链会自然停吗?

6. 它不负责的事 ​

不负责应该由谁做
定时器 / 延时任务外部 cron、Temporal、调度器
自动重试与补偿事务订阅方、工作流、Saga 实现
资金划转支付/托管系统;状态层只记事实
全文搜索 / 能力发现Marketplace、Skill Catalog、搜索索引
字段级权限 / 脱敏安全网关;当前只有写权限策略
跨 namespace 事务业务补偿或同库事务设计
图灵完备的条件逻辑执行器(工作流/Agent),规则层只做白名单比较

7. 一句话理解 ​

状态层是“业务事实的操作系统”: 它记录发生了什么、谁做的、被谁触发; 订阅是中断处理程序,执行器是处理函数; 唤醒 Agent 只是其中一种中断,跑工作流、验收、结算、告警都是同一套机制。


下一步 ​

Released under the MIT License.