@fieldwangai/agentflow 0.1.159 → 0.1.160

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/bin/lib/catalog-flows.mjs +10 -1
  2. package/bin/lib/control-while.mjs +336 -0
  3. package/bin/lib/flow-dsl/codegen.mjs +74 -11
  4. package/bin/lib/flow-dsl/ir.mjs +33 -2
  5. package/bin/lib/flow-dsl/lint.mjs +128 -3
  6. package/bin/lib/flow-dsl/parser.mjs +168 -4
  7. package/bin/lib/marketplace.mjs +10 -2
  8. package/bin/lib/node-package-manifest.mjs +7 -2
  9. package/bin/lib/node-ui-kit.mjs +156 -0
  10. package/bin/lib/paths.mjs +2 -0
  11. package/bin/lib/workspace-flow-store.mjs +11 -2
  12. package/bin/lib/workspace-graph-merge.mjs +3 -0
  13. package/bin/lib/workspace-routes.mjs +10 -1
  14. package/bin/lib/workspace-server.mjs +592 -11
  15. package/bin/pipeline/validate-flow.mjs +3 -3
  16. package/builtin/nodes/control_parse_json.md +25 -0
  17. package/builtin/nodes/control_subflow_call.md +27 -0
  18. package/builtin/nodes/control_while.md +124 -0
  19. package/builtin/nodes/provide_json.md +14 -0
  20. package/builtin/nodes/workspace_subflow_input.md +14 -0
  21. package/builtin/pipelines/subflow-preview/workspace.flow.js +29 -0
  22. package/builtin/pipelines/subflow-preview/workspace.layout.json +38 -0
  23. package/builtin/pipelines/subflow-preview/workspace.nodes.json +19 -0
  24. package/builtin/pipelines/subflow-preview/workspace.state.json +93 -0
  25. package/builtin/pipelines/while-subflow-preview/workspace.flow.js +55 -0
  26. package/builtin/pipelines/while-subflow-preview/workspace.layout.json +66 -0
  27. package/builtin/pipelines/while-subflow-preview/workspace.nodes.json +49 -0
  28. package/builtin/pipelines/while-subflow-preview/workspace.state.json +194 -0
  29. package/builtin/web-ui/dist/assets/{WorkflowAssistantThread-CY19DsYq.js → WorkflowAssistantThread-Bfo9Ythw.js} +1 -1
  30. package/builtin/web-ui/dist/assets/index-XbeI5foV.js +872 -0
  31. package/builtin/web-ui/dist/assets/index-e1omCEau.css +1 -0
  32. package/builtin/web-ui/dist/index.html +2 -2
  33. package/package.json +2 -1
  34. package/reference/flow-control-capabilities.md +41 -16
  35. package/shared/slot-types.js +58 -0
  36. package/skills/agentflow-flow-dsl/SKILL.md +60 -6
  37. package/skills/agentflow-flow-dsl/references/node-calls.md +3 -0
  38. package/skills/agentflow-flow-dsl/references/subflow-authoring.md +230 -0
  39. package/skills/agentflow-node-dsl/SKILL.md +39 -0
  40. package/skills/agentflow-node-reference/references/builtin-nodes.md +25 -0
  41. package/builtin/web-ui/dist/assets/index-B9ppXv7e.css +0 -1
  42. package/builtin/web-ui/dist/assets/index-CdEQWRrp.js +0 -872
@@ -30,8 +30,8 @@
30
30
  @keyframes af-app-loading-slide { from { transform: translateX(-115%); } to { transform: translateX(250%); } }
31
31
  @media (prefers-reduced-motion: reduce) { .af-app-loading__track span { width: 100%; animation: none; } }
32
32
  </style>
33
- <script type="module" crossorigin src="/assets/index-CdEQWRrp.js"></script>
34
- <link rel="stylesheet" crossorigin href="/assets/index-B9ppXv7e.css">
33
+ <script type="module" crossorigin src="/assets/index-XbeI5foV.js"></script>
34
+ <link rel="stylesheet" crossorigin href="/assets/index-e1omCEau.css">
35
35
  </head>
36
36
  <body>
37
37
  <div id="root">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fieldwangai/agentflow",
3
- "version": "0.1.159",
3
+ "version": "0.1.160",
4
4
  "description": "Orchestration system for long-running complex agent tasks using Cursor, OpenCode, Claude Code, or Codex as execution backends",
5
5
  "type": "module",
6
6
  "main": "bin/agentflow.mjs",
@@ -40,6 +40,7 @@
40
40
  "bin",
41
41
  "agents",
42
42
  "skills",
43
+ "shared",
43
44
  "builtin/nodes",
44
45
  "builtin/pipelines",
45
46
  "builtin/web-ui/dist",
@@ -33,21 +33,45 @@
33
33
 
34
34
  ---
35
35
 
36
- ## 3. 图必须是 DAG(无环)
36
+ ## 3. 图必须是 DAG(无环),重复执行用 control_while
37
37
 
38
38
  Workspace 运行计划做拓扑排序,**遇到环直接抛 `Workspace run graph contains a cycle`**,整次运行失败。
