## 输入、输出、变量与上下文

Workflow 的图并不靠节点名称“猜”数据。
每个值都来自声明过的契约，并在图入口、边绑定或节点配置中显式引用。
本章把这些数据通道拆开，帮助你在设计前回答三个问题：值从哪里来、在哪个作用域可见、完成时交付什么。

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

学完本章，你将能够：

- 区分 Plan 的 `input`、`output`、Run 的 `state` 与节点 `input`。
- 在 `value`、`ref`、`expr` 和字符串模板之间选择正确写法。
- 使用边 `bindings` 传递有类型的数据，不把节点 `id` 当作变量根。
- 通过编译诊断发现缺失引用、类型不匹配和并行状态冲突。

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

- **覆盖**：`plan.Document` 的数据契约、基础引用空间、模板与表达式、边绑定、状态更新。
- **不覆盖**：Agent 回调的协作字段与 WorkItem 契约，分别见第 13 章和第 14 章。
- **假设**：你已理解工作流（Workflow，代码名 `Plan`）、调用（Invocation）、运行（Run）和节点（Node）的关系。

### 四层数据各负其责

顶层 `input` 是一次 Run 的不可变调用输入。
调用开始前，runtime 按 Schema 校验它；图入口使用 `plan.input.*` 把值注入第一个节点。
顶层 `output` 是成功完成时必须满足的结果契约。
成功 `end` 的输入即 Plan 输出，缺少必填字段或类型不匹配会在编译期被拒绝。

顶层 `state` 是 Run 内可持久更新的有类型状态。
只有 `set_variable` 能写入已声明的状态字段，而且一次激活中的多项赋值读取同一个旧状态快照并原子提交。
它不能创建未声明路径，也不能改写 Plan 输入、节点输出或循环元数据。

节点 `input` 是一次激活（Activation）的局部输入。
图入口的 `input` 映射和边的 `bindings` 负责构造它。
节点离开当前激活后，后续节点不应依赖某个节点 `id` 形成的隐式命名空间；需要跨节点保留的值应进入 `state`，或沿下一条边显式绑定。

| 数据层 | 常用引用 | 生命周期 | 写入方式 |
| --- | --- | --- | --- |
| Plan 输入 | `plan.input.request` | 整个 Run 不变 | Invocation 提交 |
| 激活输入 | `input.request` | 当前激活 | 图入口或边绑定 |
| Run 状态 | `state.normalized` | 整个 Run 可更新 | `set_variable` |
| 节点输出 | `output.result` | 适用节点成功端口 | callback 或 `join` 产生 |

> **⚠️ 注意：** `output.*` 不是“任意上游节点的输出集合”。它只在当前源节点确实暴露输出的端口可用，例如 callback 模式的 `send_message`、开启线程后的消息结果，以及 `join` 的 `completed` 端口。

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

本章不涉及自然语言创建。
评审数据流时，可以把下面的问题交给作者逐项回答：

```text
逐条列出这个 Plan 的调用输入、持久状态、每个节点输入和最终输出。
对每条边说明来源引用、目标字段和类型。
标出任何依赖节点 id、到达顺序或未声明字段的隐式传值。
```

### 三种值表达式与一种模板

`ValueExpr` 只有三种互斥形态。
`value` 写字面量，`ref` 读取一个有类型的引用，`expr` 计算一个有类型的表达式。
节点的消息文本与审批摘要使用 `{{ ... }}` 字符串插值；`when`、`expr` 和 `ref` 内不加花括号。

表达式支持 `||`、`&&`、`!`、比较运算、`in`，以及 `contains`、`startsWith`、`endsWith`、`length`。
基础引用根是 `input`、`output`、`state`、`plan`、`loop`、`branches` 和 `winner`。
`item.*` 与 `items` 是受作用域限制的直接引用：前者只在 `fan_out` 的 item 边绑定中可用，后者只在配对 `join` 的 `merge` 中可用。

```yaml
# 字面量
duration: {value: PT30S}
# 直接引用
request: {ref: plan.input.request}
# 表达式
is_large: {expr: "length(input.request) >= 200"}
```

> **❌ 反例：** `ref: normalize.result` 把节点 `id` 当作数据根；`when: input.ready and input.safe` 使用了不受支持的单词运算符。

> **✅ 正例：** 把结果写入 `state.normalized`，再用 `ref: state.normalized`；条件写成 `input.ready && input.safe`。

<!-- slot: 流程 -->
### 从调用输入到最终输出

下面的流程把一个请求保存到 Run 状态，再发送摘要并完成。
数据路径是显式的：

1. 图入口把 `plan.input.request` 注入 `normalize.input.request`。
2. `set_variable` 把激活输入原子写入 `state.normalized`。
3. 完成边把状态和调用级 `scope` 绑定到 `notify`。
4. 消息完成后，成功 `end` 从状态构造 Plan 输出。
5. 任一动作失败都进入稳定错误码 `data_flow_failed`。

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

