## 可靠性与治理设计

可靠性不是给每个编排单元增加重试次数。
生产 Workflow 需要同时约束副作用身份、等待边界、失败分类、权限、补偿和人工决策，并确保恢复后仍执行同一份已发布语义。
本章给出一套从风险分析到验收证据的最小治理方法。

<!-- slot: 目标 -->
### 本章目标

学完本章，你将能够：

- 为消息、Agent 外部写入和 callback 设计幂等边界。
- 区分平台 retry、创作期 timeout、业务补偿和运营 SLA。
- 用显式端口、稳定错误码和人工兜底处理失败。
- 应用最小权限、审批分离、审计证据和恢复演练。

<!-- slot: 边界 -->
### 适用边界

- **覆盖**：当前 Plan 编译器、持久 Run、Effect 分发、审批与 callback 服务已经保证的行为，以及基于这些行为的设计方法。
- **不覆盖**：跨系统分布式事务、自动回滚所有外部副作用、无损 exactly-once 网络传输或自动满足业务 SLA；这些能力当前不由 Workflow runtime 保证。
- **假设**：外部系统能够接受业务幂等键，团队具有明确的人类执行所有者和运维责任人。

### 先分清四类机制

| 机制 | 解决的问题 | 当前平台边界 |
| --- | --- | --- |
| retry | Effect 投递发生的可重试失败 | 只覆盖 `send_message` 与 `request_approval` 的投递；不重跑编排单元或 callback 等待 |
| timeout | `wait` 的作者可见截止与超时端口 | 消息、审批、join 没有创作期 timeout |
| 补偿 | 外部副作用已经发生后的业务反向操作 | 必须由 Agent、App 或人工显式实现 |
| SLA 与升级 | 多久响应、谁接管、何时告警 | 运营策略，由监控和值班流程执行 |

retry 使用同一个持久 Effect 和冻结请求语义，并按 `maxAttempts` 与 `backoff` 调度消息或审批的投递失败。
它不会重新执行 `send_message` 对应的编排单元，也不会替 Agent 重做 callback 中的业务工作；`wait` 当前直接按持久截止时间完成或超时，不消费 retry 预算。
消息已经可见但 receipt 暂时丢失时，runtime 以 Effect 已绑定的 `MessageID` 和冻结请求读取持久 receipt，并用稳定 `clientMessageId` 校验同一幂等写入，避免制造“无消息但已完成”的假结果；此时收敛不再受原投递 `maxAttempts` 限制。
当前也不存在自动重跑整个 Plan 或某个编排单元的机制，这仍不等于任意外部业务操作 exactly-once。

业务 Agent 应为每个外部业务意图使用跨消息、Invocation 与 Run 稳定的键：业务对象 + 来源批准版本或业务事件 + 期望变化 + 目标。字段必须来自可信调用输入或平台签发身份；Run、Activation、Effect 只作审计关联，不能因新 Run 而改变同一意图的键。
重试或恢复前先查询该键：已经完成就读取结果；只有权威证据证明原请求未执行且不会再生效、动作契约又允许同一意图安全重放时，才有界执行写入，否则转人工。
callback 使用自己的幂等键保护首结果生效，但安全重放要求端口、幂等键、输出、失败码与失败消息、`workScope` 全部相同；任一项变化都会冲突。
Schema 错误会在提交事务前被拒绝，Effect 继续保持 `awaiting`，因此 Agent 可以修正后以新的完整终态请求重交。
callback 幂等不能替代外部系统幂等。

### 把失败变成可决策的数据

每个可失败编排单元都应有明确路由，或有意识地依赖编译器生成的 fail-Plan sink。
面向治理的 Plan 更适合写出业务可识别的失败终点，例如 `approval_rejected`、`execution_failed`、`recovery_failed`。
稳定错误码用于告警聚合和运行手册，不能把敏感数据或整段外部响应塞进 `code`。

系统故障、业务拒绝和人工取消是不同事实：

- 投递失败表示 Effect 没有完成其协议。
- Agent 主动失败表示业务工作无法按要求完成。
- 审批驳回是经过认证的人类决定。
- Run 取消会终止后续 Workflow 工作，但不能撤回已经可见的外部副作用。

