## 让 Agent 完成 Workflow 工作

Workflow 没有“Agent 节点”。
当流程需要分析代码、查询系统或生成业务结果时，正确模型是：`send_message` 把工作交给选定 Agent，`completion: callback` 让当前激活持久等待，Agent 再按声明的 Schema 提交结果。
这条边界把协作消息、Agent 身份、工作范围和结构化输出绑定在同一个可审计协议里。

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

学完本章，你将能够：

- 使用 `scope`、`responder` 和 `completion: callback` 选择一个 Agent。
- 区分投递范围、工作范围与相关上下文范围。
- 为 Agent 声明最小 `callbackOutput`，并沿边消费结果。
- 解释回调鉴权、幂等重放、迟到结果和 Schema 错误的真实行为。

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

- **覆盖**：静态 Agent 协作、固定或 Agent 选择的 `workScope`、相关范围、callback 输出和失败端口。
- **不覆盖**：Agent 动态拆分 WorkItem 并并行派发，见第 14 章。
- **假设**：调用者已提供经过授权的具体 `scope` 和 `agent` 值。

### 一条消息有三个范围

`scope` 是消息的投递范围。
它决定 Workflow 消息实际出现在哪里，但不会自动成为 Agent 可以组织工作的地方。
`workScope` 是本次激活唯一的工作范围；固定表达式会把范围写死在 Plan 中，显式 `null` 则允许 Agent 在回调时选择一个经过授权的范围。
完全省略 `workScope` 保留旧 callback 契约，与显式 `null` 不同。

`relatedScopes` 是供 Agent 按需读取的既有上下文范围集合。
它们不授予权限，不自动加载历史，也不具有控制流意义。
只有声明了 `workScope` 时才能声明 `relatedScopes`，并且两者只适用于 callback 完成模式。

| 字段 | 回答的问题 | 不代表什么 |
| --- | --- | --- |
| `scope` | 消息投递到哪里 | Agent 一定能在这里写工作记录 |
| `responder` | 哪个 Agent 被解析、冻结并取得回调能力 | 其他被消息唤醒的 Agent 也能完成激活 |
| `workScope` | Agent 在哪里组织本次工作 | 自动 claim Task 或改变 Task assignee |
| `relatedScopes` | 哪些范围可作为相关上下文 | 自动授权、自动注入历史或依赖关系 |

当固定投递范围和工作范围是同一个 Task 时，runtime 会为 callback 响应者维护 Task watch。
这只是通知状态，不是 claim、分配、执行租约或第二 assignee。
Plan 不应要求 Agent 为了提交 callback 而先 claim Task。

### Callback capability 冻结身份

在任何可见消息交付之前，runtime 先把 `responder` 解析为 canonical Agent ID，将它冻结到持久 Effect，并取得同一 `(Workspace, canonical Agent)` 的 capacity=1 执行租约；若容量正被占用，Effect 只进入持久队列，不能先发消息。
Agent 的 callback capability 只绑定到 Workspace、Run、Activation、Effect（callback service 内称 `CallbackID`，公共命令中称 `stepId`）和冻结响应者，不存在由 Agent 提供的 callback attempt 身份。
普通消息路由即使唤醒了其他 Agent，也不会赋予它们回调权限。

公开 `get` 是读取上下文前的鉴权边界：服务先核对认证 Agent、Workspace、Run、Activation、`CallbackID`、冻结响应者和等待态；固定 `workScope` 必须可写，作者声明的 `relatedScopes` 必须可读，全部通过后才暴露 typed input、输出契约与协作范围。
这与 `complete` 的事务 guard 是两层检查，不能用一次 `get` 代替提交时授权。

`complete` 先校验返回的 `workScope` 和 scope-typed `output`，以及每个 WorkItem 的 `workScope`/`relatedScopes`，再在首结果生效事务中重新锁定身份与范围授权；Effect 完成、可选 Task watch 和下一激活唤醒同在持久化边界。
同一个幂等键和相同结果可以安全重放；不同结果不能覆盖已经接受的结果。
Run 终止、Activation 取消或 Effect 离开等待态后，迟到 callback 不能复活旧工作。