39
39
 
40
40
  因此:
41
41
 
42
42
  - **禁止**从下游节点连边回到上游节点。
43
- - 「检查 → 修复 → 复检」要**向前展开**成多个节点,而不是回流成环。
44
- - 需要「反复重试直到达标」时,把循环放进**单个节点内部**——`agent_subAgent` 的 body 里让 agent 自己迭代,或 `tool_nodejs` 的脚本里自己 while 循环。
43
+ - 「检查 → 修复 → 复检」涉及不同角色时要**向前展开**成多个节点,而不是回流成环。
44
+ - 同一个确定性动作需要反复推进时,用 `control_while`。它在单节点状态机里重复 step 命令,
45
+ 父图仍是 DAG;`continue` 继续、`wait` 暂停且不跑下游、`done` 放行下游、`fail` 失败。
46
+
47
+ step 的 stdout 必须严格是一个 JSON 对象:
48
+
49
+ ```json
50
+ {"decision":"continue|wait|done|fail","state":{},"summary":"本轮摘要"}
51
+ ```
52
+
53
+ 循环上下文通过 `AGENTFLOW_WHILE_STATE`、绝对轮次 `AGENTFLOW_WHILE_ITERATION`、
54
+ `AGENTFLOW_WHILE_MAX_ITERATIONS`、`AGENTFLOW_WHILE_TIMEOUT_MS` 和稳定的逐轮
55
+ `AGENTFLOW_WHILE_IDEMPOTENCY_KEY` 注入。外部写操作应尽量把该幂等键传给目标 API。stdout
56
+ 留给决策对象,普通进度写 stderr。`maxIterations` 默认 20,`timeout` 默认 30m,二者在
57
+ `wait` 后恢复时继续累计;checkpoint 同时保留 state、history、已用执行时间和下一轮编号。
58
+ 输入变化会重置 checkpoint,指纹匹配但 checkpoint 损坏时会拒绝恢复,避免静默重放副作用。
45
59
 
46
60
  旧版用于成环的 `control_anyOne` / `control_toBool` / `control_agent_toBool` / `control_interval_loop` 均已下线。
47
61
 
48
62
  ---
49
63
 
50
- ## 4. 展示结果(Display
64
+ ## 4. 子流程(Subflow
65
+
66
+ 可复用的一段节点拓扑用 `flow.input`、`flow.subflow` 和 `flow.call` 表达。子流程内部继续使用
67
+ 标准 AgentFlow 节点与边,父流程只连接调用节点的契约引脚;禁止跨边界直接连内部节点。
68
+
69
+ 每次调用拥有独立 `callFrameId`,内部事件同时带 `parentNodeId` 和 `subflowId`。禁止递归调用。
70
+ 当前第一版不支持子流程内部 `wait/deferred`,遇到会明确失败;可恢复调用栈补齐后再开放。
71
+
72
+ ---
73
+
74
+ ## 5. 展示结果(Display)
51
75
 
52
76
  把产出槽连到 `display_*` 节点的 `content` 输入即可在画布上渲染:
53
77
 
@@ -66,7 +90,7 @@ Workspace 运行计划做拓扑排序,**遇到环直接抛 `Workspace run grap
66
90
 
67
91
  ---
68
92
 
69
- ## 5. 工具节点与 Agent 节点选型
93
+ ## 6. 工具节点与 Agent 节点选型
70
94
 
71
95
  **核心原则:能用工具节点确定性执行的,不要用 agent_subAgent。**
72
96
 
@@ -76,7 +100,7 @@ Workspace 运行计划做拓扑排序,**遇到环直接抛 `Workspace run grap
76
100
  | 向用户展示结果 | **display_\*** | 专用展示节点 |
77
101
  | 需要 AI 理解上下文、做判断、生成内容 | **agent_subAgent** | 需要 LLM 推理能力 |
78
102
 
79
- ### 5.1 tool_nodejs 直接执行模式(推荐)
103
+ ### 6.1 tool_nodejs 直接执行模式(推荐)
80
104
 
81
105
  在 instance 中设置 `script` 字段,运行时**跳过 AI 直接 spawn 命令**:
82
106
 
@@ -99,7 +123,7 @@ write_summary:
99
123
  - **成败判定**:以脚本进程 **exit code** 为准(0 = success,非 0 = failed)。
100
124
  - **stdout → result**:脚本 stdout 直接作为 result 槽位内容,纯文本即可。**不要用 JSON 封装 stdout。**
101
125
 
102
- ### 5.2 判断标准
126
+ ### 6.2 判断标准
103
127
 
104
128
  问自己:**"这个步骤的行为是否完全由输入决定,不需要 AI 推理?"**
105
129
 
@@ -108,7 +132,7 @@ write_summary:
108
132
  - **否** → 用 `agent_subAgent`
109
133
  - 例:根据需求撰写文档、分析代码并提出修改方案、理解上下文后做决策
110
134
 
111
- ### 5.3 `script` 与 `body` 的职责(必须遵守)
135
+ ### 6.3 `script` 与 `body` 的职责(必须遵守)
112
136
 
