@fieldwangai/agentflow 0.1.25

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 (138) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +201 -0
  3. package/README.zh-CN.md +201 -0
  4. package/agents/agentflow-node-executor-code.md +32 -0
  5. package/agents/agentflow-node-executor-planning.md +32 -0
  6. package/agents/agentflow-node-executor-requirement.md +32 -0
  7. package/agents/agentflow-node-executor-test.md +32 -0
  8. package/agents/agentflow-node-executor-ui.md +32 -0
  9. package/agents/agentflow-node-executor.md +32 -0
  10. package/agents/agents.json +8 -0
  11. package/agents/en/agentflow-node-executor.md +32 -0
  12. package/agents/zh/agentflow-node-executor.md +32 -0
  13. package/bin/agentflow.mjs +52 -0
  14. package/bin/ensure-workspace-reference.mjs +35 -0
  15. package/bin/lib/agent-runners.mjs +1199 -0
  16. package/bin/lib/agents-path.mjs +61 -0
  17. package/bin/lib/api-runner.mjs +361 -0
  18. package/bin/lib/apply.mjs +852 -0
  19. package/bin/lib/catalog-agents.mjs +300 -0
  20. package/bin/lib/catalog-flows.mjs +532 -0
  21. package/bin/lib/composer-agent.mjs +884 -0
  22. package/bin/lib/composer-flow-instances.mjs +68 -0
  23. package/bin/lib/composer-flow-skeleton.mjs +334 -0
  24. package/bin/lib/composer-flow-validate.mjs +47 -0
  25. package/bin/lib/composer-log.mjs +197 -0
  26. package/bin/lib/composer-model-router.mjs +160 -0
  27. package/bin/lib/composer-node-schema.mjs +299 -0
  28. package/bin/lib/composer-planner.mjs +749 -0
  29. package/bin/lib/composer-script-ops.mjs +233 -0
  30. package/bin/lib/composer-skill-router.mjs +384 -0
  31. package/bin/lib/flow-import.mjs +305 -0
  32. package/bin/lib/flow-normalize.mjs +71 -0
  33. package/bin/lib/flow-write.mjs +395 -0
  34. package/bin/lib/help.mjs +139 -0
  35. package/bin/lib/hub-login.mjs +54 -0
  36. package/bin/lib/hub-publish.mjs +159 -0
  37. package/bin/lib/hub-remote.mjs +189 -0
  38. package/bin/lib/hub.mjs +299 -0
  39. package/bin/lib/i18n.mjs +233 -0
  40. package/bin/lib/locales/en.json +344 -0
  41. package/bin/lib/locales/zh.json +344 -0
  42. package/bin/lib/log.mjs +37 -0
  43. package/bin/lib/main.mjs +611 -0
  44. package/bin/lib/model-config.mjs +118 -0
  45. package/bin/lib/model-lists.mjs +188 -0
  46. package/bin/lib/node-exec-context.mjs +336 -0
  47. package/bin/lib/node-execute.mjs +513 -0
  48. package/bin/lib/normalize-node-tool-command.mjs +97 -0
  49. package/bin/lib/paths.mjs +216 -0
  50. package/bin/lib/pipeline-scripts.mjs +41 -0
  51. package/bin/lib/recent-runs.mjs +173 -0
  52. package/bin/lib/run-apply-active-lock.mjs +82 -0
  53. package/bin/lib/run-events.mjs +85 -0
  54. package/bin/lib/run-node-statuses-from-disk.mjs +85 -0
  55. package/bin/lib/schedule-config.mjs +227 -0
  56. package/bin/lib/scheduler.mjs +312 -0
  57. package/bin/lib/table.mjs +4 -0
  58. package/bin/lib/terminal.mjs +42 -0
  59. package/bin/lib/ui-print.mjs +94 -0
  60. package/bin/lib/ui-server.mjs +2113 -0
  61. package/bin/lib/workspace-tree.mjs +266 -0
  62. package/bin/lib/workspace.mjs +180 -0
  63. package/bin/pipeline/build-node-prompt.mjs +179 -0
  64. package/bin/pipeline/check-cache.mjs +191 -0
  65. package/bin/pipeline/check-flow.mjs +543 -0
  66. package/bin/pipeline/collect-nodes.mjs +212 -0
  67. package/bin/pipeline/compute-cache-md5.mjs +177 -0
  68. package/bin/pipeline/ensure-run-dir.mjs +71 -0
  69. package/bin/pipeline/extract-thinking.mjs +308 -0
  70. package/bin/pipeline/gc.mjs +129 -0
  71. package/bin/pipeline/get-env.mjs +83 -0
  72. package/bin/pipeline/get-exec-id.mjs +145 -0
  73. package/bin/pipeline/get-ready-nodes.mjs +435 -0
  74. package/bin/pipeline/get-resolved-values.mjs +337 -0
  75. package/bin/pipeline/load-key.mjs +62 -0
  76. package/bin/pipeline/parse-bool.mjs +33 -0
  77. package/bin/pipeline/parse-flow.mjs +698 -0
  78. package/bin/pipeline/post-process-control-if.mjs +23 -0
  79. package/bin/pipeline/post-process-node.mjs +490 -0
  80. package/bin/pipeline/pre-process-node.mjs +449 -0
  81. package/bin/pipeline/resolve-inputs.mjs +201 -0
  82. package/bin/pipeline/run-log.mjs +34 -0
  83. package/bin/pipeline/run-tool-nodejs.mjs +160 -0
  84. package/bin/pipeline/save-key.mjs +93 -0
  85. package/bin/pipeline/snapshot-prior-round.mjs +70 -0
  86. package/bin/pipeline/validate-flow.mjs +825 -0
  87. package/bin/pipeline/validate-for-ui.mjs +226 -0
  88. package/bin/pipeline/validate-script-output.mjs +130 -0
  89. package/bin/pipeline/write-result.mjs +182 -0
  90. package/builtin/nodes/agent_subAgent.md +14 -0
  91. package/builtin/nodes/control_agent_toBool.md +20 -0
  92. package/builtin/nodes/control_anyOne.md +17 -0
  93. package/builtin/nodes/control_end.md +11 -0
  94. package/builtin/nodes/control_if.md +20 -0
  95. package/builtin/nodes/control_start.md +11 -0
  96. package/builtin/nodes/control_toBool.md +21 -0
  97. package/builtin/nodes/provide_file.md +11 -0
  98. package/builtin/nodes/provide_str.md +11 -0
  99. package/builtin/nodes/tool_get_env.md +14 -0
  100. package/builtin/nodes/tool_load_key.md +20 -0
  101. package/builtin/nodes/tool_nodejs.md +40 -0
  102. package/builtin/nodes/tool_print.md +14 -0
  103. package/builtin/nodes/tool_save_key.md +20 -0
  104. package/builtin/nodes/tool_user_ask.md +23 -0
  105. package/builtin/nodes/tool_user_check.md +22 -0
  106. package/builtin/pipelines/module-migrate/flow.yaml +819 -0
  107. package/builtin/pipelines/module-migrate/scripts/check_imports.mjs +700 -0
  108. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Makefile +362 -0
  109. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/.deps/Release/obj.target/node_modules/node-addon-api/node_addon_api_except.stamp.d +1 -0
  110. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/.deps/Release/obj.target/tree_sitter_kotlin_binding/bindings/node/binding.o.d +17 -0
  111. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/.deps/Release/obj.target/tree_sitter_kotlin_binding/src/parser.o.d +5 -0
  112. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/.deps/Release/obj.target/tree_sitter_kotlin_binding/src/scanner.o.d +8 -0
  113. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/.deps/Release/tree_sitter_kotlin_binding.node.d +1 -0
  114. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/obj.target/node_modules/node-addon-api/node_addon_api_except.stamp +0 -0
  115. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/obj.target/tree_sitter_kotlin_binding/bindings/node/binding.o +0 -0
  116. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/obj.target/tree_sitter_kotlin_binding/src/parser.o +0 -0
  117. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/obj.target/tree_sitter_kotlin_binding/src/scanner.o +0 -0
  118. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/Release/tree_sitter_kotlin_binding.node +0 -0
  119. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/binding.Makefile +6 -0
  120. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/gyp-mac-tool +768 -0
  121. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/node_modules/node-addon-api/node_addon_api.Makefile +6 -0
  122. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/node_modules/node-addon-api/node_addon_api.target.mk +122 -0
  123. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/node_modules/node-addon-api/node_addon_api_except.target.mk +126 -0
  124. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/node_modules/node-addon-api/node_addon_api_maybe.target.mk +122 -0
  125. package/builtin/pipelines/module-migrate/scripts/node_modules/tree-sitter-kotlin/build/tree_sitter_kotlin_binding.target.mk +203 -0
  126. package/builtin/pipelines/new/flow.yaml +545 -0
  127. package/builtin/pipelines/new/scripts/check-flow.mjs +9 -0
  128. package/builtin/pipelines/new/scripts/collect-nodes.mjs +211 -0
  129. package/builtin/pipelines/scripts/adjust-node-positions.mjs +113 -0
  130. package/builtin/web-ui/dist/agentflow-icon.svg +23 -0
  131. package/builtin/web-ui/dist/assets/index-CZkUPcXE.css +1 -0
  132. package/builtin/web-ui/dist/assets/index-DkkhNESc.js +190 -0
  133. package/builtin/web-ui/dist/index.html +24 -0
  134. package/package.json +67 -0
  135. package/reference/flow-control-capabilities.md +274 -0
  136. package/reference/flow-layout.md +84 -0
  137. package/reference/flow-prompt-handler-check.md +12 -0
  138. package/reference/flow-result-semantics.md +14 -0