补偿不是把 Run 状态改回原值。
它是新的、可审计的业务操作，例如关闭已创建工单、回滚部署或撤销权限。
补偿本身也可能失败，所以必须有自己的输出、幂等键、错误码和人工兜底。

<!-- slot: 提示词 -->
### 执行与补偿提示词

执行 Agent 的消息应要求返回可用于恢复的业务收据。

```text
执行已批准的变更。
使用调用输入或平台签发的业务对象、来源批准版本 / 业务事件、期望变化和目标构造跨 Run 业务幂等键；Run、Activation、Effect 只写入审计关联。
执行前按完整业务键查询是否已有结果；结果不明且无法证明原请求不会再生效时停止并转人工。
回调返回 outcome 和 receipt；outcome 只能是 succeeded 或 recovery_required。
若产生部分副作用，不要报告 succeeded，必须返回可供恢复 Agent 使用的 receipt。
```

恢复 Agent 的消息必须说明补偿目标和停止条件。

```text
根据 receipt 执行显式恢复。
先查询当前外部状态，避免重复反向操作。
回调返回新的 receipt；无法安全恢复时主动失败，并保留人工处置所需证据。
```

<!-- slot: 流程 -->
### 一条带人工门禁和显式补偿的流程

示例流程包含五个治理边界：

1. 人类审批先于外部变更；驳回直接进入稳定失败终点。
2. 执行 Agent 返回 `succeeded` 或 `recovery_required` 以及业务收据。
3. 成功结果直接完成；需要恢复时，独立 Recovery Agent 按收据执行补偿并返回新收据。
4. 任一消息协议失败都进入明确失败终点，不把异常隐藏成成功。
5. 运维监控可根据错误码和长期处于 `waiting` 状态的 Run 告警，但响应时间是运营策略。

<!-- slot: 配置 -->
### 可编译配置

```yaml
apiVersion: loop.plan/v1
kind: PlanVersion
input:
  approver: {type: member, required: true}
  operation_scope: {type: scope, required: true}
  executor: {type: agent, required: true}
  recovery_agent: {type: agent, required: true}
output:
  receipt: {type: string, required: true}
graph:
  entry:
    nodeId: approve
    port: in
    input:
      approver: {ref: plan.input.approver}
  nodes:
    - id: approve
      type: request_approval
      input:
        approver: {type: member, required: true}
      requestApproval:
        assignee: "{{ input.approver }}"
        summary: "批准执行受控变更"
        retry: {maxAttempts: 2, backoff: PT1S}
    - id: execute
      type: send_message
      input:
        operation_scope: {type: scope, required: true}
        executor: {type: agent, required: true}
      sendMessage:
        scope: {ref: input.operation_scope}
        responder: {ref: input.executor}
        text: "执行变更，回传 outcome 和 receipt"
        completion: callback
        workScope: {ref: input.operation_scope}
        callbackOutput:
          outcome:
            type: enum
            required: true
            values: [succeeded, recovery_required]
          receipt: {type: string, required: true}
        retry: {maxAttempts: 3, backoff: PT2S}
    - id: route_result
      type: switch
      input:
        outcome:
          type: enum
          required: true
          values: [succeeded, recovery_required]
        receipt: {type: string, required: true}
      switch:
        mode: first_match
        cases:
          - port: completed
            when: 'input.outcome == "succeeded"'
          - port: recover
            isDefault: true
    - id: recover
      type: send_message
      input:
        operation_scope: {type: scope, required: true}
        recovery_agent: {type: agent, required: true}
        receipt: {type: string, required: true}
      sendMessage:
        scope: {ref: input.operation_scope}
        responder: {ref: input.recovery_agent}
        text: "按收据 {{ input.receipt }} 执行显式恢复"
        completion: callback
        workScope: {ref: input.operation_scope}
        callbackOutput:
          receipt: {type: string, required: true}
        retry: {maxAttempts: 2, backoff: PT2S}
    - id: success
      type: end
      end: {outcome: succeeded}
    - id: rejected
      type: end
      end: {outcome: failed, code: approval_rejected}
    - id: execution_failure
      type: end
      end: {outcome: failed, code: execution_failed}
    - id: approval_delivery_failure
      type: end
      end: {outcome: failed, code: approval_delivery_failed}
    - id: recovery_failure
      type: end
      end: {outcome: failed, code: recovery_failed}
  edges:
    - id: approved
      source: {nodeId: approve, port: approved}
      target: {nodeId: execute, port: in}
      bindings:
        operation_scope: {ref: plan.input.operation_scope}
        executor: {ref: plan.input.executor}
    - id: rejected
      source: {nodeId: approve, port: rejected}
      target: {nodeId: rejected, port: in}
    - id: approval_failed
      source: {nodeId: approve, port: failed}
      target: {nodeId: approval_delivery_failure, port: in}
    - id: executed
      source: {nodeId: execute, port: completed}
      target: {nodeId: route_result, port: in}
      bindings:
        outcome: {ref: output.outcome}
        receipt: {ref: output.receipt}
    - id: execute_failed
      source: {nodeId: execute, port: failed}
      target: {nodeId: execution_failure, port: in}
    - id: completed
      source: {nodeId: route_result, port: completed}
      target: {nodeId: success, port: in}
      bindings:
        receipt: {ref: input.receipt}
    - id: needs_recovery
      source: {nodeId: route_result, port: recover}
      target: {nodeId: recover, port: in}
      bindings:
        operation_scope: {ref: plan.input.operation_scope}
        recovery_agent: {ref: plan.input.recovery_agent}
        receipt: {ref: input.receipt}
    - id: recovered
      source: {nodeId: recover, port: completed}
      target: {nodeId: success, port: in}
      bindings:
        receipt: {ref: output.receipt}
    - id: recovery_failed
      source: {nodeId: recover, port: failed}
      target: {nodeId: recovery_failure, port: in}
```