113
137
  | 字段 | 职责 | 有 `script` 时 | 无 `script` 时 |
114
138
  |------|------|---------------|---------------|
@@ -137,7 +161,7 @@ bad_example:
137
161
  调用 API 获取数据,解析 JSON,提取关键字段保存到文件
138
162
  ```
139
163
 
140
- ### 5.4 常见误用
164
+ ### 6.4 常见误用
141
165
 
142
166
  | 用户需求 | 错误做法 | 正确做法 |
143
167
  |----------|----------|----------|
@@ -146,7 +170,7 @@ bad_example:
146
170
  | 注入密钥 | 写死在 flow 里 | `tool_set_run_env` 或运行时环境变量 |
147
171
  | 复杂 AI 推理/生成 | tool_nodejs + body 写自然语言 | agent_subAgent(需 LLM 能力时必须用 agent) |
148
172
 
149
- ### 5.5 节点单一职责(必须遵守)
173
+ ### 6.5 节点单一职责(必须遵守)
150
174
 
151
175
  **每个节点只做一件事,工作内容保持专注和专一。**
152
176
 
@@ -167,17 +191,18 @@ bad_example:
167
191
 
168
192
  ---
169
193
 
170
- ## 6. 常见流程模式
194
+ ## 7. 常见流程模式
171
195
 
172
196
  1. **线性链**:Run → A → B → … → display
173
197
  2. **条件分支**:… → provide_bool → **control_if** → next1 连分支A、next2 连分支B(true 走 output-0,false 走 output-1)
174
- 3. **并行**:同一个 output 扇出到多个下游节点,无依赖的分支会并行执行,各自往下走
175
- 4. **检查修复 复检**:向前排成 检查节点 → 修复节点 → 复检节点 → control_if(通过走后续,不通过走汇报节点)。**不要连回上游**
176
- 5. **批量任务**:拆解节点产出 `- [ ]` 清单 执行节点在内部逐项推进control_if 按完成情况分支
198
+ 3. **控制扇出**:同一个 output 扇出到多个下游节点;当前 Workspace 运行时按拓扑序串行执行
199
+ 4. **单步收敛**:Runcontrol_while(重复同一 step)→ 下游;wait 时停在 While
200
+ 5. **检查修复复检**:不同角色向前排成多个节点。**不要连回上游**
201
+ 6. **批量任务**:拆解节点产出清单 → control_while 每轮推进一项 → done 后汇总
177
202
 
178
203
  ---
179
204
 
180
- ## 7. Edge 与 Handle 注意点
205
+ ## 8. Edge 与 Handle 注意点
181
206
 
182
207
  - **Fan-out 允许,Fan-in 禁止**:一个 output handle 可连多个 input(扇出),但**一个 input handle 只允许一条入边**(禁止扇入)。同一 `target + targetHandle` 不得出现在多条 edge 中——运行时仅取首条匹配,其余静默丢失。若需替换连线,先删旧边再加新边。
183
208
  - **禁止回流边**:任何从下游连回上游的边都会让运行计划判定成环,整次运行失败。
@@ -188,6 +213,6 @@ bad_example:
188
213
 
189
214
  ---
190
215
 
191
- ## 8. 图与 USER_PROMPT 的读写一致性
216
+ ## 9. 图与 USER_PROMPT 的读写一致性
192
217
 
193
218
  ${USER_PROMPT} 中描述的「读取」「写入」应与图中的 **handler 节点**(input/output 通过 edge 连接的节点)对应:描述的每项「读」应有节点的 input 入边,每项「写」应有节点的 output 出边。详见 [flow-prompt-handler-check.md](./flow-prompt-handler-check.md)。
@@ -0,0 +1,58 @@
1
+ const SLOT_TYPE_ALIASES = new Map([
2
+ ["文本", "text"],
3
+ ["str", "text"],
4
+ ["string", "text"],
5
+ ["文件", "file"],
6
+ ["path", "file"],
7
+ ["节点", "node"],
8
+ ["flow", "node"],
9
+ ["control", "node"],
10
+ ["boolean", "bool"],
11
+ ["布尔", "bool"],
12
+ ["object", "json"],
13
+ ]);
14
+
15
+ /**
16
+ * Lossless assignments that do not need an adapter node.
17
+ * JSON can always be serialized as text; text cannot safely become JSON
18
+ * without parsing and validation, so the reverse direction is deliberately
19
+ * absent.
20
+ */
21
+ const LOSSLESS_ASSIGNMENTS = new Set([
22
+ "json>text",
23
+ ]);
24
+
25
+ export const SLOT_TYPE_COLORS = Object.freeze({
26
+ text: "#2196f3",
27
+ file: "#4caf50",
28
+ node: "#ff9800",
29
+ bool: "#9c27b0",
30
+ json: "#00bcd4",
31
+ image: "#ec407a",
32
+ any: "#9e9e9e",
33
+ });
34
+
35
+ export function normalizeSlotType(type) {
36
+ const raw = String(type ?? "").trim().toLowerCase();
37
+ return SLOT_TYPE_ALIASES.get(raw) || raw || "node";
38
+ }
39
+
40
+ export function slotTypeCompatibility(sourceType, targetType) {
41
+ const source = normalizeSlotType(sourceType);
42
+ const target = normalizeSlotType(targetType);
43
+ if (!source || !target) return { compatible: false, kind: "incompatible", source, target };
44
+ if (source === "any" || target === "any") return { compatible: true, kind: "any", source, target };
45
+ if (source === target) return { compatible: true, kind: "exact", source, target };
46
+ if (LOSSLESS_ASSIGNMENTS.has(`${source}>${target}`)) {
47
+ return { compatible: true, kind: "lossless", source, target };
48
+ }
49
+ return { compatible: false, kind: "incompatible", source, target };
50
+ }
51
+
52
+ export function areSlotTypesCompatible(sourceType, targetType) {
53
+ return slotTypeCompatibility(sourceType, targetType).compatible;
54
+ }
55
+
56
+ export function getSlotTypeColor(type) {
57
+ return SLOT_TYPE_COLORS[normalizeSlotType(type)] || "#9e9e9e";
58
+ }
@@ -2,8 +2,8 @@
2
2
  name: agentflow-flow-dsl
3
3
  description: >-
4
4
  用 workspace.flow.js(受限 ESM)编写 AgentFlow Workspace 流程图。适用于新建流程、
5
- 修改流程结构、添加或删除节点、修改连线、控制顺序、分支和定时入口;自定义代码节点包
6
- 改用 agentflow-node-dsl。
5
+ 修改流程结构、添加或删除节点、修改连线、控制顺序、分支、子流程、受控循环和定时入口;
6
+ 自定义代码节点包改用 agentflow-node-dsl。
7
7
  ---
8
8
 
9
9
  # AgentFlow Flow DSL
@@ -78,7 +78,8 @@ export const run = flow("Run", collect, analyse, chart);
78
78
  |------|------|
79
79
  | 第 1 个参数(字符串) | 节点显示名。可省略 |
80
80
  | 第 2 个参数(对象) | **输入引脚**。每个键都是引脚名,没有例外 |
81
- | 第 3 个参数(字符串) | body:agent 是 prompt,`tool.nodejs` shell 命令 |
81
+ | 第 3 个参数 | 一般是 body;`control.while` 优先传 Condition 子流程,旧模式才传 step 脚本 |
82
+ | 第 4 个参数 | 仅双子流程 `control.while` 使用:Body 子流程 |
82
83
  | `const 变量名` | **节点 id**。改名 = 重命名节点 |
83
84
  | `x.slotName` | 引用上游输出引脚 = 连一条数据线 |
84
85
 
@@ -91,7 +92,7 @@ export const run = flow("Run", collect, analyse, chart);
91
92
  3. **结构文件禁一切控制流**:`if` / `for` / `while` / `?:` / `.map()` / `await` /
92
93
  箭头函数 / `new` / 动态属性。要写逻辑就建代码节点
93
94
  4. 一个输出可接多个输入(fan-out 允许);**一个输入只能接一条边**(fan-in 禁止);
94
- 不能成环,没有循环原语
95
+ 图不能成环;重复执行用 `control.while`,不要画回边
95
96
 
96
97
  ## 控制流
97
98
 
@@ -116,6 +117,38 @@ flow.resume(showPlan, stage2); // 闸门:跑到这停,人点第二
116
117
  flow.detached(draftA, draftB); // 有控制链但没 run 入口
117
118
  ```
