Skip to content

状态层 ​

状态层(L0)回答一个问题:"发生了什么"能否不依赖任何人的记忆,被完整、可信地重建?


为什么需要状态层 ​

在多 Agent 协作中,事实分散在:

  • Task 表(MySQL)
  • Workflow / Delivery / Escrow(MongoDB)
  • Agent 的日志与回传
  • 人的聊天记录

一旦发生争议,要回答"谁在什么时候承诺了什么、谁改了状态"非常困难。

状态层的做法是把这些事实统一成事件日志 + 物化状态:

写入方                 状态层                    读取方
──────                ────────                  ──────
买家                  StateEvent(仅追加)        履约查询
Agent        ────▶    StateEntry(投影)   ────▶ 运营视图
平台                  StateSubscription         因果链 trace

数据模型 ​

集合作用
state_events仅追加的事件日志,真相源
state_entries物化状态,path 唯一、version 单调
state_subscriptions订阅注册表(条件 + 动作)
state_namespace_counters命名空间序列号分配器

寻址 ​

路径是层级化的,命名空间与 taskId 从路径派生:

/tasks/{taskId}/deliveries/{deliveryId}
/tasks/{taskId}/deliveries/{deliveryId}/acceptance
/tasks/{taskId}/disputes/{disputeId}
/tasks/{taskId}/escrow
/tasks/{taskId}/agents/{agentId}/results/{eventId}

事件字段 ​

json
{
  "eventId": "evt_...",
  "path": "/tasks/t1/escrow",
  "op": "set",
  "value": { "status": "held", "amount": 100000 },
  "version": 3,
  "seq": 42,
  "actor": "buyer:u123",
  "causedByEventId": "evt_...",
  "triggerDepth": 1,
  "source": "agent_callback",
  "timestamp": "2026-10-18T14:00:00Z"
}
字段含义
opset / merge / append / delete
version该 path 的版本,只增
seqnamespace 内单调递增
actor责任方,缺失即拒绝写入
causedByEventId因果来源
triggerDepth触发深度,用于防环

四条硬约束 ​

约束实现
仅追加state_events 没有任何 update / delete 代码路径
可寻址所有状态都能通过 path 精确定位
可版本化readVersion(path, n) 可读历史版本
可复放replay() 从事件重建;verify() 校验物化 == 重放

物化状态不是真相源。删掉 state_entries 后,用 replay() 可以完整恢复。


写权限 ​

写入必须携带 actor(谁写的)。默认策略(🟡)是只允许写与自己有关的路径:

写入者规则
platform / system全权(平台代记)
agent / user只能写路径段中含自身 id 的路径

例外是平台代记事实:事实由当事人做出、由平台服务代记,路径里是业务 id 而不是当事人 id。开口是有界的 —— 按事实类型限定写入者,而不是一律放开:

事实路径允许写入者
需求提交/tasks/{t}/requests/{r}user / platform
交付/tasks/{t}/deliveries/{d}agent
验收/tasks/{t}/deliveries/{d}/acceptanceuser / platform
争议/tasks/{t}/disputes/{q}user / agent / platform
处置/tasks/{t}/disputes/{q}/resolutionplatform / system
托管/tasks/{t}/escrowuser / platform
额度扣减/tasks/{t}/commits/{a2aTaskId}api_client / user / platform

段数被显式校验:少一层、多一层都不认(/tasks/{t}/deliveries/{d}/x、/tasks/{t}/requests 都不是事实),避免"加一层就绕过写入者约定"。

需求提交为什么需要例外:对话确认时 submit_request 用确认的用户身份写 /tasks/{t}/requests/{会话 id} —— 路径里只有任务与会话 id。按默认策略会被 403,而契约路径没有 {userId} 变量,任何契约都绕不过去(这个缺口让「确认 → 写状态 → 触发下游」在真库上一直失败,由 facade-dialogue e2e 首次暴露)。归属校验放在上层对话入口(session 必须属于该用户),状态层只按事实类型放行,因此事件日志仍能回答"是谁确认的"(actor = user:{id})。


写入原子性 ​

一次写入包含三步:

① namespace seq $inc
② 事件 insert
③ 物化 upsert