示例只把消息投递、审批、callback、路由和持久恢复写成平台语义。
具体“执行变更”和“恢复”仍由 Agent 及其 App 或 Extension 实现。
Effect 投递与收据恢复见 [`internal/workflow/orchestrator/dispatcher.go: EffectDispatcher.dispatchMessage / dispatchApproval / resolveFailure`](https://github.com/tidbcloud/AgentTeam/src/branch/main/internal/workflow/orchestrator/dispatcher.go)。
callback 的 Schema 校验、事务提交和精确重放比较见 [`internal/workflow/orchestrator/callback_service.go: CallbackService.ExecuteCallback / callbackCompletionMatches`](https://github.com/tidbcloud/AgentTeam/src/branch/main/internal/workflow/orchestrator/callback_service.go)，审批首结果与冻结 assignee 的 fence 见 [`internal/workflow/orchestrator/approval_service.go: ApprovalDecisionService.DecideApproval`](https://github.com/tidbcloud/AgentTeam/src/branch/main/internal/workflow/orchestrator/approval_service.go)。

### 最小权限与职责分离

Invocation 的人类执行所有者、审批人、执行 Agent 和 Recovery Agent 应按风险分离。
调用输入只提供本次操作所需的 `scope`、`member` 和 `agent`，不把 Workspace 管理权限当作默认前提。
`relatedScopes` 只给可见上下文，不授予写权限。
具体 App 或 Extension 的 action 也应单独安装、选择并授权。

这里要区分平台保证与治理策略。
职责分离、双人复核和谁可以发布是团队设计与运营策略；runtime 能强制的是审批 assignee、callback responder 在 Effect 上冻结，以及首个终态结果提交后由冲突 fence 拒绝不同结果。
Run 在 Invocation 被接受时固定精确 `PlanVersionID`，之后默认版本变化不影响这个 Run；手动调用省略版本时才会在接受阶段选择当时的默认版本，见 [`internal/workflow/invocationmanage/types.go: ManualInput`](https://github.com/tidbcloud/AgentTeam/src/branch/main/internal/workflow/invocationmanage/types.go) 与 [`internal/workflow/invocationmanage/service.go: Service.InvokeManual`](https://github.com/tidbcloud/AgentTeam/src/branch/main/internal/workflow/invocationmanage/service.go)。
Binding revision 同样把精确 `PlanVersionID` 持久化为权威字段；创建新 revision 不会自动激活，必须再执行显式激活，见 [`internal/workflow/binding/types.go: RevisionSpec`](https://github.com/tidbcloud/AgentTeam/src/branch/main/internal/workflow/binding/types.go) 以及 [`internal/workflow/bindingmanage/service.go: Service.CreateRevision / Activate`](https://github.com/tidbcloud/AgentTeam/src/branch/main/internal/workflow/bindingmanage/service.go)。
因此修改默认版本既不会漂移现有 Run，也不会漂移现有 Binding。

高风险变更至少保留：

- 发布的 Plan 版本和语义 digest。
- Invocation 输入与执行所有者。
- Run、Activation、Intent、Effect 和事件时间线。
- 审批人、决定、消息 receipt 与 callback 响应者。
- 外部业务幂等键、执行收据、恢复收据和最终验收证据。

这些证据支持复盘，但不应记录密钥、访问令牌或不必要的个人数据。

<!-- slot: 异常 -->
### 故障矩阵与恢复

| 故障 | 平台可见事实 | 自动处理边界 | 人工兜底 |
| --- | --- | --- | --- |
| 消息 writer 暂时失败 | Effect 保留投递错误与尝试计数 | 可见前在 retry 预算内重试；已绑定 `MessageID` 后按冻结请求恢复 receipt，并以稳定 `clientMessageId` 校验 | 核对目标、权限与消息记录 |
| callback 输出错误 | Effect 仍为 `awaiting` | 提交前拒绝 Schema 或终态合同错误 | 指导冻结 responder 修正输出 |
| Run 进程重启 | 持久状态与 Effect 仍在 | worker 重新 claim 并继续 | 对账事件与外部收据 |
| 外部写入部分成功 | Agent 返回恢复需要与 receipt | Workflow 路由到 Recovery Agent | 恢复失败后按手册处理 |
| 长时间等待审批或 callback | Run 为 `waiting` | 当前编排单元不保证 SLA | 监控告警、升级或人工取消 |
| Run 已取消但外部副作用存在 | 后续图工作停止 | 不自动撤销外部系统 | 根据 receipt 执行补偿 |

恢复演练应至少覆盖 worker 重启、重复投递、重复 callback、迟到 callback、审批冲突、部分外部写入和补偿失败。
演练结论要包含运行事件、外部系统状态与最终业务验收，不能只看 Run 是否进入终态。

<!-- slot: 审批 -->
### 审批与人工兜底

审批摘要应让人看到对象、风险、预期副作用、验证方法和补偿入口。
审批驳回不是系统故障，应该有独立错误码和业务处置。
当前 `request_approval` 没有创作期 timeout；“30 分钟升级给值班经理”必须明确写为监控和排班策略，不能写成 runtime 自动保证。

人工取消前先判断是否已有外部副作用。
取消只阻止后续 Workflow 工作；操作人仍需根据 receipt 决定保留、补偿或升级事件。

<!-- slot: 外部系统 -->
### 外部系统

外部系统适配由 Agent 经 App 或 Extension 承担。
写操作应支持查询业务幂等键，并返回足以对账和补偿的收据。
如果外部 API 不支持幂等，Agent 应采用“先查、再写、写后核验”的最小协议，并在不确定状态时停止自动重试，转入人工兜底。

<!-- slot: 验收 -->
### 验收

- [ ] 每个外部副作用都有业务幂等键、收据和重放策略。
- [ ] retry、timeout、补偿和运营 SLA 没有混写。
- [ ] 失败、驳回、取消和部分成功具有不同的可审计处置。
- [ ] 能区分职责分离的运营策略，与冻结 responder/assignee、结果 fence 等 runtime 保证。
- [ ] Run 与 Binding 均固定精确版本，默认版本变化不会使既有执行漂移。
- [ ] 恢复演练覆盖重复、迟到、重启、部分成功和补偿失败。
- [ ] 本章完整 Plan 原样编译后 `diagnostics=[]`。

<!-- slot: 媒体槽位 -->
### 媒体槽位

<!-- media: fig-15-01-reliability-controls.png -->

建议图示以时间线展示审批、执行 Effect、外部幂等键、callback、补偿 Effect、人工接管和审计证据。

### 小结

- [ ] 平台持久化和 retry 提供恢复基础，但外部业务仍需幂等。
- [ ] 补偿是新的显式业务操作，可能失败并需要人工兜底。
- [ ] SLA、升级和排班属于运营策略，不能伪装成 runtime 语义。
- [ ] 最小权限、职责分离和可验证证据共同构成治理闭环。

完成本篇后，你已经能够把复杂 Workflow 写成可编译、可恢复、可审计且边界诚实的生产设计。