118
119
 
120
+ ### 同 Workspace 子流程
121
+
122
+ 需要把一段标准节点拓扑复用为可调用单元时,用 `flow.input` + `flow.subflow` 声明契约,
123
+ 再由父流程用 `flow.call` 调用。子流程内部仍然是 AgentFlow 节点和连线,不要包装成一个脚本:
124
+
125
+ 创建、修改或验收普通子流程、`control.while` 或其画布投影时,必须读取
126
+ [子流程与 While 编写规范](references/subflow-authoring.md)。该规范同时约束 DSL 契约、状态流、
127
+ Start / Return 边界投影和父子流程引脚映射;不要手工创建边界节点或输入代理连线。
128
+
129
+ ```js
130
+ const stateIn = flow.input("state", "json");
131
+ const inspect = agent.subAgent("检查下一项", { state: stateIn.value }, `只处理一项并返回 JSON`);
132
+ const save = tool.nodejs("规范化状态", { value: inspect.result }, `node ${flowDir}/scripts/normalize.mjs`);
133
+
134
+ export const advanceOne = flow.subflow(
135
+ "推进一项",
136
+ { state: stateIn },
137
+ flow(inspect, save),
138
+ { state: save.result },
139
+ );
140
+
141
+ const advance = flow.call("调用推进子流程", advanceOne, { state: read.result });
142
+ const { state } = advance;
143
+ export const run = flow("Run", read, advance, show);
144
+ ```
145
+
146
+ - `flow.input(name, type)` 只能作为某个子流程的输入代理。
147
+ - `flow.subflow(label, inputs, flow(...), outputs)` 的输入值引用 `flow.input`,输出值引用内部节点输出。
148
+ - `flow.call(label, subflow, pins)` 是父流程里的真实控制节点,动态引脚由契约生成。
149
+ - 禁止父流程和内部节点直接跨边界连线;所有值必须经过 `flow.call`。
150
+ - 禁止递归调用。当前第一版也不允许子流程内部 `wait/deferred`;调用帧恢复能力补齐前会明确失败。
151
+
119
152
  **`flow.fork` 不是并行。** 它是「一个 `next` 接多个下游」的写法——`flow(a, b, c)` 是线性的,