@@ -0,0 +1,435 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 根据 flow.json 和最新 intermediate/*.result.md 计算当前可执行的节点。
4
+ * 入口处先执行 cache 校验(集成 check-cache 逻辑),再计算就绪节点,保证每次取就绪列表前缓存已更新。
5
+ * 用法:node get-ready-nodes.mjs <workspaceRoot> <flowName> <uuid>
6
+ * 输出(stdout JSON):{ "readyNodes": [...], "instanceStatus": {...}, "allDone": true|false, "pendingNodes": [...] }
7
+ * pendingNodes:status 为 pending 的节点 id 列表,供主流程区分「暂停」与「卡住」。
8
+ */
9
+
10
+ import fs from "fs";
11
+ import path from "path";
12
+ import { fileURLToPath } from "url";
13
+ import { spawnSync } from "child_process";
14
+
15
+ import { getRunDir } from "../lib/paths.mjs";
16
+ import { loadAllExecIds, latestResultExecId, intermediateResultBasename } from "./get-exec-id.mjs";
17
+ import { logToRunTag } from "./run-log.mjs";
18
+
19
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
20
+
21
+ /** 从 result.md 中解析 status(匹配 YAML frontmatter 中的 status: xxx) */
22
+ function parseResultStatus(filePath) {
23
+ try {
24
+ const raw = fs.readFileSync(filePath, "utf-8");
25
+ const m = raw.match(/^\s*status:\s*["']?(\w+)["']?/m);
26
+ return m ? m[1] : null;
27
+ } catch {
28
+ return null;
29
+ }
30
+ }
31
+
32
+ /** 从 result.md 中解析 branch(control_if 用)。值为节点类型的 output 槽位名(如 next1、next2)或布尔 "true"/"false"。 */
33
+ function parseResultBranch(filePath) {
34
+ try {
35
+ const raw = fs.readFileSync(filePath, "utf-8");
36
+ const m = raw.match(/^\s*branch:\s*["']?([^"'\s]+)["']?/m);
37
+ return m ? m[1] : null;
38
+ } catch {
39
+ return null;
40
+ }
41
+ }
42
+
43
+ /** control_if:将 branch 值(槽位名 next1/next2 或 true/false)映射为出边 sourceHandle(output-0 / output-1) */
44
+ function controlIfBranchToSourceHandle(branch) {
45
+ if (!branch) return null;
46
+ const b = String(branch).toLowerCase();
47
+ if (b === "next1" || b === "true") return "output-0";
48
+ if (b === "next2" || b === "false") return "output-1";
49
+ return null;
50
+ }
51
+
52
+ /** tool_user_ask:将 branch 值映射为出边 sourceHandle。
53
+ * branch 可为:槽位名(如 "option_1")、纯数字字符串("1")或 "output-1" 形式。
54
+ * 槽位名映射依赖 instance 的 output 顺序(与 outputSlotTypes 的 key 顺序一致)。 */
55
+ function userAskBranchToSourceHandle(branch, outputSlotTypesForNode) {
56
+ if (!branch) return null;
57
+ const s = String(branch).trim();
58
+ if (/^output-\d+$/.test(s)) return s;
59
+ if (/^\d+$/.test(s)) return `output-${parseInt(s, 10)}`;
60
+ if (outputSlotTypesForNode && typeof outputSlotTypesForNode === "object") {
61
+ const names = Object.keys(outputSlotTypesForNode);
62
+ const idx = names.indexOf(s);
63
+ if (idx >= 0) return `output-${idx}`;
64
+ }
65
+ // 兜底:option_N 形式
66
+ const m = s.match(/^option_(\d+)$/i);
67
+ if (m) return `output-${parseInt(m[1], 10)}`;
68
+ return null;
69
+ }
70
+
71
+ function main() {
72
+ const args = process.argv.slice(2);
73
+ if (args.length < 3) {
74
+ console.error(
75
+ JSON.stringify({
76
+ ok: false,
77
+ error: "Usage: node get-ready-nodes.mjs <workspaceRoot> <flowName> <uuid>",
78
+ }),
79
+ );
80
+ process.exit(1);
81
+ }
82
+
83
+ const [root, flowName, uuid] = args;
84
+ const workspaceRoot = path.resolve(root);
85
+
86
+ /** 先执行 cache 校验,再计算就绪节点,避免主流程漏调 check-cache */
87
+ const checkCachePath = path.join(__dirname, "check-cache.mjs");
88
+ const cacheResult = spawnSync(
89
+ process.execPath,
90
+ [checkCachePath, workspaceRoot, flowName, uuid],
91
+ { encoding: "utf-8", stdio: ["inherit", "pipe", "inherit"] },
92
+ );
93
+ if (cacheResult.status !== 0) {
94
+ console.error(cacheResult.stdout || "check-cache failed");
95
+ process.exit(cacheResult.status ?? 1);
96
+ }
97
+ logToRunTag(workspaceRoot, flowName, uuid, "get-ready-nodes", { event: "check-cache-done", status: cacheResult.status });
98
+
99
+ const runDir = getRunDir(workspaceRoot, flowName, uuid);
100
+ const flowJsonPath = path.join(runDir, "intermediate", "flow.json");
101
+ const intermediateDir = path.join(runDir, "intermediate");
102
+
103
+ if (!fs.existsSync(flowJsonPath)) {
104
+ console.error(
105
+ JSON.stringify({
106
+ ok: false,
107
+ error: `flow.json not found: ${flowJsonPath}. Run parse-flow.mjs first.`,
108
+ }),
109
+ );
110
+ process.exit(1);
111
+ }
112
+
113
+ let flow;
114
+ try {
115
+ flow = JSON.parse(fs.readFileSync(flowJsonPath, "utf-8"));
116
+ if (!flow.ok) {
117
+ console.error(JSON.stringify({ ok: false, error: flow.error || "flow.json indicates error" }));
118
+ process.exit(1);
119
+ }
120
+ } catch (e) {
121
+ console.error(JSON.stringify({ ok: false, error: e.message }));
122
+ process.exit(1);
123
+ }
124
+
125
+ try {
126
+ const {
127
+ order,
128
+ edges = [],
129
+ predecessors: rawPredecessors,
130
+ nodeDefinitions = {},
131
+ outputSlotTypes = {},
132
+ inputSlotTypes = {},
133
+ } = flow;
134
+ const execIdMap = loadAllExecIds(workspaceRoot, flowName, uuid, order);
135
+ if (!order || !Array.isArray(order)) {
136
+ console.error(JSON.stringify({ ok: false, error: "flow.json missing order" }));
137
+ process.exit(1);
138
+ }
139
+
140
+ /** 若 flow.json 无 predecessors,从 edges 构建 */
141
+ let predecessors = rawPredecessors;
142
+ if (!predecessors || typeof predecessors !== "object") {
143
+ predecessors = {};
144
+ for (const e of edges) {
145
+ if (!e.target) continue;
146
+ if (!predecessors[e.target]) predecessors[e.target] = [];
147
+ predecessors[e.target].push(e.source);
148
+ }
149
+ }
150
+
151
+ /** 判断边是否为「节点」边:两端槽位类型均为「节点」;图由 edge 搭建,后继只看节点边,前驱用全部边。 */
152
+ const isNodeEdge = (e) => {
153
+ const outTypes = outputSlotTypes[e.source];
154
+ const inTypes = inputSlotTypes[e.target];
155
+ if (!outTypes || !inTypes) return false;
156
+ const outNames = Object.keys(outTypes);
157
+ const inNames = Object.keys(inTypes);
158
+ const oidx = parseInt(String(e.sourceHandle || "output-0").replace("output-", ""), 10) || 0;
159
+ const iidx = parseInt(String(e.targetHandle || "input-0").replace("input-", ""), 10) || 0;
160
+ const outType = outNames[oidx] != null ? outTypes[outNames[oidx]] : null;
161
+ const inType = inNames[iidx] != null ? inTypes[inNames[iidx]] : null;
162
+ return (outType === "节点" || outType === "node") && (inType === "节点" || inType === "node");
163
+ };
164
+
165
+ /** 后继:仅「节点」边(执行顺序由节点边决定);用于从 control_start 的可达集与 execId 同步 */
166
+ const successors = {};
167
+ for (const e of edges) {
168
+ if (!e.source || e.target == null) continue;
169
+ if (!isNodeEdge(e)) continue;
170
+ if (!successors[e.source]) successors[e.source] = [];
171
+ successors[e.source].push(e.target);
172
+ }
173
+
174
+ const starts = order.filter((id) => nodeDefinitions[id] === "control_start");
175
+ /** 游标:当前执行前沿;缺省为 starts,并写回 flow.json */
176
+ const hadPendingFromFile = Array.isArray(flow.pendingInstances) && flow.pendingInstances.length > 0;
177
+ let pendingInstances = hadPendingFromFile ? flow.pendingInstances : starts;
178
+ if (!hadPendingFromFile) {
179
+ flow.pendingInstances = pendingInstances;
180
+ fs.writeFileSync(flowJsonPath, JSON.stringify(flow), "utf-8");
181
+ }
182
+ logToRunTag(workspaceRoot, flowName, uuid, "get-ready-nodes", {
183
+ event: "pendingInstances-init",
184
+ pendingInstances,
185
+ usedStarts: !hadPendingFromFile,
186
+ });
187
+
188
+ const reachableFromStart = new Set();
189
+ const queue = [...starts];
190
+ while (queue.length) {
191
+ const id = queue.shift();
192
+ if (reachableFromStart.has(id)) continue;
193
+ reachableFromStart.add(id);
194
+ for (const next of successors[id] || []) {
195
+ if (!reachableFromStart.has(next)) queue.push(next);
196
+ }
197
+ }
198
+
199
+ /** 参与集:从 start 可达的节点 + 这些节点的所有前驱(递归)。下游依赖的节点(如 loadFlowKey)即使无入边也会被纳入,从而可被就绪判定。 */
200
+ const needToRun = new Set(reachableFromStart);
201
+ let changed = true;
202
+ while (changed) {
203
+ changed = false;
204
+ for (const id of needToRun) {
205
+ for (const p of predecessors[id] || []) {
206
+ if (!needToRun.has(p)) {
207
+ needToRun.add(p);
208
+ changed = true;
209
+ }
210
+ }
211
+ }
212
+ }
213
+
214
+ /** 刷新 instanceStatus:仅对参与集 needToRun 读 result,后续只用这些节点的 status */
215
+ const instanceStatus = {};
216
+ for (const instanceId of needToRun) {
217
+ const execId = execIdMap[instanceId] ?? 1;
218
+ const latestE = latestResultExecId(execId);
219
+ const resultPath = latestE
220
+ ? path.join(intermediateDir, instanceId, intermediateResultBasename(instanceId, latestE))
221
+ : null;
222
+ let status = resultPath && fs.existsSync(resultPath) ? parseResultStatus(resultPath) : null;
223
+ if (!status && (nodeDefinitions[instanceId] || "").startsWith("provide_")) {
224
+ status = "success";
225
+ }
226
+ /* control_start 在流程开头即执行且由 CLI 写 result,无 result 时也视为 success 避免角标缺失;control_end 只有流程真正跑完才会执行并写 result,不能无 result 就标 success */
227
+ if (!status && nodeDefinitions[instanceId] === "control_start") {
228
+ status = "success";
229
+ }
230
+ if (status) instanceStatus[instanceId] = status;
231
+ }
232
+ logToRunTag(workspaceRoot, flowName, uuid, "get-ready-nodes", {
233
+ event: "instanceStatus",
234
+ instanceStatus,
235
+ order,
236
+ });
237
+
238
+ /** 入边:target -> [{ source, sourceHandle }, ...],用于 control_if 分支判断 */
239
+ const inEdgesByTarget = {};
240
+ for (const e of edges) {
241
+ if (!e.target) continue;
242
+ if (!inEdgesByTarget[e.target]) inEdgesByTarget[e.target] = [];
243
+ inEdgesByTarget[e.target].push({ source: e.source, sourceHandle: e.sourceHandle || "output-0" });
244
+ }
245
+
246
+ /** 更新 pendingInstances:result 为 success 的从游标移除,并沿节点边向后移动(control_if 只加 branch 匹配的那条后继) */
247
+ const nextPending = new Set(pendingInstances);
248
+ for (const id of pendingInstances) {
249
+ if (instanceStatus[id] !== "success") continue;
250
+ nextPending.delete(id);
251
+ if (nodeDefinitions[id] === "control_if" || nodeDefinitions[id] === "tool_user_ask") {
252
+ const predLatestE = latestResultExecId(execIdMap[id] ?? 1);
253
+ const resultPath = predLatestE
254
+ ? path.join(intermediateDir, id, intermediateResultBasename(id, predLatestE))
255
+ : null;
256
+ const branch = resultPath && fs.existsSync(resultPath) ? parseResultBranch(resultPath) : null;
257
+ const expectedHandle = nodeDefinitions[id] === "control_if"
258
+ ? controlIfBranchToSourceHandle(branch)
259
+ : userAskBranchToSourceHandle(branch, outputSlotTypes[id]);
260
+ /** 同一 branch 可连多条节点边(如 true 同时到 save_key 与 anyOne),须全部加入游标,不可只用 find 取第一条 */
261
+ const outEdges = edges.filter(
262
+ (e) => e.source === id && isNodeEdge(e) && (e.sourceHandle || "output-0") === expectedHandle,
263
+ );
264
+ for (const outEdge of outEdges) {
265
+ if (outEdge.target != null) nextPending.add(outEdge.target);
266
+ }
267
+ } else {
268
+ for (const next of successors[id] || []) nextPending.add(next);
269
+ }
270
+ }
271
+ const movedSuccess = pendingInstances.filter((id) => instanceStatus[id] === "success");
272
+ const added = [...nextPending].filter((n) => !pendingInstances.includes(n));
273
+ pendingInstances = Array.from(nextPending);
274
+ const pendingSet = new Set(pendingInstances);
275
+ logToRunTag(workspaceRoot, flowName, uuid, "get-ready-nodes", {
276
+ event: "pendingInstances-update",
277
+ pendingInstances,
278
+ movedSuccess,
279
+ added,
280
+ });
281
+
282
+ /** 候选集:从 pendingInstances 沿 successors 能到达的所有节点(闭包);再扩展为包含「阻塞当前 pending 的」前驱(沿 predecessors 反向闭包),
283
+ * 以便将可运行但未在 frontier 的前驱加入 readyNodes,避免 readyNodes 为空导致卡住(如 LoadFile 的前驱未在 pending 时)。 */
284
+ const candidateSet = new Set();
285
+ let candQueue = [...pendingInstances];
286
+ while (candQueue.length) {
287
+ const id = candQueue.shift();
288
+ if (candidateSet.has(id)) continue;
289
+ candidateSet.add(id);
290
+ for (const next of successors[id] || []) {
291
+ if (!candidateSet.has(next)) candQueue.push(next);
292
+ }
293
+ }
294
+ candQueue = [...candidateSet];
295
+ while (candQueue.length) {
296
+ const id = candQueue.shift();
297
+ for (const p of predecessors[id] || []) {
298
+ if (candidateSet.has(p)) continue;
299
+ candidateSet.add(p);
300
+ candQueue.push(p);
301
+ }
302
+ }
303
+ logToRunTag(workspaceRoot, flowName, uuid, "get-ready-nodes", {
304
+ event: "candidateSet",
305
+ size: candidateSet.size,
306
+ candidateSet: [...candidateSet],
307
+ });
308
+
309
+ /** 判断 predecessor P 对 target N 是否算「就绪」:普通节点看 status;control_if / tool_user_ask 看 branch 与出边 sourceHandle */
310
+ const isPredecessorReadyFor = (predSource, predDefId, targetId) => {
311
+ if (instanceStatus[predSource] !== "success") return false;
312
+ if (predDefId !== "control_if" && predDefId !== "tool_user_ask") return true;
313
+ const inEdges = inEdgesByTarget[targetId] || [];
314
+ const edge = inEdges.find((ie) => ie.source === predSource);
315
+ if (!edge) return true;
316
+ const predLatestE = latestResultExecId(execIdMap[predSource] ?? 1);
317
+ const resultPath = predLatestE
318
+ ? path.join(intermediateDir, predSource, intermediateResultBasename(predSource, predLatestE))
319
+ : null;
320
+ const branch = resultPath && fs.existsSync(resultPath) ? parseResultBranch(resultPath) : null;
321
+ const expectedHandle = predDefId === "control_if"
322
+ ? controlIfBranchToSourceHandle(branch)
323
+ : userAskBranchToSourceHandle(branch, outputSlotTypes[predSource]);
324
+ return expectedHandle != null && (edge.sourceHandle || "output-0") === expectedHandle;
325
+ };
326
+
327
+ const terminalStatuses = ["success", "failed", "condition_not_met", "pending"];
328
+ const isRunnable = (nodeId) => {
329
+ const preds = predecessors[nodeId] || [];
330
+ if (preds.length === 0) return true;
331
+ const defId = nodeDefinitions[nodeId];
332
+ return defId === "control_anyOne"
333
+ ? preds.some((p) => isPredecessorReadyFor(p, nodeDefinitions[p], nodeId))
334
+ : preds.every((p) => isPredecessorReadyFor(p, nodeDefinitions[p], nodeId));
335
+ };
336
+
337
+ /** 将「阻塞当前 pending 的可运行前驱」加入 pendingInstances(并 pendingSet),写回 flow.json,使本轮回或下一轮能将其加入 readyNodes。 */
338
+ const blockingRunnablePreds = [];
339
+ for (const p of candidateSet) {
340
+ if (pendingSet.has(p)) continue;
341
+ if (terminalStatuses.includes(instanceStatus[p])) continue;
342
+ if (!needToRun.has(p)) continue;
343
+ if (!isRunnable(p)) continue;
344
+ const blocksSomePending = [...pendingSet].some((n) => (predecessors[n] || []).includes(p));
345
+ if (!blocksSomePending) continue;
346
+ blockingRunnablePreds.push(p);
347
+ }
348
+ if (blockingRunnablePreds.length > 0) {
349
+ for (const p of blockingRunnablePreds) {
350
+ pendingInstances.push(p);
351
+ pendingSet.add(p);
352
+ }
353
+ logToRunTag(workspaceRoot, flowName, uuid, "get-ready-nodes", {
354
+ event: "pendingInstances-supplement",
355
+ added: blockingRunnablePreds,
356
+ });
357
+ }
358
+
359
+ /** 计算 readyNodes:仅考虑参与集 needToRun 内的节点;且必须在 pendingInstances 中(游标前沿);predecessor 满足条件且自身未达终态。
360
+ * 遍历范围限定为从 pendingInstances 沿边向后的候选集 candidateSet,不再全图遍历。
361
+ * control_anyOne:至少有一个前驱 P 满足「P 已 success 且(若 P 为 control_if)P→N 的边与 P 的 branch 一致」。
362
+ * control_if 仅解锁与 branch 匹配的那条出边所连的后继。pending 视为终态。
363
+ * cache_not_met 由 check-cache.mjs 写入(prompt/input 变更导致缓存失效),非终态,节点可重新进入待执行。 */
364
+ const readyNodes = [];
365
+ for (const instanceId of candidateSet) {
366
+ if (!pendingSet.has(instanceId)) continue;
367
+ if (!needToRun.has(instanceId)) continue;
368
+ const preds = predecessors[instanceId] || [];
369
+ const definitionId = nodeDefinitions[instanceId];
370
+ const allPredsReady =
371
+ definitionId === "control_anyOne"
372
+ ? preds.some((p) => isPredecessorReadyFor(p, nodeDefinitions[p], instanceId))
373
+ : preds.every((p) => isPredecessorReadyFor(p, nodeDefinitions[p], instanceId));
374
+ if (!allPredsReady) continue;
375
+ /** 规则 1:仅 start 在无前驱时视为就绪;其他无前驱节点不在此轮加入 */
376
+ if (preds.length === 0 && definitionId !== "control_start") continue;
377
+ const isTerminal = terminalStatuses.includes(instanceStatus[instanceId]);
378
+ if (!isTerminal) readyNodes.push(instanceId);
379
+ }
380
+
381
+ /** 规则 2:按需解析依赖——当 N 仅因前驱 P 未就绪而阻塞时,将可运行的 P 加入 readyNodes;P 须在 pendingSet 内(阻塞前驱已通过 supplement 加入 pendingInstances)。 */
382
+ const addedSet = new Set(readyNodes);
383
+ for (const instanceId of candidateSet) {
384
+ if (!needToRun.has(instanceId)) continue;
385
+ if (terminalStatuses.includes(instanceStatus[instanceId])) continue;
386
+ if (addedSet.has(instanceId)) continue;
387
+ const preds = predecessors[instanceId] || [];
388
+ const missingPreds = preds.filter((p) => !instanceStatus[p]);
389
+ if (missingPreds.length === 0) continue;
390
+ for (const p of missingPreds) {
391
+ if (!pendingSet.has(p)) continue;
392
+ const otherPreds = preds.filter((x) => x !== p);
393
+ const allOthersReady = otherPreds.every((op) =>
394
+ isPredecessorReadyFor(op, nodeDefinitions[op], instanceId),
395
+ );
396
+ if (!allOthersReady) continue;
397
+ if (terminalStatuses.includes(instanceStatus[p]) || addedSet.has(p)) continue;
398
+ if (!isRunnable(p)) continue;
399
+ addedSet.add(p);
400
+ readyNodes.push(p);
401
+ }
402
+ }
403
+
404
+ /** allDone:参与集内节点均为终态,或被阻塞(某 predecessor 为 condition_not_met 故永不会执行);不在参与集内的节点视为已完成 */
405
+ const isNodeDone = (id) => {
406
+ if (!needToRun.has(id)) return true;
407
+ if (terminalStatuses.includes(instanceStatus[id])) return true;
408
+ const preds = predecessors[id] || [];
409
+ const blocked = preds.some((p) => instanceStatus[p] === "condition_not_met");
410
+ return blocked;
411
+ };
412
+ let allDone = needToRun.size === 0 ? false : [...needToRun].every(isNodeDone);
413
+
414
+ /** pendingNodes:在参与集内且 status 为 pending 的节点(待用户确认),用于主流程判断「暂停」而非「卡住」 */
415
+ const pendingNodes = [...needToRun].filter((id) => instanceStatus[id] === "pending");
416
+
417
+ flow.pendingInstances = pendingInstances;
418
+ fs.writeFileSync(flowJsonPath, JSON.stringify(flow), "utf-8");
419
+
420
+ logToRunTag(workspaceRoot, flowName, uuid, "get-ready-nodes", {
421
+ event: "ready",
422
+ readyNodes,
423
+ allDone,
424
+ pendingNodes,
425
+ pendingInstances,
426
+ instanceStatus,
427
+ });
428
+ console.log(JSON.stringify({ ok: true, readyNodes, instanceStatus, allDone, pendingNodes }));
429
+ } catch (err) {
430
+ console.error(JSON.stringify({ ok: false, error: err.message }));
431
+ process.exit(1);
432
+ }
433
+ }
434
+
435
+ main();