## 基础节点

基础节点负责产生外部效果、更新状态、持久等待或结束 Run。
它们看似简单，却决定了消息何时算完成、审批为何不会丢失、计时器超时走哪条路，以及失败是否能够被明确审计。

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

学完本章，你将能够：

- 正确配置 `send_message`、`set_variable`、`wait`、`request_approval` 和 `end`。
- 区分消息已投递、Agent 已回调和业务工作已完成。
- 解释 retry 管理的投递尝试，以及它不覆盖的业务补偿。
- 为每个成功、拒绝、失败和超时端口设计明确去向。

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

- **覆盖**：五类基础节点的 authoring 契约、Effect 出箱、等待恢复、审批决定与失败路由。
- **不覆盖**：固定并行、循环和动态 WorkItem，分别见第 12 章和第 14 章。
- **假设**：你已掌握第 10 章的数据契约与边绑定。

### 节点不是外部系统驱动器

`send_message`、`wait` 和 `request_approval` 会产生持久 Effect。
runtime 先把意图（Intent）、Effect 和状态变更持久化，再由分发器执行外部工作。
等待期间不应占住 worker、goroutine 或数据库事务；进程重启后，runtime 依据持久状态继续处理。

`set_variable` 是纯 runtime 状态更新，不调用外部系统。
`end` 是显式终点：`succeeded` 校验并交付 Plan 输出，`failed` 记录稳定错误码。
顶层成功 `end` 会立即把 Run 标记为成功，并取消其余 Activation 与开放作用域中的工作。
成功 `end` 若位于尚未闭合的 parallel 或 fan-out 分支内，runtime 会以 `run.invalid_branch_terminal` 失败，而不是把该分支当作顶层成功。

| 节点 | 成功端口 | 其他端口 | 关键含义 |
| --- | --- | --- | --- |
| `send_message` | `completed` | `failed` | `sent` 等待投递完成；`callback` 等待认证回调 |
| `set_variable` | `completed` | `failed` | 原子更新已声明的 Run 状态 |
| `wait` | `completed` | `failed`、`timed_out` | 唯一具有创作期 `timeout` 的动作节点 |
| `request_approval` | `approved` | `rejected`、`failed` | 首个有效决定生效，没有创作期超时 |
| `end` | 无 | 无 | 显式结束为成功或失败 |

### 消息的两种完成模式

`completion: sent` 表示消息投递效果完成，不表示收件人已读，也不表示消息中要求的业务工作已完成。
需要 Agent 产出结构化结果时，使用 `completion: callback`，声明 `responder`、工作范围和 `callbackOutput`。
runtime 把 callback capability 绑定到确切的 Run、Activation、Effect（callback service 内称 `CallbackID`；公共命令中为 `stepId`）和冻结响应者。
相同幂等键与相同结果可以安全重放；不同结果会冲突。
Run、Activation 或 Effect 已不再等待时，迟到 callback 会被 fence，不能复活旧工作。

回调内容不匹配 `callbackOutput` 时，完成请求被拒绝，Effect 保持等待，Agent 可以修正后再次提交。
这不是节点 retry。
retry 负责消息与审批等 Effect 的可重试投递失败；它不会自动重做 Agent 的业务判断，也不会替代补偿流程。
`wait.retry` 虽能进入编译产物，但当前 timer runtime 只按 `WakeAt` 与 `Deadline` 解析完成或超时，不消费该 retry 预算，因此新示例不依赖它。

> **⚠️ 注意：** retry 映射完全省略时，编译器冻结默认值 `maxAttempts: 1` 与 `backoff: PT1S`。一旦显式写 `retry`，两个字段都必须提供；`maxAttempts` 的上限是 100。

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

让 Agent 承担外部工作时，消息应同时说明输入、输出、边界和失败回报。

```text
检查指定变更，只读取工作范围内的文件。
返回 result 字符串，概括验证结论和阻断项。
不要修改未授权目录；无法完成时明确说明原因，不要伪造成功。
```

提示词只是消息正文，不会扩大 Agent 权限。
真正的目标范围由 `scope`、`responder`、`workScope`、`relatedScopes` 与运行时授权共同约束。

<!-- slot: 流程 -->
### 审批、Agent 回调与等待

下面的流程把基础节点串成一条可恢复链路：