> **⚠️ 注意：** `callbackOutput` 不匹配时，本次完成请求返回 `ErrCallbackOutputInvalid`，Effect 仍在等待。Agent 应修正输出后重交；节点 `retry` 不会替 Agent 重新执行这项业务工作。

### 同一 Agent 的 FIFO 容量

callback 消息按 `(Workspace, canonical Agent)` 使用 capacity=1 的持久 FIFO。
同一 Workspace、同一 Agent 的队首 Effect 才能取得租约并产生可见消息；队首之前绝不会出现后续 callback 消息。
第一次入队的顺序被持久化，投递重试和进程重启保留原位置，不能重新排队或插队。

这个 FIFO 不是全局锁：不同 Workspace、不同 canonical Agent 可以并行；普通消息、Task 活动和其他 Agent 的工作不占用该容量。
它也不表示 Task assignment、claim、Task 的业务执行租约或业务完成；Task responder 不需要 claim Task，callback 完成只释放 Workflow 的 Agent 容量。

<!-- slot: 提示词 -->
### 提示词

消息正文应给出任务、边界、验证方式和回调字段，而不是只写“帮我看看”。

```text
检查 workScope 中的变更，只读取 relatedScopes 提供的需求上下文。
运行与变更相关的测试。
回调时返回 summary 和 risk；risk 只能是 low、medium 或 high。
不得修改范围外文件，不得在输出中包含凭据。
```

Plan Schema 才是 callback 的机器契约。
提示词可以解释字段含义，但不能新增未声明字段，也不能放宽字段类型。

<!-- slot: 流程 -->
### 从投递到继续运行

静态 Agent callback 的完整链路如下：

1. 图入口把具体投递范围、响应者和相关范围注入 `review`。
2. `send_message` 创建持久 Effect；分发器解析 canonical responder、冻结到 Effect，并取得或排队等待 Agent 容量。
3. 只有 FIFO 队首取得容量后才提交可见消息；投递成功后 Effect 进入等待，Run 对外表现为 `waiting`。
4. Agent 用公开 `list` 发现等待项；`get` 完成身份、等待态及作者范围授权后才返回上下文。
5. Agent 完成检查；`complete` 的事务 guard 校验 `summary`、`risk`、返回范围和首结果。
6. `completed` 边消费输出并释放容量；主动失败或永久投递失败走 `failed`，随后下一 FIFO 队首才可交付。

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

```yaml
apiVersion: loop.plan/v1
kind: PlanVersion
input:
  review_scope: {type: scope, required: true}
  reviewer: {type: agent, required: true}
  requirement_scopes:
    type: array
    required: true
    items: {type: scope, required: true}
output:
  summary: {type: string, required: true}
  risk:
    type: enum
    required: true
    values: [low, medium, high]
graph:
  entry:
    nodeId: review
    port: in
    input:
      review_scope: {ref: plan.input.review_scope}
      reviewer: {ref: plan.input.reviewer}
      requirement_scopes: {ref: plan.input.requirement_scopes}
  nodes:
    - id: review
      type: send_message
      input:
        review_scope: {type: scope, required: true}
        reviewer: {type: agent, required: true}
        requirement_scopes:
          type: array
          required: true
          items: {type: scope, required: true}
      sendMessage:
        scope: {ref: input.review_scope}
        responder: {ref: input.reviewer}
        text: "检查变更，回传 summary 和 risk"
        completion: callback
        workScope: {ref: input.review_scope}
        relatedScopes: {ref: input.requirement_scopes}
        callbackOutput:
          summary: {type: string, required: true}
          risk:
            type: enum
            required: true
            values: [low, medium, high]
        retry: {maxAttempts: 3, backoff: PT2S}
    - id: success
      type: end
      end: {outcome: succeeded}
    - id: failure
      type: end
      end: {outcome: failed, code: agent_review_failed}
  edges:
    - id: reviewed
      source: {nodeId: review, port: completed}
      target: {nodeId: success, port: in}
      bindings:
        summary: {ref: output.summary}
        risk: {ref: output.risk}
    - id: review_failed
      source: {nodeId: review, port: failed}
      target: {nodeId: failure, port: in}
```