这三步在同一事务中完成。此前 ② 成功而 ③ 失败会留下"有事件、无状态"的不一致,verify() 能发现但无法自愈。

如果调用方已经开启事务(例如履约与托管),write() 接受外部 session,让 Mongo 业务写入与状态写入同生共死:

Mongo 业务写入 ─┐
                ├─ 同一事务 ─▶ commit
L0 状态写入   ──┘
                  L0 拒绝 ⇒ Mongo 回滚

订阅派发在 commit 之后执行,因为外部副作用无法回滚。


订阅与触发 ​

订阅让状态变化可以驱动执行:

json
{
  "pattern": "/tasks/*/deliveries/**",
  "condition": { "field": "status", "op": "eq", "value": "delivered" },
  "action": { "type": "execute_workflow", "workflowId": "wf_acceptance" },
  "maxTriggerDepth": 5
}

执行器:

执行器作用
execute_workflow命中后启动一个工作流
execute_agent命中后直接调用 Agent(A2A 或内部),结果回写状态

防环 ​

事件携带 causedByEventId 与 triggerDepth。超过订阅声明的 maxTriggerDepth 后只记录事件,不再触发执行。

一条完整的执行闭环 ​

外部 Agent 回传交付
  → 写入 /tasks/t1/deliveries/d1(actor=agent:x)
  → 命中订阅:status=delivered
  → 执行验收工作流
  → 写入 /tasks/t1/deliveries/d1/acceptance(actor=buyer)
  → 命中订阅:status=accepted
  → 触发结算(escrow)
  → 写入 /tasks/t1/escrow(actor=platform)
  → 无更多订阅 → 自然停止

traceCausation({ taskId }) 可以重建整条链路,每一跳的 actor 都可查。


旁挂原则 ​

状态层不替换既有执行路径:

场景是否写入
节点声明了 config.stateWrites✅ 写入,失败只 warn,不影响节点结果
节点未声明❌ 零写入
节点声明了 config.stateReads✅ 读取,注入 input.stateReads.{as}
履约 / 托管事实✅ 强制写入,失败即抛错

为什么履约与托管是强制的:它们是钱与责任的证据,静默丢失不可接受。节点级旁挂则是可选的观测能力,不能因为观测失败而阻断业务。


与履约、托管的关系 ​

L1 履约事实 ──强制──▶ /tasks/{id}/deliveries/...
L2 托管事实 ──强制──▶ /tasks/{id}/escrow
对话需求提交 ──强制──▶ /tasks/{id}/requests/...(对话层唯一写入口)
节点执行痕迹 ──旁挂──▶ /tasks/{id}/nodes/...
Agent 回传   ──旁挂──▶ /tasks/{id}/agents/...

对话提交的需求是强制的:用户在对话里确认后,submit_request 先按契约 input schema 校验,不合格把错误回填给模型继续追问,合格才写状态层;写入失败不会被当成"已提交"(这一条由 facade-dialogue e2e 守)。

FulfillmentService.getTrace() 只读状态层即可重建一单的完整叙事;清空 Mongo 业务集合后 trace 仍完整,这一点由 e2e 反证。


刻意没做 ​

不做原因
对外 REST 端点条件与动作形状尚未稳定,不进入公开契约
事件删除 / 归档 / 压缩会破坏 replay
为推测性字段建唯一约束会把猜测固化
订阅条件里的算术 / 时间窗 / 聚合等真实订阅需求
细粒度命名空间授权产品规则未定

验证方式 ​

bash
# 状态层单测(寻址 / 版本 / 仅追加 / actor / 权限 / 条件 / 触发 / 防环 / 旁挂 / replay)
npx jest libs/modules/src/state

# 真库 e2e(会写 .env 指向的库,自建自清)
npx jest --config ./apps/hushwork/test/jest-e2e.json --runInBand \
  --testPathPatterns="state-layer"

# L1 ↔ L0 接线 e2e
npx jest --config ./apps/hushwork/test/jest-e2e.json --runInBand \
  --testPathPatterns="fulfillment-state"

# 对话 → state → 订阅 → 结果(门面闭环,真库)
npx jest --config ./apps/hushwork/test/jest-e2e.json --runInBand \
  --testPathPatterns="facade-dialogue"

下一步 ​

Released under the MIT License.