1. `approve` 创建审批请求并等待人类决定。
2. 驳回进入稳定失败终点；通过后才向指定 Agent 发消息。
3. Agent 回调的 `result` 先写入 Run 状态。
4. `pause` 持久等待一段时间，完成后交付输出。
5. 消息、状态更新或等待失败进入统一错误终点；计时器超时进入单独错误终点。

请求审批 `request_approval` 没有创作期超时。
“两小时未批则提醒值班人”属于运营策略；如果必须自动实现，需要由外部监控发提醒，或在业务上改造为另一个可审计流程，不能给 `request_approval` 虚构 `timeout` 字段。

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

```yaml
apiVersion: loop.plan/v1
kind: PlanVersion
input:
  approver: {type: member, required: true}
  review_scope: {type: scope, required: true}
  worker: {type: agent, required: true}
output:
  result: {type: string, required: true}
state:
  final_result: {type: string, required: true, default: ""}
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: ask_agent
      type: send_message
      input:
        review_scope: {type: scope, required: true}
        worker: {type: agent, required: true}
      sendMessage:
        scope: {ref: input.review_scope}
        responder: {ref: input.worker}
        text: "检查变更并回传 result"
        completion: callback
        workScope: {ref: input.review_scope}
        callbackOutput:
          result: {type: string, required: true}
        retry: {maxAttempts: 3, backoff: PT2S}
    - id: save_result
      type: set_variable
      input:
        result: {type: string, required: true}
      setVariable:
        assignments:
          - state: final_result
            value: {ref: input.result}
    - id: pause
      type: wait
      wait:
        until: {value: PT1S}
        timeout: PT1M
    - id: success
      type: end
      end: {outcome: succeeded}
    - id: rejected
      type: end
      end: {outcome: failed, code: approval_rejected}
    - id: timed_failure
      type: end
      end: {outcome: failed, code: wait_deadline_reached}
    - id: failure
      type: end
      end: {outcome: failed, code: basic_node_failed}
  edges:
    - id: approved
      source: {nodeId: approve, port: approved}
      target: {nodeId: ask_agent, port: in}
      bindings:
        review_scope: {ref: plan.input.review_scope}
        worker: {ref: plan.input.worker}
    - id: rejected
      source: {nodeId: approve, port: rejected}
      target: {nodeId: rejected, port: in}
    - id: approval_failed
      source: {nodeId: approve, port: failed}
      target: {nodeId: failure, port: in}
    - id: agent_completed
      source: {nodeId: ask_agent, port: completed}
      target: {nodeId: save_result, port: in}
      bindings:
        result: {ref: output.result}
    - id: agent_failed
      source: {nodeId: ask_agent, port: failed}
      target: {nodeId: failure, port: in}
    - id: result_saved
      source: {nodeId: save_result, port: completed}
      target: {nodeId: pause, port: in}
    - id: save_failed
      source: {nodeId: save_result, port: failed}
      target: {nodeId: failure, port: in}
    - id: wait_completed
      source: {nodeId: pause, port: completed}
      target: {nodeId: success, port: in}
      bindings:
        result: {ref: state.final_result}
    - id: wait_failed
      source: {nodeId: pause, port: failed}
      target: {nodeId: failure, port: in}
    - id: wait_timed_out
      source: {nodeId: pause, port: timed_out}
      target: {nodeId: timed_failure, port: in}
```