```yaml
apiVersion: loop.plan/v1
kind: PlanVersion
input:
  request: {type: string, required: true}
  dispatch_scope: {type: scope, required: true}
output:
  summary: {type: string, required: true}
state:
  normalized: {type: string, required: true, default: ""}
graph:
  entry:
    nodeId: normalize
    port: in
    input:
      request: {ref: plan.input.request}
  nodes:
    - id: normalize
      type: set_variable
      input:
        request: {type: string, required: true}
      setVariable:
        assignments:
          - state: normalized
            value: {ref: input.request}
    - id: notify
      type: send_message
      input:
        dispatch_scope: {type: scope, required: true}
        summary: {type: string, required: true}
      sendMessage:
        scope: {ref: input.dispatch_scope}
        text: "已登记请求：{{ input.summary }}"
        completion: sent
        retry: {maxAttempts: 2, backoff: PT1S}
    - id: success
      type: end
      end: {outcome: succeeded}
    - id: failure
      type: end
      end: {outcome: failed, code: data_flow_failed}
  edges:
    - id: normalized
      source: {nodeId: normalize, port: completed}
      target: {nodeId: notify, port: in}
      bindings:
        dispatch_scope: {ref: plan.input.dispatch_scope}
        summary: {ref: state.normalized}
    - id: normalize_failed
      source: {nodeId: normalize, port: failed}
      target: {nodeId: failure, port: in}
    - id: notified
      source: {nodeId: notify, port: completed}
      target: {nodeId: success, port: in}
      bindings:
        summary: {ref: state.normalized}
    - id: notify_failed
      source: {nodeId: notify, port: failed}
      target: {nodeId: failure, port: in}
```

这个示例以 [`internal/workflow/plan/types.go: Document / Schema / ValueExpr / Edge`](https://github.com/tidbcloud/AgentTeam/src/branch/main/internal/workflow/plan/types.go) 为契约。
[`internal/workflow/run/reducer.go: Kernel.execute`](https://github.com/tidbcloud/AgentTeam/src/branch/main/internal/workflow/run/reducer.go) 先基于同一个旧状态求出所有 `set_variable` 赋值，整体通过 Run 状态契约后才替换 `state.Variables`；[`internal/workflow/run/reducer_test.go: TestSetVariableIsAtomicAndUsesOldStateSnapshot`](https://github.com/tidbcloud/AgentTeam/src/branch/main/internal/workflow/run/reducer_test.go) 覆盖这一原子快照语义。
类型可赋值与表达式编译分别由 [`internal/workflow/plan/schema_contract_test.go: TestSchemaAssignabilityRetainsArrayEnumAndNominalContracts`](https://github.com/tidbcloud/AgentTeam/src/branch/main/internal/workflow/plan/schema_contract_test.go) 和 [`internal/workflow/plan/ir_expression_test.go: TestCompileArtifactValueExpr_DirectReferenceAndExpressionHaveIdenticalAST`](https://github.com/tidbcloud/AgentTeam/src/branch/main/internal/workflow/plan/ir_expression_test.go) 覆盖。

### 省略绑定不是动态复制

当源契约与目标契约能够被编译器证明为安全的整体透传时，入口映射或边 `bindings` 可以省略。
这不是运行时按名称随意复制，也不会绕过必填性和类型检查。
企业指南中的示例优先保留关键绑定，让 Review 能直接看出数据来源；只在透传关系明显且已由编译器证明时省略。

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

| 现象 | 原因 | 处置 |
| --- | --- | --- |
| `binding.required_target` | 目标节点的必填输入没有绑定 | 补齐入口映射或边绑定 |
| `binding.type_mismatch` | 引用类型不能赋给目标 Schema | 修正 Schema 或增加显式转换设计 |
| 引用不可用 | 在错误端口或作用域读取 `output`、`loop`、`item` 等根 | 把读取移动到该作用域允许的绑定位置 |
| 并行状态诊断 | 两个可同时运行的分支访问同一状态，且至少一个分支写入 | 改为分支结果经 `join` 显式合并；只读与只读可以并行 |

编译诊断发生在发布或显式校验阶段，不应通过运行时试错绕过。
若业务数据本身不满足调用输入 Schema，应拒绝本次 Invocation，而不是用默认空字符串掩盖问题。

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

本章不展开 `request_approval`。
请求审批 `request_approval` 的输入仍遵守同一套激活输入和模板规则；它的等待、决定和失败端口见第 11 章。

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

Plan 不直接发起任意 HTTP 请求。
需要读取外部系统时，由 Agent 在 callback 工作中调用已授权的 App 或 Extension，再把声明过的业务字段返回给 Workflow。
回调输出应先通过 Schema 校验，再由边 `bindings` 构造后续节点输入。
需要跨节点持久化时，后续节点再用 `set_variable` 写入已声明的 `state` 字段。

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

- [ ] 能为任意值指出其 `plan.input`、`input`、`state` 或适用的 `output` 来源。
- [ ] 能解释为什么节点 `id` 不是变量根。
- [ ] 能为每条边核对目标必填字段与来源类型。
- [ ] 能用 `set_variable` 保存跨节点状态，并避免并行读写竞争。
- [ ] 本章完整 Plan 原样编译后 `diagnostics=[]`。

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

<!-- media: fig-10-01-data-scopes.png -->

建议图示把 Plan 输入、激活输入、Run 状态、节点输出和边绑定画成五个不同区域，并标出各引用根的可见范围。

### 小结

- [ ] Plan 输入不可变，Run 状态只能按声明更新，节点输入由入口或边构造。
- [ ] `value`、`ref`、`expr` 和模板各有明确用途。
- [ ] 类型、必填性与引用作用域都由编译器检查。

下一章逐一解释基础节点的端口、等待和失败语义。