120
153
  没法在链里写出扇出,所以有了它。编译出来就是两条边,图里不存在 fork 这个东西:
121
154
 
@@ -131,7 +164,25 @@ build.next → testB.prev
131
164
 
132
165
  **分支不能汇合**——fan-in 禁止,`control.anyOne` 运行时没实现。两条分支各自收尾。
133
166
 
134
- **没有循环**。「改到通过为止」只能展开成固定轮次的嵌套 gate。
167
+ ### 受控循环
168
+
169
+ **图仍然不能成环。** 重复执行时用 `control.while` 把循环收进一个有上限、超时和 checkpoint 的
170
+ 状态机节点;不要画回边,也不要用 DSL 的 JavaScript `while`。优先传入显式 Condition/Body 子流程:
171
+
172
+ ```js
173
+ const advance = control.while("推进到人工边界", {
174
+ state: initial.value,
175
+ maxIterations: "20",
176
+ timeout: "30m",
177
+ }, shouldContinue, advanceOne);
178
+ export const run4 = flow("Run", advance, report); // done 才继续;wait 会暂停
179
+ ```
180
+
181
+ - Condition 固定输出 `decision`,Body 固定输出下一版 `state`;两者都可选输出 `summary`。
182
+ - `state` 是用户可见的业务状态载体。`iteration`、`idempotencyKey` 由运行时注入,保留在 DSL
183
+ 契约中,但不要要求普通用户在画布上配置或连接。
184
+ - `maxIterations` 和 `timeout` 必须显式设置。新流程不要使用旧的脚本式 While,除非用户要求兼容。
185
+ - Condition/Body 的完整声明、状态迁移、固定输出和手动画布编辑方式都在专项规范中;不要凭记忆简写。
135
186
 
136
187
  `control.agentToBool` 是 `runtime: degraded`:它靠通用 agent 路径工作,没有任何东西
137
188
  约束模型输出,而 `parse-bool` 只认 `true` / `1` / `yes` / `on`。prompt 里必须写死
@@ -207,8 +258,11 @@ const doc = display.html("使用说明", { content: file("docs/guide.html") });
207
258
  | 给用户看结果 | `display.markdown` / `.code` / `.html` / `.chart` / `.table` |
208
259
  | 加载 skills 给下游 agent | `control.loadSkills` → `skillsContext` |
209
260
  | 加载知识库 / 代码仓 | `control.cdWorkspace` → `knowledgeContext` |
210
- | 固定文本 / 密钥 | `provide.str` / `provide.password` |
261
+ | 固定文本 / JSON / 密钥 | `provide.str` / `provide.json` / `provide.password` |
262
+ | 文本显式解析为 JSON | `control.parseJson` |
211
263
  | 文本转 bool 做分支 | `control.agentToBool` → `prediction` |
264
+ | 复用一段标准节点拓扑 | `flow.subflow` + `flow.call` |
265
+ | 重复执行显式条件与单轮拓扑 | `control.while(..., conditionFlow, bodyFlow)` |
212
266
 
213
267
  `display.*` 的内容一般来自连线;写字面量则是手写文档节点(也合法,且不会被运行覆盖)。
214
268
 
@@ -12,7 +12,9 @@
12
12
  | `control.if` | prediction:bool | — |
13
13
  | `control.loadMcp` | serverNames:text | mcpContext:text |
14
14
  | `control.loadSkills` | skillKeys:text | skillsContext:text |
15
+ | `control.parseJson` | value:text | result:json |
15
16
  | `control.userWorkspace` | — | workspaceContext:text, cwd:file |
17
+ | `control.while` | state:json, maxIterations:text, timeout:text | result:json, state:json, decision:text, iterations:text, summary:text, history:json, checkpointFingerprint:text |
16
18
  | `display.ascii` | content:text | content:text |
17
19
  | `display.chart` | content:text, filePath:file, workspaceContext:text | content:text |
18
20
  | `display.code` | content:text, language:text, fileName:text, wrap:bool | content:text |
@@ -24,6 +26,7 @@
24
26
  | `display.table` | content:text, filePath:file, workspaceContext:text | content:text |
25
27
  | `provide.bool` | — | value:bool |
26
28
  | `provide.file` | — | value:file |
29
+ | `provide.json` | — | value:json |
27
30
  | `provide.password` | — | value:text |
28
31
  | `provide.str` | — | value:text |
29
32
  | `tool.displayShareLink` | title:text, layout:text, nodeIds:text, baseUrl:text | url:text, shareId:text, expiresAt:text |