节点端口来自 [`internal/workflow/plan/authoring_capabilities.go: compilerAuthoringCapabilities`](https://github.com/tidbcloud/AgentTeam/src/branch/main/internal/workflow/plan/authoring_capabilities.go)，retry 默认契约由 [`internal/workflow/plan/action_defaults_test.go: TestCompileDefaultsWhollyOmittedActionRetry`](https://github.com/tidbcloud/AgentTeam/src/branch/main/internal/workflow/plan/action_defaults_test.go) 验证。
[`internal/workflow/run/reducer.go: Kernel.execute`](https://github.com/tidbcloud/AgentTeam/src/branch/main/internal/workflow/run/reducer.go) 定义 `end`、状态更新和 Effect Intent；[`internal/workflow/orchestrator/dispatcher.go: EffectDispatcher.dispatchClaim`](https://github.com/tidbcloud/AgentTeam/src/branch/main/internal/workflow/orchestrator/dispatcher.go) 定义 timer、消息和审批的实际分发边界。
[`internal/workflow/orchestrator/dispatcher_timer_approval_test.go: TestEffectDispatcherCompletesTimerWithoutNetwork`](https://github.com/tidbcloud/AgentTeam/src/branch/main/internal/workflow/orchestrator/dispatcher_timer_approval_test.go) 证明 timer 直接按时间解析，不走网络投递重试。

### 审批决定与身份

`requestApproval.assignee` 在创作期是别名，运行时解析并冻结为唯一在岗人类成员。
审批决定只有 `approved` 和 `rejected`；服务按 Run、Activation、Effect、冻结 assignee、通知身份和客户端决定幂等键验证请求。
第一个经过认证的有效决定生效；只有相同决定、相同客户端决定幂等键，以及相同冻结 actor、notification、assignee 的精确重试才会重放，同决定但不同键或其他字段不同也会冲突，已经离开等待态的提交会被 fence。
运行内审批与团队 Workflow 发布审批是两个独立域，不应共享状态名称或业务解释。

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

| 现象 | 原因 | 处置 |
| --- | --- | --- |
| 消息投递永久失败 | writer 不可用、权限变化或 retry 耗尽 | 走 `failed` 端口，保留错误码并人工判断是否重跑 |
| 回调 Schema 不符 | Agent 少字段或类型错误 | 修正同一等待 Effect 的回调后重交 |
| 审批人无法解析 | 别名无唯一在岗人类成员 | 修正输入或组织成员配置 |
| 等待截止 | `wait` 未在 `timeout` 前完成 | 走超时端口，执行明确的失败或恢复设计 |
| 重复外部副作用 | 适配器把一次 Run / Effect 身份误作跨 Run 业务身份 | 使用可信输入或平台签发的业务幂等键去重，技术 ID 只关联审计 |

runtime 为同一 Effect 的消息重放复用稳定的 `clientMessageId`，并把 callback capability 绑定到确定的 Run、Activation、Effect（callback service 内称 `CallbackID`；公共命令中为 `stepId`）和冻结响应者。
外部系统仍必须实现跨消息、Invocation 与 Run 稳定的业务幂等：键至少包含业务对象、来源批准版本或业务事件、期望变化与目标，并且各部分来自可信调用输入或平台签发身份。Run、Activation、Effect 与 `clientMessageId` 只关联一次技术执行及审计，不能单独或组合充当跨 Run 业务键。

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

审批摘要应包含对象、变更、风险和决定后果，但不得把敏感凭据放入消息。
驳回应进入明确端口，不能与系统投递失败混为一谈。
审批响应时限、升级联系人和节假日值班是运营策略，应由监控和责任表执行；当前节点本身不保证这些 SLA。

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

Agent 通过已授权 App 或 Extension 操作外部系统，再提交 callback 输出。
Workflow 只编排消息、等待、审批和结果传递。
若外部写入成功但 callback 丢失，恢复流程应先按业务幂等键查询既有结果，再决定补交回调或执行人工补偿，不能盲目重复写入。

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

- [ ] 能解释 `sent` 与 `callback` 的完成边界。
- [ ] 能区分 Effect retry、回调修正重交和业务补偿。
- [ ] 能列出 `wait` 与 `request_approval` 的真实端口和超时边界。
- [ ] 每个非终止端口都有明确路由，失败 `end` 使用稳定错误码。
- [ ] 本章完整 Plan 原样编译后 `diagnostics=[]`。

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

<!-- media: fig-11-01-effect-lifecycle.png -->

建议图示展示 Intent 持久化、Effect 投递、等待、callback 或审批决定、归约器恢复和后续激活之间的关系。

### 小结

- [ ] 基础节点通过显式端口表达完成、拒绝、失败与超时。
- [ ] callback 等业务结果；消息与审批 retry 对首次 Effect 投递执行有界尝试，而当前 wait timer 不消费 retry 预算。
- [ ] 消息已可见且仅 receipt 丢失时，runtime 使用稳定 `clientMessageId` 继续恢复，不再受原 `maxAttempts` 限制。
- [ ] 外部副作用需要业务幂等键，SLA 与升级属于运营策略。

下一章把这些节点放进条件、并行与循环作用域。