这个示例依据 [`internal/workflow/plan/types.go: SendMessageConfig`](https://github.com/tidbcloud/AgentTeam/src/branch/main/internal/workflow/plan/types.go) 与 [`internal/workflow/plan/validate_send_message.go: validator.validateSendMessageConfig`](https://github.com/tidbcloud/AgentTeam/src/branch/main/internal/workflow/plan/validate_send_message.go) 的 authoring 契约。
关键实现来源如下：

- [`internal/workflow/orchestrator/effects.go: MessageRequest、ResolveCallbackResponderAuthority`](https://github.com/tidbcloud/AgentTeam/src/branch/main/internal/workflow/orchestrator/effects.go)：消息身份、冻结响应者与 receipt 边界。
- [`internal/store/workflow/run/agent_queue.go: ClaimCallbackEffectCapacity`](https://github.com/tidbcloud/AgentTeam/src/branch/main/internal/store/workflow/run/agent_queue.go)：同 Workspace/canonical Agent 的 capacity=1 与持久 FIFO。
- [`internal/store/workflow/run/effects.go: Store.ResolveAwaitingEffect`](https://github.com/tidbcloud/AgentTeam/src/branch/main/internal/store/workflow/run/effects.go)：终态 Effect、下一激活与 FIFO 容量释放的事务边界。
- [`internal/workflow/orchestrator/callback_service.go: CallbackService.ExecuteCallback`](https://github.com/tidbcloud/AgentTeam/src/branch/main/internal/workflow/orchestrator/callback_service.go)：公开读取、首结果和精确幂等重放。
- [`internal/workflow/orchestrator/callback_scope_authorization.go: callbackScopePreCommitGuard`](https://github.com/tidbcloud/AgentTeam/src/branch/main/internal/workflow/orchestrator/callback_scope_authorization.go)：`complete` 返回新范围时的事务权限 guard；冻结的 callback 上下文无需在 `get` / `pause` / `park` 时重复鉴权。
- [`presets/assets/skills/workflow-plan-author/skills/workflow-plan-author/zh-cn/SKILL.md: Public Workflow CLI / Step`](https://github.com/tidbcloud/AgentTeam/src/branch/main/presets/assets/skills/workflow-plan-author/skills/workflow-plan-author/zh-cn/SKILL.md)：官方中文公开命令契约。

### 固定工作范围与 Agent 选择范围

示例使用固定 `workScope`，适合作者已经知道协作 Task 或 Thread 的场景。
如果 Agent 必须根据工作内容选择或创建范围，可以显式写 `workScope: null`。
Agent 完成时必须返回具体且经过授权的 `workScope`；runtime 冻结该选择，后续重放不能改到另一个范围。

固定或 Agent 选择的工作范围都要同时满足 callback Agent 和本次 Invocation 的人类执行所有者可写。
相关范围则要求双方可见，但不要求可写。
范围必须存在、处于活跃状态、属于同一 Workspace 且 kind 合法。

### 用公开 CLI 完成 Step

只使用公开 `workflow step` 对象；`runId` 和 `stepId` 是显式 selector，不是凭据。
先发现当前认证 Agent 的等待项：

```bash
$ loop exec workflow step - <<'JSON'
{"action":"list","limit":50}
JSON
```

`list` 只发现该 Agent 当前 `awaiting` 的 Step。
从结果取得真实 ID 后，用二者读取精确契约：

```bash
$ loop exec workflow step - <<'JSON'
{"action":"get","runId":"wri_review_01","stepId":"wfx_review_01"}
JSON
```

本章 Plan 的 `get` 会返回 `outputContract` 中必填的字符串 `summary` 和枚举 `risk`，以及固定 `workScope`：

```json
{
  "outputContract": [
    {"name": "summary", "schema": {"type": "string", "required": true}},
    {"name": "risk", "schema": {"type": "enum", "required": true, "values": ["low", "medium", "high"]}}
  ],
  "workScope": {"kind": "task", "scopeId": "tsk_review_01"}
}
```

成功时按这个返回值逐字段提交；下面把 `get` 返回的固定范围原样带回，不能重建或替换：

```bash
$ loop exec workflow step - <<'JSON'
{"action":"complete","runId":"wri_review_01","stepId":"wfx_review_01","output":{"summary":"相关测试通过，未发现阻断项","risk":"low"},"workScope":{"kind":"task","scopeId":"tsk_review_01"},"idempotencyKey":"review-01-complete-v1"}
JSON
```

确实无法完成时，改走 `fail`，不要伪造业务输出：

```bash
$ loop exec workflow step - <<'JSON'
{"action":"fail","runId":"wri_review_01","stepId":"wfx_review_01","code":"review_blocked","message":"缺少读取依赖仓库的权限","idempotencyKey":"review-01-fail-v1"}
JSON
```

这里没有 callback claim；即使 `workScope` 是 Task，也不需要 `task claim`。
同一 `idempotencyKey` 只能用于**完全相同的 terminal result** 重试：端口、输出、失败码/消息和 `workScope` 任一变化都必须视为冲突，不能用同一个键从 `fail` 改成 `complete`。

### 暂停与驻留（`pause` / `park`）

两个非终态命令让 Agent 在“继续等待”的前提下控制自动恢复。二者都是回调上的**行锁条件过渡**：并发只允许一个业务操作报告成功，输家得到 `not-waiting/conflict`，不会从旧快照返回成功。

#### `pause` —— 永久暂停自动恢复

`pause` 是非终态动作：Step 保持 `awaiting`，但自动恢复被**永久**停止（`recovery_paused_at` 写盘）。后续 Agent 活动不会解除暂停；只有 callback `complete`/`fail`、`cancel` 或 Run 终态结束并清理。`pause` 之后 Step 仍可被 `get` 读取，也可随时 `complete`/`fail` 完成业务。

```bash
$ loop exec workflow step - <<'JSON'
{"action":"pause","runId":"wri_review_01","stepId":"wfx_review_01"}
JSON
```

- `pause` 不是 `park` 的“后到覆盖”：若 `park` 已经赢得该回调，后续 `pause` 会输（返回 `not-waiting/conflict`），`park` FSM 继续持有所有权。
- 暂停后自动恢复不再发送 Recovery Prompt，也不再推进 1/2/4/8/16 调度。

#### `park` —— 按权威空闲自动驻留、按权威工作自动恢复

`park` 是非终态动作：它武装持久 park FSM（`arm_on_idle`），自动恢复暂停但 Step 仍 `awaiting`。随后由 **RecoveryRunner 的权威运行态观察**过渡：

- 第一个权威 `idle` 观察确认 `parked`（不发送恢复提示）。
- 之后第一个权威 `working` 观察清除 park，并把恢复调度原子重置为全新 1/2/4/8/16 分钟计划。
- `unknown`/`offline` 运行态零过渡、零写入。

```bash
$ loop exec workflow step - <<'JSON'
{"action":"park","runId":"wri_review_01","stepId":"wfx_review_01"}
JSON
```

- 已 `pause` 的回调不能被 `park`：`recovery_paused_at IS NULL` 是行锁前置条件，输家得到 `not-waiting/conflict`。
- 同一权威观察 epochs 上的重复观察幂等；过期观察（读取后、提交前运行态已变化）被拒绝，零过渡。
- `park` 之后 Step 仍可被 `get` 读取，也可随时 `complete`/`fail` 完成业务。

#### `wait` 已移除

旧 `wait` 命令**已删除**，并且**没有别名**：任何 `{"action":"wait",...}` 请求都会在 action 校验处失败关闭（返回 `action must be list, get, complete, fail, pause, or park`）。需要保持回调不终态等待时使用 `pause` 或 `park`，二者语义见上。

<!-- slot: 异常 -->
### 常见异常与处置

| 现象 | 原因 | 处置 |
| --- | --- | --- |
| callback 未出现在 Agent 列表 | Agent 不是冻结响应者，或 Effect 尚未进入等待 | 检查 `responder` 解析、投递结果和 Run 状态 |
| `ErrCallbackUnauthorized` | 调用者不是冻结 Agent，或范围权限不满足 | 使用正确 Agent，并修复范围权限 |
| `ErrCallbackOutputInvalid` | 缺字段、类型错误、额外工作范围非法 | 按声明 Schema 修正同一 callback |
| `ErrCallbackNotWaiting` | Run、Activation 或 Effect 已不再等待 | 停止旧工作，不要尝试复活 |
| callback 冲突 | 同一激活已接受不同结果 | 读取当前结果，按审计记录处理 |

Agent 主动报告失败会选择 `failed` 端口。
业务设计应区分“Agent 判断任务无法完成”和“消息投递失败”；两者最终可进入同一个稳定失败终点，但审计事件与故障处理不同。

<!-- slot: 审批 -->
### 审批

Agent callback 不是人类审批。
若风险决定必须由人承担，应在 callback 后根据输出路由到独立 `request_approval`，并由明确的人类成员做决定。
不能把“Reviewer Agent 返回 low risk”写成法律、财务或生产发布批准。

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

Agent 可以在其权限内使用 App 或 Extension 查询代码托管、监控或工单系统。
Workflow 不直接调用这些系统。
外部写入需要跨消息、Invocation 与 Run 稳定的业务幂等键：业务对象 + 来源批准版本或业务事件 + 期望变化 + 目标，各部分来自可信调用输入或平台签发身份。Run、Activation、Effect 与 callback 幂等键只作本次技术执行和审计关联；Agent callback 不能自造月份、周期或版本来铸造稳定业务身份。结果不明时按完整业务键查询权威状态，再决定补交 callback、人工接管或在契约允许时有界重放。

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

- [ ] 能分别说明 `scope`、`responder`、`workScope` 和 `relatedScopes`。
- [ ] 能解释消息可见前的 responder 冻结、capacity=1 与持久 FIFO。
- [ ] 能区分 `get` 暴露上下文前的授权和 `complete` 首结果事务 guard。
- [ ] 能用公开 CLI 执行 `list` → `get` → `complete` 或 `fail`。
- [ ] callback 输出字段最小、类型明确，不包含凭据。
- [ ] 能解释错误输出为何修正重交而不是触发节点 retry。
- [ ] 能解释重复、冲突、取消和迟到 callback 的处理。
- [ ] 能区分 callback / Effect 技术幂等与跨 Run 外部业务幂等，且业务键来源可信。
- [ ] 本章完整 Plan 原样编译后 `diagnostics=[]`。

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

<!-- media: fig-13-01-agent-callback-boundary.png -->

建议图示把消息投递、响应者冻结、工作范围授权、Agent 执行、Schema 校验和下一激活放在一条持久时间线上。

### 小结

- [ ] Agent 工作通过 callback 模式的 `send_message` 表达，不存在 Agent 节点。
- [ ] 消息范围、工作范围和相关范围具有不同权限语义。
- [ ] callback 是绑定 Workspace、Run、Activation、Effect、冻结响应者、Schema 和幂等边界的持久协议。
- [ ] 同一 Workspace/canonical Agent 的 FIFO 只排序 callback 交付，不等于 Task claim 或业务完成。

下一章让 Planner Agent 返回一组有依赖关系的 WorkItem，并由 runtime 有界并行执行。