@@ -0,0 +1,230 @@
1
+ # 子流程与 `control.while` 编写规范
2
+
3
+ ## 目录
4
+
5
+ 1. 选型
6
+ 2. 普通子流程 DSL
7
+ 3. While 的状态模型
8
+ 4. While DSL 契约
9
+ 5. 用户可见画布语义
10
+ 6. 手动编辑方式
11
+ 7. 运行与 Preview 验收
12
+
13
+ ## 1. 选型
14
+
15
+ 按以下边界选型:
16
+
17
+ - 复用一段标准 AgentFlow 节点拓扑时,用 `flow.subflow` + `flow.call`。
18
+ - 按状态重复执行同一套 Condition/Body 拓扑时,用 `control.while`。
19
+ - 封装一个确定性计算或外部工具调用时,用代码节点,不要伪装成子流程。
20
+ - 多个角色、阶段或业务分支只执行一次时,继续使用父图中的普通节点和控制流,不要全部塞进 While。
21
+
22
+ 子流程内部仍是普通 AgentFlow 节点。父流程通过显式输入/输出契约调用它,不得直接连接内部节点。
23
+
24
+ ## 2. 普通子流程 DSL
25
+
26
+ 按 `flow.input`、`flow.subflow`、`flow.call` 三层声明:
27
+
28
+ ```js
29
+ const issueIn = flow.input("issue", "text");
30
+ const inspect = agent.subAgent(
31
+ "分析单个 Issue",
32
+ { issue: issueIn.value },
33
+ `只分析本次输入的一个 Issue,给出目标、风险和下一步。`,
34
+ );
35
+ const normalize = agent.subAgent(
36
+ "规范化交付摘要",
37
+ { analysis: inspect.result },
38
+ `输出父流程可直接展示的摘要。`,
39
+ );
40
+
41
+ export const inspectIssue = flow.subflow(
42
+ "Issue 单项分析",
43
+ { issue: issueIn },
44
+ flow(inspect, normalize),
45
+ { summary: normalize.result, raw: inspect.result },
46
+ );
47
+
48
+ const callInspect = flow.call(
49
+ "调用 Issue 分析子流程",
50
+ inspectIssue,
51
+ { issue: pendingIssue.value },
52
+ );
53
+ const { summary, raw } = callInspect;
54
+ export const run = flow("Run", callInspect, showSummary);
55
+ ```
56
+
57
+ 遵守以下约束:
58
+
59
+ - 每个契约输入绑定一个 `flow.input(name, type)`,内部消费者读取它的 `.value`。
60
+ - `flow.subflow(label, inputs, flow(...), outputs)` 的第三个参数是非空内部控制链。
61
+ - 每个契约输出绑定内部成员节点的输出槽。
62
+ - `flow.call(label, subflow, pins)` 是父流程里的真实执行节点;动态数据引脚来自子流程契约。
63
+ - 一个内部节点只能属于一个子流程。禁止直接跨边界连线、跨子流程连线和递归调用。
64
+ - 当前子流程内部不接受 `wait/deferred`。需要暂停 While 时,由 Condition 返回 `wait`。
65
+
66
+ 普通子流程的输出契约可以变化。画布编辑器中把某个内部数据输出拖到 Return 的 `add output`,再命名
67
+ 输出;重命名或删除后,所有父图 `SUBFLOW CALL` 的同名输出及相关连线必须同步迁移或移除。
68
+
69
+ ## 3. While 的状态模型
70
+
71
+ 把 While 理解为以下状态迁移,而不是一条隐藏的回边:
72
+
73
+ ```text
74
+ 初始 state₀
75
+ → Condition(stateₙ)
76
+ ├─ continue → Body(stateₙ) → stateₙ₊₁ → 下一轮 Condition
77
+ ├─ wait → 保存 checkpoint,暂停 Run
78
+ ├─ done → 结束 While,继续父流程下游
79
+ └─ fail → While 失败
80
+ ```
81
+
82
+ 只把业务上需要跨轮保留的数据放入 `state`,例如:
83
+
84
+ ```json
85
+ {
86
+ "cursor": 2,
87
+ "records": [{ "id": "U-001" }, { "id": "U-002" }],
88
+ "valid": [{ "id": "U-001" }],
89
+ "invalid": []
90
+ }
91
+ ```
92
+
93
+ 每轮 Body 必须返回完整的下一版 `state`。业务变量不要增加成 While 顶层输出,也不要依赖隐藏的
94
+ 局部变量;把它们作为 `state` 的 JSON 字段显式更新。
95
+
96
+ `iteration` 是运行时绝对轮次,`idempotencyKey` 是逐轮稳定幂等键。它们属于执行上下文,不是业务
97
+ 状态:DSL 契约必须声明,产品画布默认隐藏,普通用户不需要连接或配置。只有高级 DSL/代码节点确实
98
+ 需要轮次或外部写幂等时,才在内部消费它们。
99
+
100
+ ## 4. While DSL 契约
101
+
102
+ 优先使用显式 Condition/Body 子流程:
103
+
104
+ ```js
105
+ const conditionState = flow.input("state", "json");
106
+ const conditionIteration = flow.input("iteration", "text");
107
+ const check = agent.subAgent(
108
+ "判断是否继续",
109
+ { state: conditionState.value },
110
+ `检查 state。只返回 continue、wait、done 或 fail 之一。`,
111
+ );
112
+
113
+ export const shouldContinue = flow.subflow(
114
+ "是否继续",
115
+ { state: conditionState, iteration: conditionIteration },
116
+ flow(check),
117
+ { decision: check.result },
118
+ );
119
+
120
+ const bodyState = flow.input("state", "json");
121
+ const bodyIteration = flow.input("iteration", "text");
122
+ const bodyKey = flow.input("idempotencyKey", "text");
123
+ const step = tool.nodejs(
124
+ "推进一轮",
125
+ { state: bodyState.value, idempotencyKey: bodyKey.value },
126
+ `node ${flowDir}/scripts/advance-one.mjs`,
127
+ );
128
+ const nextState = control.parseJson("校验下一版状态", { value: step.result });
129
+
130
+ export const advanceOne = flow.subflow(
131
+ "执行一轮",
132
+ { state: bodyState, iteration: bodyIteration, idempotencyKey: bodyKey },
133
+ flow(step, nextState),
134
+ { state: nextState.result },
135
+ );
136
+
137
+ const initial = provide.json("初始状态", {
138
+ value: "{\"cursor\":0,\"records\":[],\"valid\":[],\"invalid\":[]}",
139
+ });
140
+ const loop = control.while("逐条处理", {
141
+ state: initial.value,
142
+ maxIterations: "20",
143
+ timeout: "30m",
144
+ }, shouldContinue, advanceOne);
145
+
146
+ const summarize = agent.subAgent("生成最终摘要", { state: loop.state }, `把最终 state 整理成 Markdown。`);
147
+ const report = display.markdown("最终结果", { content: summarize.result });
148
+ export const run = flow("Run", loop, summarize, report);
149
+ ```
150
+
151
+ 固定契约如下:
152
+
153
+ | 子流程 | DSL 输入 | DSL 输出 | 产品画布中用户需要理解的部分 |
154
+ |--------|----------|----------|--------------------------------|
155
+ | Condition | `state:json`, `iteration:text` | 必需 `decision:text`;可选 `summary:text` | `state → decision` |
156
+ | Body | `state:json`, `iteration:text`, `idempotencyKey:text` | 必需 `state:json`;可选 `summary:text` | `stateₙ → stateₙ₊₁` |
157
+
158
+ While Return 是固定契约:Condition 只能返回 `decision/summary`,Body 只能返回 `state/summary`。
159
+ 不要在 While Return 添加任意顶层变量。普通 Subflow Return 才支持动态输出。
160
+
161
+ 只在兼容旧流程时使用脚本式 While:
162
+
163
+ ```js
164
+ const loop = control.while("推进", {
165
+ state: initial.value,
166
+ maxIterations: "20",
167
+ timeout: "30m",
168
+ }, `node ${flowDir}/scripts/advance-one.mjs`);
169
+ ```
170
+
171
+ 旧脚本 stdout 只能是 `{"decision":"continue|wait|done|fail","state":{},"summary":"..."}`;日志写
172
+ stderr。新流程不要用脚本替代 Condition/Body 子流程。
173
+
174
+ ## 5. 用户可见画布语义
175
+
176
+ 保持“运行时完整、产品上不暴露噪音”:
177
+
178
+ - 父图把普通调用显示为独立的 **SUBFLOW CALL** 节点,动态输入/输出与声明契约同名。
179
+ - 父图把 While 显示为状态机卡片:左侧 `state` 是初始业务状态输入,右侧 `state` 是最终业务状态。
180
+ - While 卡片中的 Condition/Body 是可进入的子流程入口;`maxIterations` 和 `timeout` 可直接配置。
181
+ - While 卡片明确展示 `stateₙ → check → run → stateₙ₊₁`,运行后展示当前 state、最新结果和历史。
182
+ - `CHECKS`、`RUNS`、`CALLS` 虚线只表示调用关系,由系统生成,不是数据线或控制线,也不能手拉。
183
+ - 点击 Condition、Body 或普通 Subflow 卡片进入独立子流程编辑模式;不要删除父图的调用关系视图。
184
+ - 子图显示可拖动的 **SUBFLOW START** 和 **SUBFLOW RETURN**,复用普通节点的 Port Rail、Handle、
185
+ 选中和拖动规则。
186
+ - While 的 Start 只展示业务 `state`;隐藏 `iteration/idempotencyKey`。它们仍留在 DSL/IR 中由运行时注入。
187
+ - While Return 显示固定锁定契约。Body Return 有可用值时展示 state 字段、类型和预览,并可定位来源节点。
188
+ - 普通 Subflow Return 显示 `add output`,允许新增、重命名和删除契约输出。
189
+ - `workspace_subflow_input` 是 IR 输入代理,画布不重复显示;Start 的同名输出线代表它的真实绑定。
190
+
191
+ React 组件名、CSS 类名和像素尺寸属于实现细节,不写入 DSL。
192
+
193
+ ## 6. 手动编辑方式
194
+
195
+ 创建 While 后按以下方式操作:
196
+
197
+ 1. 从普通 JSON 数据节点的输出引脚连接到 While 左侧 `state`。
198
+ 2. 在 While 卡片中设置最大轮次和超时。
199
+ 3. 点击 Condition 卡片进入编辑器,添加判断节点;连接 Start `next` 到首节点 `prev`,连接 Start
200
+ `state` 到判断节点数据输入,连接末节点 `next` 到 Return `prev`,再把结果连到 Return `decision`。
201
+ 4. 点击 Body 卡片进入编辑器,连接 Start `state` 到单轮处理节点,最后把完整下一版 JSON 状态连到
202
+ Return `state`;文本结果先经过 `control.parseJson`。
203
+ 5. 需要摘要时连接可选 `summary`。不要寻找或手工连接 `iteration/idempotencyKey` 产品引脚。
204
+ 6. 返回父图,确认 Condition/Body 卡片、状态流说明和两条调用虚线仍存在。
205
+
206
+ 创建普通子流程时,Start/Return 的控制线和数据线方式相同;区别是 Return 输出可以通过 `add output`
207
+ 扩展,父图 SUBFLOW CALL 会同步出现同名输出。
208
+
209
+ ## 7. 运行与 Preview 验收
210
+
211
+ 完成 DSL 后执行:
212
+
213
+ ```bash
214
+ agentflow flow dsl lint <flowDir>
215
+ agentflow flow dsl layout <flowDir>
216
+ ```
217
+
218
+ 涉及子流程或 While 时,不能只凭 lint、单元测试或 DSL 文本宣称完成。打开真实 Workspace Preview:
219
+
220
+ 1. 确认父流程只通过 SUBFLOW CALL 或 While 调用子流程,没有直接跨边界线。
221
+ 2. 确认调用虚线稳定落到对应 Start,且不会被误读成 `next` 控制线。
222
+ 3. 确认 Start/Return 可拖动,保存刷新后位置和契约不丢失。
223
+ 4. 确认普通子流程多输出在 Return 与 Call 两侧逐项对应。
224
+ 5. 确认 While 只暴露业务 state,Condition/Body 固定输出正确,运行时字段没有泄露到普通 UI。
225
+ 6. 确认节点高度随内容收敛,没有大面积空白、越界历史或重叠引脚。
226
+ 7. 实际运行至少两轮,验证 `state` 确实变化;`continue` 才启动 Body,`done` 继续下游,`wait` 保存
227
+ checkpoint 且不触发下游。
228
+ 8. 重新运行等待中的 While,确认轮次、累计超时、历史和幂等键不重置。
229
+
230
+ 任何一项失败,都先修正 DSL、投影或运行时,再重新 lint、测试和预览。
@@ -115,6 +115,45 @@ await fs.writeFile(p, csvText); await fs.writeFile(outputs.deduped, p); // ✗
115
115
 
