@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.
- package/bin/lib/catalog-flows.mjs +10 -1
- package/bin/lib/control-while.mjs +336 -0
- package/bin/lib/flow-dsl/codegen.mjs +74 -11
- package/bin/lib/flow-dsl/ir.mjs +33 -2
- package/bin/lib/flow-dsl/lint.mjs +128 -3
- package/bin/lib/flow-dsl/parser.mjs +168 -4
- package/bin/lib/marketplace.mjs +10 -2
- package/bin/lib/node-package-manifest.mjs +7 -2
- package/bin/lib/node-ui-kit.mjs +156 -0
- package/bin/lib/paths.mjs +2 -0
- package/bin/lib/workspace-flow-store.mjs +11 -2
- package/bin/lib/workspace-graph-merge.mjs +3 -0
- package/bin/lib/workspace-routes.mjs +10 -1
- package/bin/lib/workspace-server.mjs +592 -11
- package/bin/pipeline/validate-flow.mjs +3 -3
- package/builtin/nodes/control_parse_json.md +25 -0
- package/builtin/nodes/control_subflow_call.md +27 -0
- package/builtin/nodes/control_while.md +124 -0
- package/builtin/nodes/provide_json.md +14 -0
- package/builtin/nodes/workspace_subflow_input.md +14 -0
- package/builtin/pipelines/subflow-preview/workspace.flow.js +29 -0
- package/builtin/pipelines/subflow-preview/workspace.layout.json +38 -0
- package/builtin/pipelines/subflow-preview/workspace.nodes.json +19 -0
- package/builtin/pipelines/subflow-preview/workspace.state.json +93 -0
- package/builtin/pipelines/while-subflow-preview/workspace.flow.js +55 -0
- package/builtin/pipelines/while-subflow-preview/workspace.layout.json +66 -0
- package/builtin/pipelines/while-subflow-preview/workspace.nodes.json +49 -0
- package/builtin/pipelines/while-subflow-preview/workspace.state.json +194 -0
- package/builtin/web-ui/dist/assets/{WorkflowAssistantThread-CY19DsYq.js → WorkflowAssistantThread-Bfo9Ythw.js} +1 -1
- package/builtin/web-ui/dist/assets/index-XbeI5foV.js +872 -0
- package/builtin/web-ui/dist/assets/index-e1omCEau.css +1 -0
- package/builtin/web-ui/dist/index.html +2 -2
- package/package.json +2 -1
- package/reference/flow-control-capabilities.md +41 -16
- package/shared/slot-types.js +58 -0
- package/skills/agentflow-flow-dsl/SKILL.md +60 -6
- package/skills/agentflow-flow-dsl/references/node-calls.md +3 -0
- package/skills/agentflow-flow-dsl/references/subflow-authoring.md +230 -0
- package/skills/agentflow-node-dsl/SKILL.md +39 -0
- package/skills/agentflow-node-reference/references/builtin-nodes.md +25 -0
- package/builtin/web-ui/dist/assets/index-B9ppXv7e.css +0 -1
- package/builtin/web-ui/dist/assets/index-CdEQWRrp.js +0 -872
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
} from "./marketplace.mjs";
|
|
30
30
|
import { isWorkspacePreviewDir } from "./workspace-preview.mjs";
|
|
31
31
|
import { RETIRED_NODE_IDS } from "./legacy-flow-execution.mjs";
|
|
32
|
+
import { normalizeNodeUiForSlots } from "./node-ui-kit.mjs";
|
|
32
33
|
|
|
33
34
|
/** 从指定目录收集含 flow.yaml 的子目录名。 */
|
|
34
35
|
export function collectPipelineNamesFromDir(dirPath) {
|
|
@@ -219,6 +220,7 @@ export function parseNodeFrontmatter(raw) {
|
|
|
219
220
|
displayName: undefined,
|
|
220
221
|
description: undefined,
|
|
221
222
|
guide: undefined,
|
|
223
|
+
ui: undefined,
|
|
222
224
|
runtime: "native",
|
|
223
225
|
type: "",
|
|
224
226
|
paletteHidden: false,
|
|
@@ -242,6 +244,7 @@ export function parseNodeFrontmatter(raw) {
|
|
|
242
244
|
data.paletteHidden = String(parsed.palette ?? "").trim().toLowerCase() === "hidden";
|
|
243
245
|
data.input = normalizeFrontmatterSlots(parsed.input);
|
|
244
246
|
data.output = normalizeFrontmatterSlots(parsed.output);
|
|
247
|
+
data.ui = normalizeNodeUiForSlots(parsed.ui, data.input, data.output);
|
|
245
248
|
return data;
|
|
246
249
|
}
|
|
247
250
|
} catch {
|
|
@@ -320,7 +323,7 @@ export function listNodesJson(workspaceRoot, flowId, flowSource, opts = {}) {
|
|
|
320
323
|
// frontmatter 的 type: 优先于按 id 前缀的推断(workspace_run 等不带前缀的节点靠它归类)
|
|
321
324
|
if (data.type) type = data.type;
|
|
322
325
|
// frontmatter 的 palette: hidden 与 RETIRED_NODE_IDS 等价,让节点自带可见性
|
|
323
|
-
if (data.paletteHidden) continue;
|
|
326
|
+
if (data.paletteHidden && !opts.includeHidden) continue;
|
|
324
327
|
const strippedId =
|
|
325
328
|
id.replace(/^agent_?/i, "").replace(/^control_?/i, "").replace(/^provide_?/i, "").replace(/^tool_?/i, "") || id;
|
|
326
329
|
const label = data.displayName ?? strippedId;
|
|
@@ -343,6 +346,8 @@ export function listNodesJson(workspaceRoot, flowId, flowSource, opts = {}) {
|
|
|
343
346
|
displayName: translatedDisplayName || data.displayName,
|
|
344
347
|
description: translatedDescription || data.description,
|
|
345
348
|
guide: translatedGuide || data.guide,
|
|
349
|
+
ui: data.ui,
|
|
350
|
+
paletteHidden: Boolean(data.paletteHidden),
|
|
346
351
|
inputs: data.input,
|
|
347
352
|
outputs: data.output,
|
|
348
353
|
source: flowIdOpt ? "flow" : "project",
|
|
@@ -364,6 +369,7 @@ export function listNodesJson(workspaceRoot, flowId, flowSource, opts = {}) {
|
|
|
364
369
|
label: manifest.displayName,
|
|
365
370
|
displayName: manifest.displayName,
|
|
366
371
|
description: manifest.description,
|
|
372
|
+
ui: manifest.ui,
|
|
367
373
|
inputs: manifest.input,
|
|
368
374
|
outputs: manifest.output,
|
|
369
375
|
source: flowIdOpt ? "flow" : "project",
|
|
@@ -392,6 +398,7 @@ export function listNodesJson(workspaceRoot, flowId, flowSource, opts = {}) {
|
|
|
392
398
|
label: manifest.displayName,
|
|
393
399
|
displayName: manifest.displayName,
|
|
394
400
|
description: manifest.description,
|
|
401
|
+
ui: manifest.ui,
|
|
395
402
|
inputs: manifest.input,
|
|
396
403
|
outputs: manifest.output,
|
|
397
404
|
source: manifest.source || "marketplace",
|
|
@@ -664,6 +671,7 @@ export function readNodeJson(workspaceRoot, nodeId, flowId, flowSource, opts = {
|
|
|
664
671
|
version: resolved.version,
|
|
665
672
|
packageDir: resolved.packageDir,
|
|
666
673
|
runtime: resolved.runtime,
|
|
674
|
+
ui: resolved.ui,
|
|
667
675
|
};
|
|
668
676
|
}
|
|
669
677
|
const fileName = nodeId.endsWith(".md") ? nodeId : `${nodeId}.md`;
|
|
@@ -724,6 +732,7 @@ export function readNodeJson(workspaceRoot, nodeId, flowId, flowSource, opts = {
|
|
|
724
732
|
executionLogic: content || undefined,
|
|
725
733
|
description: data.description,
|
|
726
734
|
guide: data.guide,
|
|
735
|
+
ui: data.ui,
|
|
727
736
|
};
|
|
728
737
|
} catch (_) {}
|
|
729
738
|
}
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
|
|
3
|
+
const DECISIONS = new Set(["continue", "wait", "done", "fail"]);
|
|
4
|
+
const STEP_RESULT_KEYS = new Set(["decision", "state", "summary"]);
|
|
5
|
+
|
|
6
|
+
export const DEFAULT_CONTROL_WHILE_MAX_ITERATIONS = 20;
|
|
7
|
+
export const DEFAULT_CONTROL_WHILE_TIMEOUT_MS = 30 * 60 * 1000;
|
|
8
|
+
export const MAX_CONTROL_WHILE_ITERATIONS = 1000;
|
|
9
|
+
export const MAX_CONTROL_WHILE_TIMEOUT_MS = 7 * 24 * 60 * 60 * 1000;
|
|
10
|
+
export const MAX_CONTROL_WHILE_STEP_STDOUT_BYTES = 1024 * 1024;
|
|
11
|
+
export const MAX_CONTROL_WHILE_STEP_STDERR_BYTES = 256 * 1024;
|
|
12
|
+
export const MAX_CONTROL_WHILE_SUMMARY_LENGTH = 4000;
|
|
13
|
+
export const MAX_CONTROL_WHILE_STATE_BYTES = 1024 * 1024;
|
|
14
|
+
|
|
15
|
+
export function parseControlWhileDurationMs(raw, fallback = DEFAULT_CONTROL_WHILE_TIMEOUT_MS) {
|
|
16
|
+
const text = String(raw ?? "").trim().toLowerCase();
|
|
17
|
+
if (!text) return fallback;
|
|
18
|
+
const match = text.match(/^(\d+(?:\.\d+)?)\s*(ms|s|m|h|d)?$/);
|
|
19
|
+
if (!match) throw new Error(`control.while invalid timeout: ${raw}`);
|
|
20
|
+
const value = Number(match[1]);
|
|
21
|
+
const unit = match[2] || "s";
|
|
22
|
+
const factor = unit === "ms" ? 1 : unit === "s" ? 1000 : unit === "m" ? 60_000 : unit === "h" ? 3_600_000 : 86_400_000;
|
|
23
|
+
const result = Math.round(value * factor);
|
|
24
|
+
if (!Number.isFinite(result) || result <= 0) throw new Error(`control.while timeout must be greater than zero: ${raw}`);
|
|
25
|
+
if (result > MAX_CONTROL_WHILE_TIMEOUT_MS) throw new Error("control.while timeout must not exceed 7d");
|
|
26
|
+
return result;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function normalizeControlWhileConfig(inputs = {}) {
|
|
30
|
+
const rawMax = String(inputs.maxIterations ?? "").trim();
|
|
31
|
+
const maxIterations = rawMax ? Number(rawMax) : DEFAULT_CONTROL_WHILE_MAX_ITERATIONS;
|
|
32
|
+
if (!Number.isInteger(maxIterations) || maxIterations < 1 || maxIterations > MAX_CONTROL_WHILE_ITERATIONS) {
|
|
33
|
+
throw new Error(`control.while maxIterations must be an integer between 1 and ${MAX_CONTROL_WHILE_ITERATIONS}`);
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
maxIterations,
|
|
37
|
+
timeoutMs: parseControlWhileDurationMs(inputs.timeout, DEFAULT_CONTROL_WHILE_TIMEOUT_MS),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function normalizeControlWhileInitialState(raw) {
|
|
42
|
+
if (raw == null || String(raw).trim() === "") return null;
|
|
43
|
+
if (typeof raw === "object") {
|
|
44
|
+
assertControlWhileStateSize(raw);
|
|
45
|
+
return raw;
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
const parsed = JSON.parse(String(raw));
|
|
49
|
+
assertControlWhileStateSize(parsed);
|
|
50
|
+
return parsed;
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if (String(error?.message || "").startsWith("control.while state")) throw error;
|
|
53
|
+
throw new Error(`control.while state must be valid JSON: ${error.message}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function serializeControlWhileState(value) {
|
|
58
|
+
const serialized = JSON.stringify(value ?? null);
|
|
59
|
+
if (serialized === undefined) throw new Error("control.while state must be JSON serializable");
|
|
60
|
+
if (Buffer.byteLength(serialized, "utf-8") > MAX_CONTROL_WHILE_STATE_BYTES) {
|
|
61
|
+
throw new Error(`control.while state must not exceed ${MAX_CONTROL_WHILE_STATE_BYTES} bytes`);
|
|
62
|
+
}
|
|
63
|
+
return serialized;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function assertControlWhileStateSize(value) {
|
|
67
|
+
serializeControlWhileState(value);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function parseControlWhileStepResult(raw, currentState = null) {
|
|
71
|
+
const text = String(raw ?? "").trim();
|
|
72
|
+
if (!text) throw new Error("control.while step returned empty stdout; expected one JSON object");
|
|
73
|
+
if (Buffer.byteLength(text, "utf-8") > MAX_CONTROL_WHILE_STEP_STDOUT_BYTES) {
|
|
74
|
+
throw new Error(`control.while step stdout must not exceed ${MAX_CONTROL_WHILE_STEP_STDOUT_BYTES} bytes`);
|
|
75
|
+
}
|
|
76
|
+
let parsed;
|
|
77
|
+
try {
|
|
78
|
+
parsed = JSON.parse(text);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
throw new Error(`control.while step stdout must be exactly one JSON object: ${error.message}`);
|
|
81
|
+
}
|
|
82
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
83
|
+
throw new Error("control.while step stdout must be a JSON object");
|
|
84
|
+
}
|
|
85
|
+
const unknownKeys = Object.keys(parsed).filter((key) => !STEP_RESULT_KEYS.has(key));
|
|
86
|
+
if (unknownKeys.length) {
|
|
87
|
+
throw new Error(`control.while step result contains unsupported fields: ${unknownKeys.join(", ")}`);
|
|
88
|
+
}
|
|
89
|
+
if (typeof parsed.decision !== "string") {
|
|
90
|
+
throw new Error("control.while decision must be a string");
|
|
91
|
+
}
|
|
92
|
+
const decision = parsed.decision;
|
|
93
|
+
if (!DECISIONS.has(decision)) {
|
|
94
|
+
throw new Error(`control.while decision must be continue, wait, done, or fail; received ${JSON.stringify(parsed.decision)}`);
|
|
95
|
+
}
|
|
96
|
+
if (parsed.summary != null && typeof parsed.summary !== "string") {
|
|
97
|
+
throw new Error("control.while summary must be a string");
|
|
98
|
+
}
|
|
99
|
+
const summary = parsed.summary || "";
|
|
100
|
+
if (summary.length > MAX_CONTROL_WHILE_SUMMARY_LENGTH) {
|
|
101
|
+
throw new Error(`control.while summary must not exceed ${MAX_CONTROL_WHILE_SUMMARY_LENGTH} characters`);
|
|
102
|
+
}
|
|
103
|
+
const state = Object.prototype.hasOwnProperty.call(parsed, "state") ? parsed.state : currentState;
|
|
104
|
+
assertControlWhileStateSize(state);
|
|
105
|
+
return {
|
|
106
|
+
decision,
|
|
107
|
+
state,
|
|
108
|
+
summary,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function normalizeControlWhileHistory(raw) {
|
|
113
|
+
if (raw == null || String(raw).trim() === "") return [];
|
|
114
|
+
let parsed = raw;
|
|
115
|
+
if (!Array.isArray(raw)) {
|
|
116
|
+
try {
|
|
117
|
+
parsed = JSON.parse(String(raw));
|
|
118
|
+
} catch (error) {
|
|
119
|
+
throw new Error(`control.while checkpoint history must be valid JSON: ${error.message}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (!Array.isArray(parsed)) throw new Error("control.while checkpoint history must be an array");
|
|
123
|
+
if (parsed.length > MAX_CONTROL_WHILE_ITERATIONS) {
|
|
124
|
+
throw new Error(`control.while checkpoint history must not exceed ${MAX_CONTROL_WHILE_ITERATIONS} entries`);
|
|
125
|
+
}
|
|
126
|
+
let previousIteration = 0;
|
|
127
|
+
return parsed.map((rawEntry, index) => {
|
|
128
|
+
if (!rawEntry || typeof rawEntry !== "object" || Array.isArray(rawEntry)) {
|
|
129
|
+
throw new Error(`control.while checkpoint history entry ${index + 1} must be an object`);
|
|
130
|
+
}
|
|
131
|
+
const iteration = Number(rawEntry.iteration);
|
|
132
|
+
if (!Number.isInteger(iteration) || iteration < 1 || iteration <= previousIteration) {
|
|
133
|
+
throw new Error("control.while checkpoint iterations must be positive and strictly increasing");
|
|
134
|
+
}
|
|
135
|
+
const decision = rawEntry.decision;
|
|
136
|
+
if (typeof decision !== "string" || !DECISIONS.has(decision)) {
|
|
137
|
+
throw new Error(`control.while checkpoint entry ${iteration} has an invalid decision`);
|
|
138
|
+
}
|
|
139
|
+
if (rawEntry.summary != null && typeof rawEntry.summary !== "string") {
|
|
140
|
+
throw new Error(`control.while checkpoint entry ${iteration} has an invalid summary`);
|
|
141
|
+
}
|
|
142
|
+
const elapsedMs = Number(rawEntry.elapsedMs ?? 0);
|
|
143
|
+
if (!Number.isFinite(elapsedMs) || elapsedMs < 0) {
|
|
144
|
+
throw new Error(`control.while checkpoint entry ${iteration} has an invalid elapsedMs`);
|
|
145
|
+
}
|
|
146
|
+
const idempotencyKey = rawEntry.idempotencyKey == null ? "" : String(rawEntry.idempotencyKey);
|
|
147
|
+
if (idempotencyKey.length > 128) {
|
|
148
|
+
throw new Error(`control.while checkpoint entry ${iteration} has an invalid idempotencyKey`);
|
|
149
|
+
}
|
|
150
|
+
previousIteration = iteration;
|
|
151
|
+
return {
|
|
152
|
+
iteration,
|
|
153
|
+
decision,
|
|
154
|
+
summary: rawEntry.summary || "",
|
|
155
|
+
elapsedMs: Math.round(elapsedMs),
|
|
156
|
+
...(idempotencyKey ? { idempotencyKey } : {}),
|
|
157
|
+
};
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function resolveControlWhileCheckpoint({
|
|
162
|
+
previousDecision = "",
|
|
163
|
+
state = "",
|
|
164
|
+
history = "",
|
|
165
|
+
iterations = "",
|
|
166
|
+
fingerprint = "",
|
|
167
|
+
expectedFingerprint = "",
|
|
168
|
+
allowResume = true,
|
|
169
|
+
} = {}) {
|
|
170
|
+
if (String(previousDecision || "") !== "wait") return { resumable: false, reason: "not_waiting" };
|
|
171
|
+
if (!allowResume) return { resumable: false, reason: "resume_disabled" };
|
|
172
|
+
if (!fingerprint || fingerprint !== expectedFingerprint) return { resumable: false, reason: "fingerprint_mismatch" };
|
|
173
|
+
|
|
174
|
+
const parsedHistory = normalizeControlWhileHistory(history);
|
|
175
|
+
const last = parsedHistory[parsedHistory.length - 1];
|
|
176
|
+
const recordedIterations = Number(iterations);
|
|
177
|
+
if (!last || last.decision !== "wait") {
|
|
178
|
+
throw new Error("control.while waiting checkpoint is corrupt: history must end with wait");
|
|
179
|
+
}
|
|
180
|
+
if (!Number.isInteger(recordedIterations) || recordedIterations !== last.iteration) {
|
|
181
|
+
throw new Error("control.while waiting checkpoint is corrupt: iterations do not match history");
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
resumable: true,
|
|
185
|
+
reason: "waiting_checkpoint",
|
|
186
|
+
state: normalizeControlWhileInitialState(state),
|
|
187
|
+
history: parsedHistory,
|
|
188
|
+
nextIteration: last.iteration + 1,
|
|
189
|
+
elapsedMs: parsedHistory.reduce((total, entry) => total + entry.elapsedMs, 0),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function controlWhileCheckpointFingerprint({ inputFingerprint = "", initialState = null } = {}) {
|
|
194
|
+
return crypto.createHash("sha256")
|
|
195
|
+
.update(`${String(inputFingerprint || "")}\u0000${serializeControlWhileState(initialState)}`)
|
|
196
|
+
.digest("hex")
|
|
197
|
+
.slice(0, 24);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function controlWhileIdempotencyKey({ checkpointFingerprint = "", nodeId = "", iteration = 1 } = {}) {
|
|
201
|
+
const absoluteIteration = Number(iteration);
|
|
202
|
+
if (!checkpointFingerprint || !nodeId || !Number.isInteger(absoluteIteration) || absoluteIteration < 1) {
|
|
203
|
+
throw new Error("control.while cannot create idempotency key without checkpoint fingerprint, nodeId, and iteration");
|
|
204
|
+
}
|
|
205
|
+
const digest = crypto.createHash("sha256")
|
|
206
|
+
.update(`${checkpointFingerprint}\u0000${nodeId}\u0000${absoluteIteration}`)
|
|
207
|
+
.digest("hex")
|
|
208
|
+
.slice(0, 32);
|
|
209
|
+
return `afw_${digest}`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function controlWhileTimeoutError(timeoutMs) {
|
|
213
|
+
const error = new Error(`control.while timed out after ${timeoutMs}ms`);
|
|
214
|
+
error.code = "CONTROL_WHILE_TIMEOUT";
|
|
215
|
+
return error;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Execute one deterministic step repeatedly. The graph stays acyclic: iteration is entirely
|
|
220
|
+
* represented by this state machine and its per-iteration events.
|
|
221
|
+
*/
|
|
222
|
+
export async function runControlWhile({
|
|
223
|
+
initialState = null,
|
|
224
|
+
initialHistory = [],
|
|
225
|
+
initialElapsedMs = 0,
|
|
226
|
+
startIteration = null,
|
|
227
|
+
maxIterations = DEFAULT_CONTROL_WHILE_MAX_ITERATIONS,
|
|
228
|
+
timeoutMs = DEFAULT_CONTROL_WHILE_TIMEOUT_MS,
|
|
229
|
+
executeStep,
|
|
230
|
+
idempotencyKeyForIteration = null,
|
|
231
|
+
signal = null,
|
|
232
|
+
onIterationStart = null,
|
|
233
|
+
onIterationDone = null,
|
|
234
|
+
now = () => Date.now(),
|
|
235
|
+
} = {}) {
|
|
236
|
+
if (typeof executeStep !== "function") throw new Error("control.while executeStep is required");
|
|
237
|
+
const history = normalizeControlWhileHistory(initialHistory);
|
|
238
|
+
const inferredStartIteration = history.length ? history[history.length - 1].iteration + 1 : 1;
|
|
239
|
+
const firstIteration = startIteration == null ? inferredStartIteration : Number(startIteration);
|
|
240
|
+
if (!Number.isInteger(firstIteration) || firstIteration < 1 || firstIteration !== inferredStartIteration) {
|
|
241
|
+
throw new Error("control.while startIteration must continue directly after checkpoint history");
|
|
242
|
+
}
|
|
243
|
+
const elapsedBeforeRun = Number(initialElapsedMs);
|
|
244
|
+
if (!Number.isFinite(elapsedBeforeRun) || elapsedBeforeRun < 0) {
|
|
245
|
+
throw new Error("control.while initialElapsedMs must be a non-negative number");
|
|
246
|
+
}
|
|
247
|
+
const startedAt = now();
|
|
248
|
+
let state = initialState;
|
|
249
|
+
|
|
250
|
+
for (let iteration = firstIteration; iteration <= maxIterations; iteration += 1) {
|
|
251
|
+
if (signal?.aborted) {
|
|
252
|
+
const error = new Error("Workspace run stopped");
|
|
253
|
+
error.code = "WORKSPACE_RUN_ABORTED";
|
|
254
|
+
throw error;
|
|
255
|
+
}
|
|
256
|
+
const elapsedBefore = elapsedBeforeRun + Math.max(0, now() - startedAt);
|
|
257
|
+
const remainingMs = timeoutMs - elapsedBefore;
|
|
258
|
+
if (remainingMs <= 0) return {
|
|
259
|
+
decision: "fail",
|
|
260
|
+
state,
|
|
261
|
+
summary: `Reached timeout after ${timeoutMs}ms`,
|
|
262
|
+
iterations: history.length ? history[history.length - 1].iteration : firstIteration - 1,
|
|
263
|
+
history,
|
|
264
|
+
reason: "timeout",
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
const idempotencyKey = typeof idempotencyKeyForIteration === "function"
|
|
268
|
+
? String(idempotencyKeyForIteration({ iteration, state }) || "")
|
|
269
|
+
: "";
|
|
270
|
+
onIterationStart?.({ iteration, state, elapsedMs: elapsedBefore, remainingMs, idempotencyKey });
|
|
271
|
+
const iterationStartedAt = now();
|
|
272
|
+
const controller = new AbortController();
|
|
273
|
+
let timedOut = false;
|
|
274
|
+
const abortFromParent = () => controller.abort(signal?.reason);
|
|
275
|
+
signal?.addEventListener("abort", abortFromParent, { once: true });
|
|
276
|
+
const timer = setTimeout(() => {
|
|
277
|
+
timedOut = true;
|
|
278
|
+
controller.abort(controlWhileTimeoutError(timeoutMs));
|
|
279
|
+
}, remainingMs);
|
|
280
|
+
|
|
281
|
+
let raw;
|
|
282
|
+
try {
|
|
283
|
+
raw = await executeStep({ iteration, state, remainingMs, signal: controller.signal, idempotencyKey });
|
|
284
|
+
} catch (error) {
|
|
285
|
+
if (signal?.aborted) {
|
|
286
|
+
const stopped = new Error("Workspace run stopped");
|
|
287
|
+
stopped.code = "WORKSPACE_RUN_ABORTED";
|
|
288
|
+
throw stopped;
|
|
289
|
+
}
|
|
290
|
+
if (timedOut) return {
|
|
291
|
+
decision: "fail",
|
|
292
|
+
state,
|
|
293
|
+
summary: `Reached timeout after ${timeoutMs}ms`,
|
|
294
|
+
iterations: history.length ? history[history.length - 1].iteration : firstIteration - 1,
|
|
295
|
+
history,
|
|
296
|
+
reason: "timeout",
|
|
297
|
+
};
|
|
298
|
+
throw error;
|
|
299
|
+
} finally {
|
|
300
|
+
clearTimeout(timer);
|
|
301
|
+
signal?.removeEventListener("abort", abortFromParent);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const step = parseControlWhileStepResult(raw, state);
|
|
305
|
+
state = step.state;
|
|
306
|
+
const entry = {
|
|
307
|
+
iteration,
|
|
308
|
+
decision: step.decision,
|
|
309
|
+
summary: step.summary,
|
|
310
|
+
elapsedMs: Math.max(0, now() - iterationStartedAt),
|
|
311
|
+
...(idempotencyKey ? { idempotencyKey } : {}),
|
|
312
|
+
};
|
|
313
|
+
history.push(entry);
|
|
314
|
+
onIterationDone?.({ ...entry, state });
|
|
315
|
+
|
|
316
|
+
if (step.decision !== "continue") {
|
|
317
|
+
return {
|
|
318
|
+
decision: step.decision,
|
|
319
|
+
state,
|
|
320
|
+
summary: step.summary,
|
|
321
|
+
iterations: iteration,
|
|
322
|
+
history,
|
|
323
|
+
reason: step.decision,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return {
|
|
329
|
+
decision: "fail",
|
|
330
|
+
state,
|
|
331
|
+
summary: `Reached maxIterations (${maxIterations}) without done or wait`,
|
|
332
|
+
iterations: maxIterations,
|
|
333
|
+
history,
|
|
334
|
+
reason: "max_iterations",
|
|
335
|
+
};
|
|
336
|
+
}
|
|
@@ -64,6 +64,8 @@ function displayFileExt(kind) {
|
|
|
64
64
|
*/
|
|
65
65
|
export function generateFlowSource(ir, opts = {}) {
|
|
66
66
|
const N = ir.nodes;
|
|
67
|
+
const subflows = ir?.subflows && typeof ir.subflows === "object" ? ir.subflows : {};
|
|
68
|
+
const subflowMemberIds = new Set(Object.values(subflows).flatMap((subflow) => subflow?.nodeIds || []));
|
|
67
69
|
const files = [];
|
|
68
70
|
const emitFile = (rel, text) => {
|
|
69
71
|
files.push({ path: rel, text });
|
|
@@ -153,7 +155,9 @@ export function generateFlowSource(ir, opts = {}) {
|
|
|
153
155
|
const callee = (id) => bindingOf.get(id)?.name || apiCall(apiName(N[id].definitionId));
|
|
154
156
|
|
|
155
157
|
const bodyTextOf = (id) => String(
|
|
156
|
-
(N[id].definitionId === "tool_nodejs"
|
|
158
|
+
((N[id].definitionId === "tool_nodejs" || N[id].definitionId === "control_while") && N[id].script)
|
|
159
|
+
? N[id].script
|
|
160
|
+
: (N[id].body || ""),
|
|
157
161
|
);
|
|
158
162
|
|
|
159
163
|
/**
|
|
@@ -246,13 +250,14 @@ export function generateFlowSource(ir, opts = {}) {
|
|
|
246
250
|
}
|
|
247
251
|
|
|
248
252
|
// 把控制链展开成嵌套序列;分叉处变成数组,由 printItem 打成 flow.fork(...)
|
|
249
|
-
function expandChain(id) {
|
|
253
|
+
function expandChain(id, allowed = null) {
|
|
250
254
|
const kids = (controlNext.get(id) || [])
|
|
251
255
|
.filter((x) => !RUN_DEFINITIONS.has(N[x.to].definitionId))
|
|
256
|
+
.filter((x) => !allowed || allowed.has(x.to))
|
|
252
257
|
.map((x) => x.to);
|
|
253
258
|
if (isIf(id) || !kids.length) return [id];
|
|
254
|
-
if (kids.length === 1) return [id, ...expandChain(kids[0])];
|
|
255
|
-
return [id, kids.map(expandChain)];
|
|
259
|
+
if (kids.length === 1) return [id, ...expandChain(kids[0], allowed)];
|
|
260
|
+
return [id, kids.map((kid) => expandChain(kid, allowed))];
|
|
256
261
|
}
|
|
257
262
|
const collectIds = (seq, acc = []) => {
|
|
258
263
|
for (const item of seq) {
|
|
@@ -268,16 +273,39 @@ export function generateFlowSource(ir, opts = {}) {
|
|
|
268
273
|
);
|
|
269
274
|
|
|
270
275
|
const declared = new Set();
|
|
276
|
+
const declaredSubflows = new Set();
|
|
271
277
|
const out = [];
|
|
272
278
|
|
|
273
|
-
function chainFrom(roots) {
|
|
279
|
+
function chainFrom(roots, allowed = null) {
|
|
274
280
|
const seq = roots.length === 1
|
|
275
|
-
? expandChain(roots[0])
|
|
276
|
-
: (roots.length ? [roots.map(expandChain)] : []);
|
|
281
|
+
? expandChain(roots[0], allowed)
|
|
282
|
+
: (roots.length ? [roots.map((root) => expandChain(root, allowed))] : []);
|
|
277
283
|
for (const id of collectIds(seq)) declare(id, false);
|
|
278
284
|
return seq;
|
|
279
285
|
}
|
|
280
286
|
|
|
287
|
+
const outputRef = (nodeId, slot) => outVar.get(`${nodeId}|${slot}`) || `${nodeId}.${slot}`;
|
|
288
|
+
|
|
289
|
+
function declareSubflow(subflowId) {
|
|
290
|
+
if (declaredSubflows.has(subflowId)) return;
|
|
291
|
+
const subflow = subflows[subflowId];
|
|
292
|
+
if (!subflow) return;
|
|
293
|
+
declaredSubflows.add(subflowId);
|
|
294
|
+
const allowed = new Set(subflow.nodeIds || []);
|
|
295
|
+
for (const binding of Object.values(subflow.inputs || {})) declare(binding.nodeId, false);
|
|
296
|
+
const seq = chainFrom((subflow.roots || []).filter((id) => allowed.has(id)), allowed).map(printItem);
|
|
297
|
+
for (const binding of Object.values(subflow.outputs || {})) declare(binding.nodeId, false);
|
|
298
|
+
const inputEntries = Object.entries(subflow.inputs || {}).map(([name, binding]) => (
|
|
299
|
+
`${isIdentifier(name) ? name : JSON.stringify(name)}: ${binding.nodeId}`
|
|
300
|
+
));
|
|
301
|
+
const outputEntries = Object.entries(subflow.outputs || {}).map(([name, binding]) => (
|
|
302
|
+
`${isIdentifier(name) ? name : JSON.stringify(name)}: ${outputRef(binding.nodeId, binding.slot)}`
|
|
303
|
+
));
|
|
304
|
+
const inputObject = inputEntries.length ? `{ ${inputEntries.join(", ")} }` : "{}";
|
|
305
|
+
const outputObject = outputEntries.length ? `{ ${outputEntries.join(", ")} }` : "{}";
|
|
306
|
+
out.push(`export const ${subflowId} = ${apiCall("flow.subflow")}(${literal(subflow.label || subflowId)}, ${inputObject}, ${apiCall("flow")}(${seq.join(", ")}), ${outputObject});\n`);
|
|
307
|
+
}
|
|
308
|
+
|
|
281
309
|
function declare(id, exported) {
|
|
282
310
|
if (declared.has(id)) return;
|
|
283
311
|
declared.add(id);
|
|
@@ -285,18 +313,50 @@ export function generateFlowSource(ir, opts = {}) {
|
|
|
285
313
|
for (const dep of dataIn.get(id) || []) if (!declared.has(dep.from)) declare(dep.from, true);
|
|
286
314
|
|
|
287
315
|
const node = N[id];
|
|
316
|
+
if (node.definitionId === "workspace_subflow_input") {
|
|
317
|
+
const name = String(node.attrs?.subflowInputName || node.label || id);
|
|
318
|
+
const type = String(node.attrs?.subflowInputType || node.packageDef?.output?.[0]?.type || "text");
|
|
319
|
+
out.push(`const ${id} = ${apiCall("flow.input")}(${literal(name)}, ${literal(type)});\n`);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
if (node.definitionId === "control_subflow_call") {
|
|
323
|
+
const subflowId = String(node.attrs?.subflowId || "");
|
|
324
|
+
declareSubflow(subflowId);
|
|
325
|
+
const args = [];
|
|
326
|
+
if (node.label) args.push(literal(node.label));
|
|
327
|
+
args.push(subflowId);
|
|
328
|
+
args.push(pinsObject(id));
|
|
329
|
+
out.push(`${exported ? "export " : ""}const ${id} = ${apiCall("flow.call")}(${args.join(", ")});\n`);
|
|
330
|
+
const bindings = (destructured.get(id) || []).map((slot) => {
|
|
331
|
+
const variable = outVar.get(`${id}|${slot}`);
|
|
332
|
+
return variable === slot ? slot : `${isIdentifier(slot) ? slot : JSON.stringify(slot)}: ${variable}`;
|
|
333
|
+
});
|
|
334
|
+
if (bindings.length) out.push(`const { ${bindings.join(", ")} } = ${id};\n`);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
288
337
|
const folds = isIf(id) ? new Map() : bodyFolds(id);
|
|
289
338
|
const args = [];
|
|
290
339
|
if (node.label) args.push(literal(node.label));
|
|
291
340
|
args.push(pinsObject(id, folds));
|
|
292
341
|
|
|
293
|
-
|
|
342
|
+
const conditionSubflowId = node.definitionId === "control_while"
|
|
343
|
+
? String(node.attrs?.conditionSubflowId || "")
|
|
344
|
+
: "";
|
|
345
|
+
const bodySubflowId = node.definitionId === "control_while"
|
|
346
|
+
? String(node.attrs?.bodySubflowId || "")
|
|
347
|
+
: "";
|
|
348
|
+
if (conditionSubflowId || bodySubflowId) {
|
|
349
|
+
declareSubflow(conditionSubflowId);
|
|
350
|
+
declareSubflow(bodySubflowId);
|
|
351
|
+
args.push(conditionSubflowId || "undefined");
|
|
352
|
+
args.push(bodySubflowId || "undefined");
|
|
353
|
+
} else if (isIf(id)) {
|
|
294
354
|
const thenIds = (controlNext.get(id) || []).filter((x) => x.slot === "next1").map((x) => x.to);
|
|
295
355
|
const elseIds = (controlNext.get(id) || []).filter((x) => x.slot === "next2").map((x) => x.to);
|
|
296
356
|
args.push(`${apiCall("flow")}(${chainFrom(thenIds).map(printItem).join(", ")})`);
|
|
297
357
|
args.push(`${apiCall("flow")}(${chainFrom(elseIds).map(printItem).join(", ")})`);
|
|
298
358
|
} else {
|
|
299
|
-
const usesScript = node.definitionId === "tool_nodejs" && node.script;
|
|
359
|
+
const usesScript = (node.definitionId === "tool_nodejs" || node.definitionId === "control_while") && node.script;
|
|
300
360
|
const body = usesScript ? node.script : node.body;
|
|
301
361
|
if (body) {
|
|
302
362
|
args.push(folds.size
|
|
@@ -319,6 +379,9 @@ export function generateFlowSource(ir, opts = {}) {
|
|
|
319
379
|
|
|
320
380
|
const ids = Object.keys(N).sort();
|
|
321
381
|
|
|
382
|
+
// 子流程先于父流程调用声明;内部节点仍是普通 DSL 节点,只是拥有独立作用域。
|
|
383
|
+
for (const subflowId of Object.keys(subflows).sort()) declareSubflow(subflowId);
|
|
384
|
+
|
|
322
385
|
for (const runId of ids) {
|
|
323
386
|
const definitionId = N[runId].definitionId;
|
|
324
387
|
if (!RUN_DEFINITIONS.has(definitionId)) continue;
|
|
@@ -344,12 +407,12 @@ export function generateFlowSource(ir, opts = {}) {
|
|
|
344
407
|
// 没有 run 入口、但自成控制链的孤儿链条——语料里真的有,丢掉就等于删图
|
|
345
408
|
const controlTargets = new Set(ir.edges.map((e) => e.split("|")).filter((p) => p[3] === "prev").map((p) => p[2]));
|
|
346
409
|
for (const id of ids) {
|
|
347
|
-
if (declared.has(id) || RUN_DEFINITIONS.has(N[id].definitionId) || controlTargets.has(id)) continue;
|
|
410
|
+
if (declared.has(id) || subflowMemberIds.has(id) || RUN_DEFINITIONS.has(N[id].definitionId) || controlTargets.has(id)) continue;
|
|
348
411
|
if (!(controlNext.get(id) || []).length) continue;
|
|
349
412
|
out.push(`${apiCall("flow.detached")}(${chainFrom([id]).map(printItem).join(", ")});\n`);
|
|
350
413
|
}
|
|
351
414
|
for (const id of ids) {
|
|
352
|
-
if (!declared.has(id) && !RUN_DEFINITIONS.has(N[id].definitionId)) declare(id, true);
|
|
415
|
+
if (!declared.has(id) && !subflowMemberIds.has(id) && !RUN_DEFINITIONS.has(N[id].definitionId)) declare(id, true);
|
|
353
416
|
}
|
|
354
417
|
|
|
355
418
|
const flowImports = flowApiRoots.map((root) => {
|
package/bin/lib/flow-dsl/ir.mjs
CHANGED
|
@@ -46,6 +46,11 @@ export const NODE_META_KEYS = [
|
|
|
46
46
|
"marketplacePackageId",
|
|
47
47
|
"marketplaceVersion",
|
|
48
48
|
"sourceContextRunNodeId",
|
|
49
|
+
"subflowId",
|
|
50
|
+
"subflowInputName",
|
|
51
|
+
"subflowInputType",
|
|
52
|
+
"conditionSubflowId",
|
|
53
|
+
"bodySubflowId",
|
|
49
54
|
];
|
|
50
55
|
|
|
51
56
|
function handleIndex(handle) {
|
|
@@ -100,6 +105,21 @@ export function graphToIr(graph) {
|
|
|
100
105
|
node.extraIn = [];
|
|
101
106
|
node.extraOut = [];
|
|
102
107
|
|
|
108
|
+
if (definitionId === "control_subflow_call" || definitionId === "workspace_subflow_input") {
|
|
109
|
+
node.packageDef = {
|
|
110
|
+
input: (instance.input || []).map((slot) => ({
|
|
111
|
+
name: String(slot?.name || ""),
|
|
112
|
+
type: String(slot?.type || "text"),
|
|
113
|
+
...(slot?.required ? { required: true } : {}),
|
|
114
|
+
})).filter((slot) => slot.name),
|
|
115
|
+
output: (instance.output || []).map((slot) => ({
|
|
116
|
+
name: String(slot?.name || ""),
|
|
117
|
+
type: String(slot?.type || "text"),
|
|
118
|
+
...(slot?.required ? { required: true } : {}),
|
|
119
|
+
})).filter((slot) => slot.name),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
103
123
|
for (const slot of instance.input || []) {
|
|
104
124
|
const name = String(slot?.name || "");
|
|
105
125
|
if (!name) continue;
|
|
@@ -144,7 +164,12 @@ export function graphToIr(graph) {
|
|
|
144
164
|
};
|
|
145
165
|
}
|
|
146
166
|
|
|
147
|
-
return {
|
|
167
|
+
return {
|
|
168
|
+
nodes,
|
|
169
|
+
edges: [...new Set(edges)].sort(),
|
|
170
|
+
slotOrder,
|
|
171
|
+
subflows: graph?.subflows && typeof graph.subflows === "object" ? graph.subflows : {},
|
|
172
|
+
};
|
|
148
173
|
}
|
|
149
174
|
|
|
150
175
|
/**
|
|
@@ -299,7 +324,13 @@ export function irToGraph(ir, layout = { nodes: {} }, nodeMeta = { nodes: {} })
|
|
|
299
324
|
}
|
|
300
325
|
if (layout.viewport) ui.viewport = layout.viewport;
|
|
301
326
|
|
|
302
|
-
return {
|
|
327
|
+
return {
|
|
328
|
+
version: 1,
|
|
329
|
+
instances,
|
|
330
|
+
edges,
|
|
331
|
+
ui,
|
|
332
|
+
subflows: ir?.subflows && typeof ir.subflows === "object" ? ir.subflows : {},
|
|
333
|
+
};
|
|
303
334
|
}
|
|
304
335
|
|
|
305
336
|
export { CTRL_SLOTS, STD_SLOTS };
|