State
状态层(L0):把"发生了什么"变成可寻址、可版本化、仅追加、可复放的事实。
职责
| 负责 | 不负责 |
|---|---|
| 事件日志与物化状态 | 业务状态机(fulfillment / escrow) |
| 寻址、版本、复放 | 对外 REST 写接口 |
| actor 归属与因果链 | 权限体系(只留默认策略) |
| 订阅匹配与触发 | 业务规则判断 |
| 旁挂读写 | 替换既有执行路径 |
数据模型
| 集合 | 作用 |
|---|---|
state_events | 仅追加事件(真相源) |
state_entries | 物化状态(投影) |
state_subscriptions | 订阅 |
state_namespace_counters | 序列分配 |
state_domains | 域声明(治理单位:factClass / 权威 / 期间策略) |
路径
/tasks/{taskId}/deliveries/{deliveryId}
/tasks/{taskId}/deliveries/{deliveryId}/acceptance
/tasks/{taskId}/disputes/{disputeId}
/tasks/{taskId}/escrow
/tasks/{taskId}/agents/{agentId}/results/{eventId}
/tasks/{taskId}/nodes/{nodeId}事件
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"
}服务结构
| 服务 | 职责 |
|---|---|
StateService | 读写、寻址、版本、权限、派发 |
StateReplayService | replay() / verify() / readVersion() / traceCausation() |
StateSubscriptionService | 订阅注册、pattern 匹配、条件委派 |
StateTriggerDispatcher | 触发编排、防环、执行器注册 |
StateNodeWriteService | 节点旁挂写 |
StateNodeReadService | 节点旁挂读 |
StateConditionEvaluator | 白名单比较器(无 eval) |
写入
typescript
await state.write({
path: '/tasks/t1/escrow',
op: 'set',
value: { status: 'held', amount: 100000 },
actor: 'buyer:u123',
});内部三步在同一事务:
① seq $inc
② 事件 insert
③ 物化 upsert支持外部 session:
typescript
await state.write({ ..., session, onCommitted });这样履约/托管可以与业务写入同生共死。
复放与校验
typescript
// 重建
const state = await replay({ taskId: 't1' });
// 校验物化 == 重放
const ok = await verify({ namespace: 'tasks' });
// 读历史版本
const v2 = await readVersion('/tasks/t1/escrow', 2);
// 因果链
const trace = await traceCausation({ taskId: 't1' });replay 与物化写入共用同一个 reducer,保证两条路径结果一致。
订阅与触发
json
{
"pattern": "/tasks/*/deliveries/**",
"condition": { "field": "status", "op": "eq", "value": "delivered" },
"action": { "type": "execute_agent", "agentId": "..." },
"maxTriggerDepth": 5
}| 执行器 | 作用 |
|---|---|
execute_workflow | 启动工作流 |
execute_agent | 直接调 Agent,结果回写状态 |
防环依靠 causedByEventId + triggerDepth。
域治理与时间轴
域声明
平台按事实形态分四类存储,但共享同一套寻址与权限:
factClass | 存储 | 例子 |
|---|---|---|
event | state_events | 订单、期间、交付 |
ledger | ledger_accounts + ledger_movements | 库存、配额 |
master | master_entities + master_aliases | 物料、供应商 |
computation | computation_runs | 定价、MRP |
每个域必须声明 factClass、authority、periodPolicy、writers。
- 未声明的顶层域写入被拒(内置
tasks/agents除外) factClass声明后不可更改——换类别等于换存储形态- 声明有 60s 缓存,写入热路径不查库
详见 四类事实形态与域模型。
双时间轴
| 轴 | 字段 | 用途 |
|---|---|---|
| 业务发生 | occurredAt | 归属期间、余额计算 |
| 系统记录 | at | 审计「何时知道」、复放顺序 |
occurredAtSource 标明可信度:declared(调用方声明)/ source(外部系统)/ inferred(回退为记录时间)。 inferred 的事实不能进被重开过的期间。
期间门禁
StateService.writeMany()
→ 域声明缓存
→ PeriodGuard.assertWritable() ← 已关闭期间 → 409
→ prepareWrite()只看 /periods/{id}/... 子路径——根路径永远放行,否则 close() 之后无法 reopen()。 绕过记账服务直接写状态层,同样被拒。
旁挂语义
| 场景 | 行为 |
|---|---|
节点声明 stateWrites | 写,失败只 warn |
| 节点未声明 | 零写入 |
节点声明 stateReads | 读,注入 input.stateReads.{as} |
| 履约 / 托管 | 强制写,失败即抛错 |
边界与限制
| 限制 | 说明 |
|---|---|
| 无对外端点 | 条件/动作形状未稳定 |
| 事件不删除 | 删除会破坏 replay |
| 订阅条件有限 | 无算术、时间窗、多事件聚合 |
| 无细粒度授权 | 只有默认写权限策略 |
| 事件会增长 | 尚无归档策略 |
测试
bash
npx jest libs/modules/src/state
npx jest --config ./apps/hushwork/test/jest-e2e.json --runInBand \
--testPathPatterns="state-layer"源码位置
libs/modules/src/state/
├── schemas/
├── services/
├── triggers/
├── state.module.ts
└── state-wiring.module.ts