116
116
  可用类型:`text` `file` `bool` `node` `image` `json`。未知类型直接报错。
117
117
 
118
+ ## Node UI Kit(可选)
119
+
120
+ 节点需要在画布上解释输入绑定、执行步骤或运行状态时,在 `export default` 中声明 `ui.card`。
121
+ UI 与清单一起静态解析和发布;节点包不能注入 React、HTML 或事件处理器,只能组合平台白名单
122
+ 组件:`binding`、`code`、`decision`、`metrics`、`summary`、`history`。
123
+
124
+ ```js
125
+ export default {
126
+ id: "advance_state",
127
+ version: "1.0.0",
128
+ name: "推进状态",
129
+ inputs: { state: { type: "json" }, max: { type: "text", default: "20" } },
130
+ outputs: { decision: { type: "text" }, iterations: { type: "text" }, history: { type: "json" } },
131
+ ui: {
132
+ card: {
133
+ template: "state-machine", // details | state-machine
134
+ icon: "repeat", // Material Symbols 名称
135
+ tone: "purple", // neutral | blue | purple | green | amber | red
136
+ sections: [
137
+ { type: "binding", label: "State", input: "state" },
138
+ { type: "code", label: "Step", field: "script" },
139
+ { type: "decision", label: "Decision", output: "decision", source: "step.result.decision",
140
+ options: [
141
+ { value: "continue", label: "continue", tone: "purple" },
142
+ { value: "done", label: "done", tone: "green" },
143
+ ] },
144
+ { type: "metrics", label: "Progress", items: [
145
+ { label: "Iteration", output: "iterations", maxInput: "max" },
146
+ ] },
147
+ { type: "history", label: "History", output: "history", limit: 3 },
148
+ ],
149
+ },
150
+ },
151
+ };
152
+ ```
153
+
154
+ 字段里的 `input` / `output` 必须引用本节点已声明的槽位;`code.field` 只能是 `script`、
155
+ `scriptRef`、`body`、`implementationRef`。未知组件或字段会在清单规范化时被丢弃,不会进入 UI。
156
+
118
157
  ## `run(inputs, outputs, dirs)`
119
158
 
120
159
  | 参数 | 内容 |