@fieldwangai/agentflow 0.1.157 → 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 +75 -12
- 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/locales/en.json +4 -0
- package/bin/lib/locales/zh.json +4 -0
- package/bin/lib/marketplace.mjs +10 -2
- package/bin/lib/mcp-server.mjs +1 -0
- 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 +601 -15
- 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/display_code.md +39 -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-g_ljSViJ.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 +61 -7
- package/skills/agentflow-flow-dsl/references/node-calls.md +4 -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 +33 -0
- package/builtin/web-ui/dist/assets/index-C0iq6zHl.js +0 -870
- package/builtin/web-ui/dist/assets/index-mmXs3H9P.css +0 -1
|
@@ -13,6 +13,18 @@ import { readAuthUsers } from "./auth.mjs";
|
|
|
13
13
|
import { listFlowsJson, listNodesJson } from "./catalog-flows.mjs";
|
|
14
14
|
import { startComposerAgent } from "./composer-agent.mjs";
|
|
15
15
|
import { loadResourcesForSkillKeys } from "./composer-skill-router.mjs";
|
|
16
|
+
import {
|
|
17
|
+
MAX_CONTROL_WHILE_STEP_STDERR_BYTES,
|
|
18
|
+
MAX_CONTROL_WHILE_STEP_STDOUT_BYTES,
|
|
19
|
+
controlWhileCheckpointFingerprint,
|
|
20
|
+
controlWhileIdempotencyKey,
|
|
21
|
+
normalizeControlWhileConfig,
|
|
22
|
+
normalizeControlWhileInitialState,
|
|
23
|
+
parseControlWhileStepResult,
|
|
24
|
+
resolveControlWhileCheckpoint,
|
|
25
|
+
runControlWhile,
|
|
26
|
+
serializeControlWhileState,
|
|
27
|
+
} from "./control-while.mjs";
|
|
16
28
|
import { graphToFlowFiles } from "./flow-dsl/index.mjs";
|
|
17
29
|
import { buildGitContext, inferGitRepoRootFromWorktree, loadGitWorktree, normalizeGitContext, runGit, sanitizeWorktreeName, unloadGitWorktree } from "./git-worktree.mjs";
|
|
18
30
|
import { createGitLabMergeRequest } from "./gitlab-mr.mjs";
|
|
@@ -29,7 +41,7 @@ import { readMergedEnvObject, runtimeEnvForUser } from "./user-env.mjs";
|
|
|
29
41
|
import { sendWecomAppMarkdown, sendWecomGroupMarkdown } from "./wecom.mjs";
|
|
30
42
|
import { getWorkspaceCollaborationByFlow, getWorkspaceCollaborationForProject, listWorkspaceCollaborationsForUser, workspaceCollaborationAccess, workspaceCollaborationSummary } from "./workspace-collaboration.mjs";
|
|
31
43
|
import { FLOW_SOURCE_FILENAME, WORKSPACE_GRAPH_FILENAME, WorkspaceFlowParseError, readWorkspaceGraphFiles, readWorkspaceRunFingerprints, writeWorkspaceGraphFiles } from "./workspace-flow-store.mjs";
|
|
32
|
-
import { createWorkspaceRunController } from "./workspace-run-controller.mjs";
|
|
44
|
+
import { createWorkspaceRunController, terminateWorkspaceChild } from "./workspace-run-controller.mjs";
|
|
33
45
|
import { appendWorkspaceRunLogEvent, createWorkspaceRunLogSession, finishWorkspaceRunLogSession } from "./workspace-run-logs.mjs";
|
|
34
46
|
import { splitWorkspaceGraph } from "./workspace-state.mjs";
|
|
35
47
|
import { getPipelineFiles } from "./workspace-tree.mjs";
|
|
@@ -1122,6 +1134,7 @@ function normalizeWorkspaceGraphPayload(payload) {
|
|
|
1122
1134
|
instances: graph?.instances && typeof graph.instances === "object" && !Array.isArray(graph.instances) ? graph.instances : {},
|
|
1123
1135
|
edges: Array.isArray(graph?.edges) ? graph.edges : [],
|
|
1124
1136
|
ui: graph?.ui && typeof graph.ui === "object" ? graph.ui : { nodePositions: {} },
|
|
1137
|
+
subflows: graph?.subflows && typeof graph.subflows === "object" && !Array.isArray(graph.subflows) ? graph.subflows : {},
|
|
1125
1138
|
updatedAt: new Date().toISOString(),
|
|
1126
1139
|
};
|
|
1127
1140
|
}
|
|
@@ -1130,7 +1143,7 @@ export function workspaceRunTouchedNodeIds(result) {
|
|
|
1130
1143
|
const ids = new Set();
|
|
1131
1144
|
// A deferred run has only executed the prefix ending at the waiting node. Merging the
|
|
1132
1145
|
// complete plan here would overwrite unrelated edits made while Jenkins is running.
|
|
1133
|
-
if (!result?.deferred) {
|
|
1146
|
+
if (!result?.deferred && !(Array.isArray(result?.pauseNodeIds) && result.pauseNodeIds.length)) {
|
|
1134
1147
|
for (const id of Array.isArray(result?.order) ? result.order : []) {
|
|
1135
1148
|
const text = String(id || "").trim();
|
|
1136
1149
|
if (text) ids.add(text);
|
|
@@ -2210,6 +2223,7 @@ function workspaceInstanceText(instance) {
|
|
|
2210
2223
|
function workspaceDisplayKind(definitionId) {
|
|
2211
2224
|
const id = String(definitionId || "");
|
|
2212
2225
|
if (id === "display_markdown") return "markdown";
|
|
2226
|
+
if (id === "display_code") return "code";
|
|
2213
2227
|
if (id === "display_mermaid") return "mermaid";
|
|
2214
2228
|
if (id === "display_ascii") return "ascii";
|
|
2215
2229
|
if (id === "display_html") return "html";
|
|
@@ -2240,6 +2254,7 @@ export function workspaceDisplayTextFilePath(value, kind = "") {
|
|
|
2240
2254
|
html: new Set(["html", "htm"]),
|
|
2241
2255
|
react: new Set(["json", "jsx", "tsx", "js", "txt"]),
|
|
2242
2256
|
markdown: new Set(["md", "markdown", "txt"]),
|
|
2257
|
+
code: new Set(["txt", "js", "jsx", "mjs", "cjs", "ts", "tsx", "py", "kt", "kts", "java", "go", "rs", "sh", "bash", "zsh", "json", "yaml", "yml", "xml", "html", "htm", "css", "scss", "sql", "md"]),
|
|
2243
2258
|
mermaid: new Set(["mmd", "mermaid", "txt"]),
|
|
2244
2259
|
ascii: new Set(["txt", "log"]),
|
|
2245
2260
|
chart: new Set(["json"]),
|
|
@@ -2488,6 +2503,46 @@ export function workspacePublishAgentOutputFiles(structured, runPackage) {
|
|
|
2488
2503
|
: base;
|
|
2489
2504
|
}
|
|
2490
2505
|
|
|
2506
|
+
/**
|
|
2507
|
+
* Node package outputs use files as a transport contract, not as the semantic
|
|
2508
|
+
* value of every slot. Text/json/bool slots receive the file contents; only
|
|
2509
|
+
* file/image slots intentionally expose a published artifact path.
|
|
2510
|
+
*/
|
|
2511
|
+
export function workspaceMaterializeNodePackageOutputValues(structured, runPackage, instance) {
|
|
2512
|
+
if (!structured || !runPackage || !instance) return structured;
|
|
2513
|
+
const slots = (Array.isArray(instance.output) ? instance.output : [])
|
|
2514
|
+
.filter((slot) => !isWorkspaceSemanticOutputSlot(slot));
|
|
2515
|
+
if (!slots.length) return structured;
|
|
2516
|
+
const outParams = { ...(structured.outParams || {}) };
|
|
2517
|
+
let result = structured.result;
|
|
2518
|
+
let resultFile = structured.resultFile;
|
|
2519
|
+
let changed = false;
|
|
2520
|
+
for (const slot of slots) {
|
|
2521
|
+
const name = String(slot?.name || "").trim();
|
|
2522
|
+
if (!name) continue;
|
|
2523
|
+
const type = String(slot?.type || "text").trim().toLowerCase();
|
|
2524
|
+
if (type === "file" || type === "image") continue;
|
|
2525
|
+
const primary = workspaceIsPrimaryOutputSlot(slot, instance.output);
|
|
2526
|
+
const fileKey = `${name}File`;
|
|
2527
|
+
const relPath = primary ? resultFile : outParams[fileKey];
|
|
2528
|
+
const clean = workspaceSafeNodeOutputRelPath(relPath);
|
|
2529
|
+
if (!clean) continue;
|
|
2530
|
+
const source = workspaceNodeOutputCandidates(runPackage, clean)
|
|
2531
|
+
.find((candidate) => fs.existsSync(candidate) && fs.statSync(candidate).isFile());
|
|
2532
|
+
if (!source) continue;
|
|
2533
|
+
const value = fs.readFileSync(source, "utf8");
|
|
2534
|
+
if (primary) {
|
|
2535
|
+
result = value;
|
|
2536
|
+
resultFile = "";
|
|
2537
|
+
} else {
|
|
2538
|
+
outParams[name] = value;
|
|
2539
|
+
delete outParams[fileKey];
|
|
2540
|
+
}
|
|
2541
|
+
changed = true;
|
|
2542
|
+
}
|
|
2543
|
+
return changed ? { ...structured, result, resultFile, outParams } : structured;
|
|
2544
|
+
}
|
|
2545
|
+
|
|
2491
2546
|
function workspaceOutputFieldForSlot(slot, slots = null) {
|
|
2492
2547
|
const name = String(slot?.name || "").trim();
|
|
2493
2548
|
if (!name || workspaceIsPrimaryOutputSlot(slot, slots)) return "result";
|
|
@@ -2601,6 +2656,7 @@ function workspaceDownstreamSlotKind(slot) {
|
|
|
2601
2656
|
const name = String(slot?.name || "").trim().toLowerCase();
|
|
2602
2657
|
const type = String(slot?.type || "").trim().toLowerCase();
|
|
2603
2658
|
if (type === "markdown" || name === "markdown" || name.endsWith("markdown")) return "markdown";
|
|
2659
|
+
if (type === "code" || name === "code" || name.endsWith("code")) return "code";
|
|
2604
2660
|
if (type === "html" || name === "html" || name.endsWith("html")) return "html";
|
|
2605
2661
|
if (type === "mermaid" || name === "mermaid" || name.endsWith("mermaid")) return "mermaid";
|
|
2606
2662
|
if (type === "ascii" || name === "ascii") return "ascii";
|
|
@@ -2642,6 +2698,7 @@ function workspaceResultOutputSpec(graph, nodeId) {
|
|
|
2642
2698
|
html: "html",
|
|
2643
2699
|
react: "json",
|
|
2644
2700
|
markdown: "md",
|
|
2701
|
+
code: "txt",
|
|
2645
2702
|
mermaid: "mmd",
|
|
2646
2703
|
ascii: "txt",
|
|
2647
2704
|
chart: "json",
|
|
@@ -2714,6 +2771,7 @@ function workspaceOutputProtocolRequirements(graph, nodeId) {
|
|
|
2714
2771
|
html: "内容必须是可直接放入 iframe 渲染的 HTML;不要使用 Markdown 代码围栏。",
|
|
2715
2772
|
react: "内容必须是 React 工程 JSON,包含 title、entry、files;files 至少包含 src/App.jsx,可包含 CSS 文件。",
|
|
2716
2773
|
markdown: "内容必须是 Markdown 正文;除非正文确实需要代码块,否则不要额外包裹代码围栏。",
|
|
2774
|
+
code: "内容必须是原始代码文本;不要使用 Markdown 代码围栏,也不要附加解释。",
|
|
2717
2775
|
mermaid: "内容必须是 Mermaid 图表代码,例如 flowchart/sequenceDiagram;不要使用 Markdown 代码围栏。",
|
|
2718
2776
|
ascii: "内容必须是纯文本/ASCII 图或表格;不要输出 HTML 或 Markdown 装饰。",
|
|
2719
2777
|
image: "内容必须是可作为 img src 使用的图片地址、data URL 或 base64 data URL;不要输出 Markdown 图片语法。",
|
|
@@ -2934,6 +2992,50 @@ function workspaceControlIfBranchToSourceHandle(branch) {
|
|
|
2934
2992
|
// 从此对它的搜索全部静默返回空。
|
|
2935
2993
|
const FINGERPRINT_SEP = "\u0000";
|
|
2936
2994
|
|
|
2995
|
+
function workspaceSubflowFingerprintDescriptor(graph, subflowId, stack = new Set()) {
|
|
2996
|
+
const id = String(subflowId || "").trim();
|
|
2997
|
+
const subflow = graph?.subflows?.[id];
|
|
2998
|
+
if (!id || !subflow) return { id, missing: true };
|
|
2999
|
+
if (stack.has(id)) return { id, recursive: true };
|
|
3000
|
+
|
|
3001
|
+
const nextStack = new Set(stack);
|
|
3002
|
+
nextStack.add(id);
|
|
3003
|
+
const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
|
|
3004
|
+
const memberSet = new Set(Array.isArray(subflow.nodeIds) ? subflow.nodeIds.map(String) : []);
|
|
3005
|
+
const memberShape = [...memberSet].sort().map((memberId) => {
|
|
3006
|
+
const member = instances[memberId] || {};
|
|
3007
|
+
return [
|
|
3008
|
+
memberId,
|
|
3009
|
+
String(member.definitionId || ""),
|
|
3010
|
+
String(member.body || ""),
|
|
3011
|
+
String(member.script || ""),
|
|
3012
|
+
String(member.scriptRef || ""),
|
|
3013
|
+
String(member.subflowId || ""),
|
|
3014
|
+
String(member.conditionSubflowId || ""),
|
|
3015
|
+
String(member.bodySubflowId || ""),
|
|
3016
|
+
(member.input || []).map((slot) => [String(slot?.name || ""), String(slot?.type || ""), workspaceSlotValue(slot)]),
|
|
3017
|
+
];
|
|
3018
|
+
});
|
|
3019
|
+
const internalEdges = (graph?.edges || []).filter((edge) => (
|
|
3020
|
+
memberSet.has(String(edge?.source || "")) && memberSet.has(String(edge?.target || ""))
|
|
3021
|
+
));
|
|
3022
|
+
const nestedIds = new Set();
|
|
3023
|
+
for (const memberId of memberSet) {
|
|
3024
|
+
const member = instances[memberId] || {};
|
|
3025
|
+
if (String(member.definitionId || "") === "control_subflow_call" && member.subflowId) {
|
|
3026
|
+
nestedIds.add(String(member.subflowId));
|
|
3027
|
+
}
|
|
3028
|
+
if (String(member.definitionId || "") === "control_while") {
|
|
3029
|
+
if (member.conditionSubflowId) nestedIds.add(String(member.conditionSubflowId));
|
|
3030
|
+
if (member.bodySubflowId) nestedIds.add(String(member.bodySubflowId));
|
|
3031
|
+
}
|
|
3032
|
+
}
|
|
3033
|
+
const nested = [...nestedIds].sort().map((nestedId) => (
|
|
3034
|
+
workspaceSubflowFingerprintDescriptor(graph, nestedId, nextStack)
|
|
3035
|
+
));
|
|
3036
|
+
return { id, subflow, memberShape, internalEdges, nested };
|
|
3037
|
+
}
|
|
3038
|
+
|
|
2937
3039
|
export function workspaceNodeInputFingerprint(graph, nodeId, memo = new Map(), stack = new Set()) {
|
|
2938
3040
|
const id = String(nodeId || "");
|
|
2939
3041
|
if (memo.has(id)) return memo.get(id);
|
|
@@ -2952,6 +3054,20 @@ export function workspaceNodeInputFingerprint(graph, nodeId, memo = new Map(), s
|
|
|
2952
3054
|
marketplaceRef ? "" : String(instance.script || ""),
|
|
2953
3055
|
String(instance.model || ""),
|
|
2954
3056
|
];
|
|
3057
|
+
if (String(instance.definitionId || "") === "control_subflow_call") {
|
|
3058
|
+
const subflowId = String(instance.subflowId || "");
|
|
3059
|
+
parts.push(`subflow=${JSON.stringify(workspaceSubflowFingerprintDescriptor(graph, subflowId))}`);
|
|
3060
|
+
}
|
|
3061
|
+
if (String(instance.definitionId || "") === "control_while") {
|
|
3062
|
+
const conditionSubflowId = String(instance.conditionSubflowId || "");
|
|
3063
|
+
const bodySubflowId = String(instance.bodySubflowId || "");
|
|
3064
|
+
if (conditionSubflowId || bodySubflowId) {
|
|
3065
|
+
parts.push(`whileSubflows=${JSON.stringify({
|
|
3066
|
+
condition: workspaceSubflowFingerprintDescriptor(graph, conditionSubflowId),
|
|
3067
|
+
body: workspaceSubflowFingerprintDescriptor(graph, bodySubflowId),
|
|
3068
|
+
})}`);
|
|
3069
|
+
}
|
|
3070
|
+
}
|
|
2955
3071
|
|
|
2956
3072
|
const incoming = new Map();
|
|
2957
3073
|
for (const edge of Array.isArray(graph?.edges) ? graph.edges : []) {
|
|
@@ -3000,7 +3116,7 @@ function workspaceEdgeHasCachedOutput(graph, edge, scopedRoot = "", opts = {}) {
|
|
|
3000
3116
|
if (!source) return false;
|
|
3001
3117
|
const defId = String(source.definitionId || "");
|
|
3002
3118
|
// provide.* 不执行,它的值就是作者填的;没有「上次跑出来的」这回事,也就无从失效
|
|
3003
|
-
if (defId === "provide_str" || defId === "provide_bool" || defId === "provide_file" || defId === "provide_password") {
|
|
3119
|
+
if (defId === "provide_str" || defId === "provide_json" || defId === "provide_bool" || defId === "provide_file" || defId === "provide_password") {
|
|
3004
3120
|
return Boolean(String(workspaceInstanceText(source) || "").trim());
|
|
3005
3121
|
}
|
|
3006
3122
|
// 有值还不够,得是**这套输入**跑出来的值。指纹对不上说明上游或节点自身改过,重跑
|
|
@@ -4107,7 +4223,7 @@ function isWorkspaceOneClickTaskDefinitionId(definitionId) {
|
|
|
4107
4223
|
|
|
4108
4224
|
function workspaceContextRunDisplayKind(instance) {
|
|
4109
4225
|
const raw = workspaceSlotValue(workspaceSlotByName(instance, "displayType")).trim().toLowerCase();
|
|
4110
|
-
if (["markdown", "html", "react", "table", "chart", "ascii", "mermaid"].includes(raw)) return raw;
|
|
4226
|
+
if (["markdown", "code", "html", "react", "table", "chart", "ascii", "mermaid"].includes(raw)) return raw;
|
|
4111
4227
|
return "markdown";
|
|
4112
4228
|
}
|
|
4113
4229
|
|
|
@@ -4180,7 +4296,7 @@ function buildWorkspaceMcpManifestBlock(results, servers = [], selectedNames = [
|
|
|
4180
4296
|
].join("\n");
|
|
4181
4297
|
}
|
|
4182
4298
|
|
|
4183
|
-
function workspaceWriteDisplayContent(instance, content) {
|
|
4299
|
+
export function workspaceWriteDisplayContent(instance, content) {
|
|
4184
4300
|
const next = { ...(instance || {}) };
|
|
4185
4301
|
const kind = workspaceDisplayKind(next.definitionId);
|
|
4186
4302
|
const unwrapped = workspaceUnwrapOutputEnvelopeForDisplay(content);
|
|
@@ -4189,12 +4305,12 @@ function workspaceWriteDisplayContent(instance, content) {
|
|
|
4189
4305
|
next.displayReloadKey = `${Date.now()}-${crypto.randomBytes(4).toString("hex")}`;
|
|
4190
4306
|
next.body = text;
|
|
4191
4307
|
next.input = (Array.isArray(next.input) ? next.input : []).map((slot) => (
|
|
4192
|
-
String(slot?.name || "") === primaryName
|
|
4308
|
+
String(slot?.name || "") === primaryName
|
|
4193
4309
|
? { ...slot, default: text, value: text }
|
|
4194
4310
|
: slot
|
|
4195
4311
|
));
|
|
4196
4312
|
next.output = (Array.isArray(next.output) ? next.output : []).map((slot) => (
|
|
4197
|
-
String(slot?.name || "") === primaryName
|
|
4313
|
+
String(slot?.name || "") === primaryName
|
|
4198
4314
|
? { ...slot, default: text, value: text }
|
|
4199
4315
|
: slot
|
|
4200
4316
|
));
|
|
@@ -4629,6 +4745,9 @@ async function workspaceRunToolNodejsScript({
|
|
|
4629
4745
|
emit,
|
|
4630
4746
|
signal,
|
|
4631
4747
|
onActiveChild,
|
|
4748
|
+
stderrAsStatus = false,
|
|
4749
|
+
maxStdoutBytes = Infinity,
|
|
4750
|
+
maxStderrBytes = Infinity,
|
|
4632
4751
|
}) {
|
|
4633
4752
|
const scriptRef = String(instance?.scriptRef || "").trim();
|
|
4634
4753
|
const scriptAbs = scriptRef ? workspaceResolveFlowFile(scopedRoot, scriptRef, "scriptRef") : "";
|
|
@@ -4687,6 +4806,9 @@ async function workspaceRunToolNodejsScript({
|
|
|
4687
4806
|
if (typeof onActiveChild === "function") onActiveChild(child, { processGroup });
|
|
4688
4807
|
let stdout = "";
|
|
4689
4808
|
let stderr = "";
|
|
4809
|
+
let stdoutBytes = 0;
|
|
4810
|
+
let stderrBytes = 0;
|
|
4811
|
+
let outputLimitError = null;
|
|
4690
4812
|
let settled = false;
|
|
4691
4813
|
const finish = (callback) => {
|
|
4692
4814
|
if (settled) return;
|
|
@@ -4697,13 +4819,33 @@ async function workspaceRunToolNodejsScript({
|
|
|
4697
4819
|
child.stdout.setEncoding("utf-8");
|
|
4698
4820
|
child.stderr.setEncoding("utf-8");
|
|
4699
4821
|
child.stdout.on("data", (chunk) => {
|
|
4700
|
-
|
|
4822
|
+
if (outputLimitError) return;
|
|
4823
|
+
const text = String(chunk);
|
|
4824
|
+
stdoutBytes += Buffer.byteLength(text, "utf-8");
|
|
4825
|
+
if (stdoutBytes > maxStdoutBytes) {
|
|
4826
|
+
outputLimitError = new Error(`script stdout exceeded ${maxStdoutBytes} bytes`);
|
|
4827
|
+
terminateWorkspaceChild(child, { processGroup });
|
|
4828
|
+
return;
|
|
4829
|
+
}
|
|
4830
|
+
stdout += text;
|
|
4701
4831
|
});
|
|
4702
4832
|
child.stderr.on("data", (chunk) => {
|
|
4703
|
-
|
|
4833
|
+
if (outputLimitError) return;
|
|
4834
|
+
const text = String(chunk);
|
|
4835
|
+
stderrBytes += Buffer.byteLength(text, "utf-8");
|
|
4836
|
+
if (stderrBytes > maxStderrBytes) {
|
|
4837
|
+
outputLimitError = new Error(`script stderr exceeded ${maxStderrBytes} bytes`);
|
|
4838
|
+
terminateWorkspaceChild(child, { processGroup });
|
|
4839
|
+
return;
|
|
4840
|
+
}
|
|
4841
|
+
stderr += text;
|
|
4704
4842
|
});
|
|
4705
4843
|
child.on("error", (error) => finish(() => reject(error)));
|
|
4706
4844
|
child.on("close", (code) => {
|
|
4845
|
+
if (outputLimitError) {
|
|
4846
|
+
finish(() => reject(outputLimitError));
|
|
4847
|
+
return;
|
|
4848
|
+
}
|
|
4707
4849
|
if (signal?.aborted) {
|
|
4708
4850
|
finish(() => {
|
|
4709
4851
|
const error = new Error("Workspace run stopped");
|
|
@@ -4713,7 +4855,8 @@ async function workspaceRunToolNodejsScript({
|
|
|
4713
4855
|
return;
|
|
4714
4856
|
}
|
|
4715
4857
|
if (stderr.trim()) {
|
|
4716
|
-
emit?.({ type: "
|
|
4858
|
+
if (stderrAsStatus) emit?.({ type: "status", line: `[step stderr] ${stderr.trim().slice(-4000)}` });
|
|
4859
|
+
else emit?.({ type: "natural", kind: "warning", text: `[script stderr]\n${stderr.trim().slice(-4000)}` });
|
|
4717
4860
|
}
|
|
4718
4861
|
if (code !== 0) {
|
|
4719
4862
|
finish(() => reject(new Error(`tool_nodejs script exited ${code}${stderr.trim() ? `: ${stderr.trim().slice(-800)}` : ""}`)));
|
|
@@ -4739,6 +4882,140 @@ function workspaceNodeModelKey(instance, fallback = "") {
|
|
|
4739
4882
|
return String(fallback || "").trim();
|
|
4740
4883
|
}
|
|
4741
4884
|
|
|
4885
|
+
function workspaceAssertWhileSubflowContract(graph, nodeId, conditionSubflowId, bodySubflowId) {
|
|
4886
|
+
const conditionId = String(conditionSubflowId || "").trim();
|
|
4887
|
+
const bodyId = String(bodySubflowId || "").trim();
|
|
4888
|
+
if (!conditionId || !bodyId) {
|
|
4889
|
+
throw new Error(`control.while ${nodeId} requires both conditionSubflowId and bodySubflowId`);
|
|
4890
|
+
}
|
|
4891
|
+
if (conditionId === bodyId) {
|
|
4892
|
+
throw new Error(`control.while ${nodeId} condition and body must be different subflows`);
|
|
4893
|
+
}
|
|
4894
|
+
const condition = graph?.subflows?.[conditionId];
|
|
4895
|
+
const body = graph?.subflows?.[bodyId];
|
|
4896
|
+
if (!condition) throw new Error(`control.while ${nodeId} references missing condition subflow ${conditionId}`);
|
|
4897
|
+
if (!body) throw new Error(`control.while ${nodeId} references missing body subflow ${bodyId}`);
|
|
4898
|
+
for (const name of ["state", "iteration"]) {
|
|
4899
|
+
if (!condition.inputs?.[name]) throw new Error(`control.while ${nodeId} condition subflow ${conditionId} requires input ${name}`);
|
|
4900
|
+
}
|
|
4901
|
+
if (!condition.outputs?.decision) {
|
|
4902
|
+
throw new Error(`control.while ${nodeId} condition subflow ${conditionId} requires output decision`);
|
|
4903
|
+
}
|
|
4904
|
+
for (const name of ["state", "iteration", "idempotencyKey"]) {
|
|
4905
|
+
if (!body.inputs?.[name]) throw new Error(`control.while ${nodeId} body subflow ${bodyId} requires input ${name}`);
|
|
4906
|
+
}
|
|
4907
|
+
if (!body.outputs?.state) {
|
|
4908
|
+
throw new Error(`control.while ${nodeId} body subflow ${bodyId} requires output state`);
|
|
4909
|
+
}
|
|
4910
|
+
return { conditionId, bodyId, condition, body };
|
|
4911
|
+
}
|
|
4912
|
+
|
|
4913
|
+
async function workspaceRunSubflowFrame({
|
|
4914
|
+
root,
|
|
4915
|
+
scopedRoot,
|
|
4916
|
+
payload,
|
|
4917
|
+
userCtx,
|
|
4918
|
+
opts,
|
|
4919
|
+
graph,
|
|
4920
|
+
parentNodeId,
|
|
4921
|
+
parentDefinitionId,
|
|
4922
|
+
subflowId,
|
|
4923
|
+
inputValues = {},
|
|
4924
|
+
signal = null,
|
|
4925
|
+
emit,
|
|
4926
|
+
eventContext = {},
|
|
4927
|
+
}) {
|
|
4928
|
+
const id = String(subflowId || "").trim();
|
|
4929
|
+
const subflow = graph?.subflows?.[id];
|
|
4930
|
+
if (!subflow) throw new Error(`Subflow ${id || "(empty)"} does not exist`);
|
|
4931
|
+
const callStack = Array.isArray(opts?.subflowCallStack) ? opts.subflowCallStack : [];
|
|
4932
|
+
if (callStack.includes(id)) {
|
|
4933
|
+
throw new Error(`Recursive subflow call is not allowed: ${[...callStack, id].join(" -> ")}`);
|
|
4934
|
+
}
|
|
4935
|
+
|
|
4936
|
+
const memberSet = new Set((subflow.nodeIds || []).map(String));
|
|
4937
|
+
if (!memberSet.size) throw new Error(`Subflow ${id} has no member nodes`);
|
|
4938
|
+
const callFrameId = `${parentNodeId}:${String(eventContext?.whileRole || "call")}:${String(eventContext?.iteration || "0")}:${crypto.randomBytes(6).toString("hex")}`;
|
|
4939
|
+
const childGraph = typeof structuredClone === "function"
|
|
4940
|
+
? structuredClone(graph)
|
|
4941
|
+
: JSON.parse(JSON.stringify(graph));
|
|
4942
|
+
const syntheticRunId = `__subflow_run_${crypto.randomBytes(6).toString("hex")}`;
|
|
4943
|
+
childGraph.instances[syntheticRunId] = {
|
|
4944
|
+
definitionId: "workspace_run",
|
|
4945
|
+
label: `${subflow.label || id} · call frame`,
|
|
4946
|
+
input: [{ type: "node", name: "prev", value: "" }],
|
|
4947
|
+
output: [{ type: "node", name: "next", value: "" }],
|
|
4948
|
+
};
|
|
4949
|
+
childGraph.edges = (childGraph.edges || []).filter((edge) => (
|
|
4950
|
+
memberSet.has(String(edge?.source || "")) && memberSet.has(String(edge?.target || ""))
|
|
4951
|
+
));
|
|
4952
|
+
for (const rootId of subflow.roots || []) {
|
|
4953
|
+
const rootInstance = childGraph.instances?.[rootId];
|
|
4954
|
+
const prevIndex = (rootInstance?.input || []).findIndex((slot) => String(slot?.name || "") === "prev");
|
|
4955
|
+
if (prevIndex < 0) throw new Error(`Subflow ${id} root ${rootId} has no prev input`);
|
|
4956
|
+
childGraph.edges.push({
|
|
4957
|
+
source: syntheticRunId,
|
|
4958
|
+
target: rootId,
|
|
4959
|
+
sourceHandle: "output-0",
|
|
4960
|
+
targetHandle: `input-${prevIndex}`,
|
|
4961
|
+
});
|
|
4962
|
+
}
|
|
4963
|
+
for (const [name, binding] of Object.entries(subflow.inputs || {})) {
|
|
4964
|
+
const proxy = childGraph.instances?.[binding.nodeId];
|
|
4965
|
+
if (!proxy) throw new Error(`Subflow ${id} input ${name} references missing proxy ${binding.nodeId}`);
|
|
4966
|
+
childGraph.instances[binding.nodeId] = workspaceSetOutputSlot(proxy, binding.slot || "value", inputValues[name] ?? "");
|
|
4967
|
+
}
|
|
4968
|
+
|
|
4969
|
+
const frameContext = {
|
|
4970
|
+
...eventContext,
|
|
4971
|
+
parentNodeId,
|
|
4972
|
+
subflowId: id,
|
|
4973
|
+
callFrameId,
|
|
4974
|
+
};
|
|
4975
|
+
emit({
|
|
4976
|
+
type: "subflow-start",
|
|
4977
|
+
nodeId: parentNodeId,
|
|
4978
|
+
definitionId: parentDefinitionId,
|
|
4979
|
+
inputs: Object.keys(subflow.inputs || {}),
|
|
4980
|
+
...frameContext,
|
|
4981
|
+
});
|
|
4982
|
+
const childResult = await runWorkspaceGraph(root, scopedRoot, {
|
|
4983
|
+
...payload,
|
|
4984
|
+
graph: childGraph,
|
|
4985
|
+
runNodeId: syntheticRunId,
|
|
4986
|
+
ignoreCache: true,
|
|
4987
|
+
forceNodeIds: [],
|
|
4988
|
+
}, userCtx, {
|
|
4989
|
+
...opts,
|
|
4990
|
+
signal: signal || opts?.signal || null,
|
|
4991
|
+
subflowCallStack: [...callStack, id],
|
|
4992
|
+
onEvent: (event) => {
|
|
4993
|
+
if (event?.type === "graph") return;
|
|
4994
|
+
emit({ ...event, ...frameContext });
|
|
4995
|
+
},
|
|
4996
|
+
});
|
|
4997
|
+
if (childResult.deferred || childResult.pauseNodeIds?.length) {
|
|
4998
|
+
throw new Error(`Subflow ${id} paused or deferred; resumable subflow call frames are not supported yet`);
|
|
4999
|
+
}
|
|
5000
|
+
for (const memberId of memberSet) {
|
|
5001
|
+
if (childResult.graph.instances?.[memberId]) graph.instances[memberId] = childResult.graph.instances[memberId];
|
|
5002
|
+
}
|
|
5003
|
+
const resultValues = {};
|
|
5004
|
+
for (const [name, binding] of Object.entries(subflow.outputs || {})) {
|
|
5005
|
+
const source = childResult.graph.instances?.[binding.nodeId];
|
|
5006
|
+
const slot = (source?.output || []).find((item) => String(item?.name || "") === String(binding.slot || ""));
|
|
5007
|
+
resultValues[name] = workspaceSlotValue(slot);
|
|
5008
|
+
}
|
|
5009
|
+
emit({
|
|
5010
|
+
type: "subflow-done",
|
|
5011
|
+
nodeId: parentNodeId,
|
|
5012
|
+
definitionId: parentDefinitionId,
|
|
5013
|
+
outputs: Object.keys(resultValues),
|
|
5014
|
+
...frameContext,
|
|
5015
|
+
});
|
|
5016
|
+
return { resultValues, callFrameId };
|
|
5017
|
+
}
|
|
5018
|
+
|
|
4742
5019
|
export async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts = {}) {
|
|
4743
5020
|
const graph = hydrateWorkspaceGraphForRuntime(root, {
|
|
4744
5021
|
root: scopedRoot,
|
|
@@ -4747,8 +5024,11 @@ export async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {},
|
|
|
4747
5024
|
archived: payload.archived === true || payload.flowArchived === true,
|
|
4748
5025
|
}, payload.graph || {}, userCtx);
|
|
4749
5026
|
const runNodeId = String(payload?.runNodeId || "").trim();
|
|
5027
|
+
const forceNodeIds = new Set((Array.isArray(payload?.forceNodeIds) ? payload.forceNodeIds : [])
|
|
5028
|
+
.map((nodeId) => String(nodeId || "").trim())
|
|
5029
|
+
.filter(Boolean));
|
|
4750
5030
|
const { order, pauseNodeIds } = workspaceRunPlan(graph, runNodeId, scopedRoot, {
|
|
4751
|
-
forceNodeIds
|
|
5031
|
+
forceNodeIds,
|
|
4752
5032
|
ignoreCache: payload?.ignoreCache === true,
|
|
4753
5033
|
});
|
|
4754
5034
|
// 指纹只由「节点定义 + 上游指纹 + 没接线的槽位值」决定,运行过程中这些都不变,所以
|
|
@@ -4830,6 +5110,7 @@ export async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {},
|
|
|
4830
5110
|
const runTmpRoot = workspaceCreateRunTmpRoot(scopedRoot, runNodeId);
|
|
4831
5111
|
const controlBranches = new Map();
|
|
4832
5112
|
const skippedNodes = new Set();
|
|
5113
|
+
const runtimePauseNodeIds = [];
|
|
4833
5114
|
let deferred = null;
|
|
4834
5115
|
const incomingControlEdgesByTarget = new Map();
|
|
4835
5116
|
for (const edge of Array.isArray(graph?.edges) ? graph.edges : []) {
|
|
@@ -4887,6 +5168,45 @@ export async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {},
|
|
|
4887
5168
|
continue;
|
|
4888
5169
|
}
|
|
4889
5170
|
|
|
5171
|
+
if (defId === "workspace_subflow_input") {
|
|
5172
|
+
const value = workspaceSlotValue((instance.output || []).find((slot) => String(slot?.name || "") === "value"));
|
|
5173
|
+
publishNodeOutput(nodeId, value, { emitGraph: true });
|
|
5174
|
+
emit({ type: "node-done", nodeId, definitionId: defId });
|
|
5175
|
+
continue;
|
|
5176
|
+
}
|
|
5177
|
+
|
|
5178
|
+
if (defId === "control_subflow_call") {
|
|
5179
|
+
const subflowId = String(instance.subflowId || "").trim();
|
|
5180
|
+
const subflow = graph?.subflows?.[subflowId];
|
|
5181
|
+
if (!subflow) throw new Error(`flow.call ${nodeId} references missing subflow ${subflowId || "(empty)"}`);
|
|
5182
|
+
const inputValues = workspaceInputValues(graph, nodeId, outputs, scopedRoot);
|
|
5183
|
+
const { resultValues, callFrameId } = await workspaceRunSubflowFrame({
|
|
5184
|
+
root,
|
|
5185
|
+
scopedRoot,
|
|
5186
|
+
payload,
|
|
5187
|
+
userCtx,
|
|
5188
|
+
opts,
|
|
5189
|
+
graph,
|
|
5190
|
+
parentNodeId: nodeId,
|
|
5191
|
+
parentDefinitionId: defId,
|
|
5192
|
+
subflowId,
|
|
5193
|
+
inputValues,
|
|
5194
|
+
signal,
|
|
5195
|
+
emit,
|
|
5196
|
+
});
|
|
5197
|
+
let nextInstance = instance;
|
|
5198
|
+
for (const [name, value] of Object.entries(resultValues)) {
|
|
5199
|
+
nextInstance = workspaceSetOutputSlot(nextInstance, name, value);
|
|
5200
|
+
}
|
|
5201
|
+
graph.instances[nodeId] = nextInstance;
|
|
5202
|
+
const primaryName = Object.keys(subflow.outputs || {})[0] || "";
|
|
5203
|
+
const primaryValue = primaryName ? resultValues[primaryName] : "";
|
|
5204
|
+
const updatedDisplays = publishNodeOutput(nodeId, primaryValue);
|
|
5205
|
+
emit({ type: "graph", nodeId, displayNodeIds: updatedDisplays, graph });
|
|
5206
|
+
emit({ type: "node-done", nodeId, definitionId: defId, subflowId, callFrameId });
|
|
5207
|
+
continue;
|
|
5208
|
+
}
|
|
5209
|
+
|
|
4890
5210
|
if (defId === "control_load_skills") {
|
|
4891
5211
|
const skillStartedAt = Date.now();
|
|
4892
5212
|
const nodeSkillKeys = selectedSkillKeysFromInstance(instance);
|
|
@@ -4956,6 +5276,252 @@ export async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {},
|
|
|
4956
5276
|
continue;
|
|
4957
5277
|
}
|
|
4958
5278
|
|
|
5279
|
+
if (defId === "control_parse_json") {
|
|
5280
|
+
const inputValues = workspaceInputValues(graph, nodeId, outputs, scopedRoot);
|
|
5281
|
+
const raw = String(inputValues.value ?? "").trim();
|
|
5282
|
+
if (!raw) throw new Error(`control.parseJson ${nodeId} requires a non-empty value`);
|
|
5283
|
+
let parsed;
|
|
5284
|
+
try {
|
|
5285
|
+
parsed = JSON.parse(raw);
|
|
5286
|
+
} catch (error) {
|
|
5287
|
+
throw new Error(`control.parseJson ${nodeId} received invalid JSON: ${error.message}`);
|
|
5288
|
+
}
|
|
5289
|
+
const content = JSON.stringify(parsed);
|
|
5290
|
+
graph.instances[nodeId] = workspaceSetOutputSlot(instance, "result", content);
|
|
5291
|
+
publishNodeOutput(nodeId, content, { emitGraph: true });
|
|
5292
|
+
emit({ type: "node-done", nodeId, definitionId: defId });
|
|
5293
|
+
continue;
|
|
5294
|
+
}
|
|
5295
|
+
|
|
5296
|
+
if (defId === "control_while") {
|
|
5297
|
+
const inputValues = workspaceInputValues(graph, nodeId, outputs, scopedRoot);
|
|
5298
|
+
const config = normalizeControlWhileConfig(inputValues);
|
|
5299
|
+
const stepScript = String(instance.script || instance.body || "").trim();
|
|
5300
|
+
const stepScriptRef = String(instance.scriptRef || "").trim();
|
|
5301
|
+
const conditionSubflowId = String(instance.conditionSubflowId || "").trim();
|
|
5302
|
+
const bodySubflowId = String(instance.bodySubflowId || "").trim();
|
|
5303
|
+
const usesSubflows = Boolean(conditionSubflowId || bodySubflowId);
|
|
5304
|
+
const whileSubflows = usesSubflows
|
|
5305
|
+
? workspaceAssertWhileSubflowContract(graph, nodeId, conditionSubflowId, bodySubflowId)
|
|
5306
|
+
: null;
|
|
5307
|
+
if (!usesSubflows && !stepScript && !stepScriptRef) {
|
|
5308
|
+
throw new Error(`control.while ${nodeId} requires condition/body subflows or a step command/scriptRef`);
|
|
5309
|
+
}
|
|
5310
|
+
const previousDecision = workspaceSlotValue(workspaceSlotByName(instance, "decision")).trim().toLowerCase();
|
|
5311
|
+
const checkpointState = workspaceSlotValue(
|
|
5312
|
+
(Array.isArray(instance.output) ? instance.output : []).find((slot) => String(slot?.name || "") === "state"),
|
|
5313
|
+
);
|
|
5314
|
+
const checkpointFingerprint = workspaceSlotValue(
|
|
5315
|
+
(Array.isArray(instance.output) ? instance.output : []).find((slot) => String(slot?.name || "") === "checkpointFingerprint"),
|
|
5316
|
+
);
|
|
5317
|
+
const inputState = normalizeControlWhileInitialState(inputValues.state);
|
|
5318
|
+
const expectedCheckpointFingerprint = controlWhileCheckpointFingerprint({
|
|
5319
|
+
inputFingerprint: plannedFingerprints.get(nodeId) || "",
|
|
5320
|
+
initialState: inputState,
|
|
5321
|
+
});
|
|
5322
|
+
const checkpoint = resolveControlWhileCheckpoint({
|
|
5323
|
+
previousDecision,
|
|
5324
|
+
state: checkpointState,
|
|
5325
|
+
history: workspaceSlotValue(workspaceSlotByName(instance, "history")),
|
|
5326
|
+
iterations: workspaceSlotValue(workspaceSlotByName(instance, "iterations")),
|
|
5327
|
+
fingerprint: checkpointFingerprint,
|
|
5328
|
+
expectedFingerprint: expectedCheckpointFingerprint,
|
|
5329
|
+
allowResume: payload?.ignoreCache !== true && !forceNodeIds.has(nodeId),
|
|
5330
|
+
});
|
|
5331
|
+
const initialState = checkpoint.resumable ? checkpoint.state : inputState;
|
|
5332
|
+
if (checkpoint.resumable) {
|
|
5333
|
+
emit({ type: "status", nodeId, line: "control.while resume from waiting checkpoint" });
|
|
5334
|
+
} else if (previousDecision === "wait") {
|
|
5335
|
+
emit({ type: "status", nodeId, line: `control.while reset waiting checkpoint: ${checkpoint.reason}` });
|
|
5336
|
+
}
|
|
5337
|
+
const loop = await runControlWhile({
|
|
5338
|
+
initialState,
|
|
5339
|
+
initialHistory: checkpoint.resumable ? checkpoint.history : [],
|
|
5340
|
+
initialElapsedMs: checkpoint.resumable ? checkpoint.elapsedMs : 0,
|
|
5341
|
+
startIteration: checkpoint.resumable ? checkpoint.nextIteration : 1,
|
|
5342
|
+
...config,
|
|
5343
|
+
signal,
|
|
5344
|
+
idempotencyKeyForIteration: ({ iteration }) => controlWhileIdempotencyKey({
|
|
5345
|
+
checkpointFingerprint: expectedCheckpointFingerprint,
|
|
5346
|
+
nodeId,
|
|
5347
|
+
iteration,
|
|
5348
|
+
}),
|
|
5349
|
+
onIterationStart: ({ iteration, remainingMs, idempotencyKey }) => {
|
|
5350
|
+
emit({
|
|
5351
|
+
type: "while-iteration-start",
|
|
5352
|
+
nodeId,
|
|
5353
|
+
definitionId: defId,
|
|
5354
|
+
iteration,
|
|
5355
|
+
maxIterations: config.maxIterations,
|
|
5356
|
+
remainingMs,
|
|
5357
|
+
idempotencyKey,
|
|
5358
|
+
});
|
|
5359
|
+
emit({ type: "status", nodeId, line: `control.while iteration ${iteration}/${config.maxIterations}` });
|
|
5360
|
+
},
|
|
5361
|
+
onIterationDone: ({ iteration, decision, summary, elapsedMs, idempotencyKey }) => {
|
|
5362
|
+
emit({
|
|
5363
|
+
type: "while-iteration-done",
|
|
5364
|
+
nodeId,
|
|
5365
|
+
definitionId: defId,
|
|
5366
|
+
iteration,
|
|
5367
|
+
decision,
|
|
5368
|
+
summary,
|
|
5369
|
+
elapsedMs,
|
|
5370
|
+
idempotencyKey,
|
|
5371
|
+
});
|
|
5372
|
+
emit({ type: "status", nodeId, line: `control.while iteration ${iteration}: ${decision}${summary ? ` · ${summary}` : ""}` });
|
|
5373
|
+
},
|
|
5374
|
+
executeStep: async ({ iteration, state, signal: stepSignal, idempotencyKey }) => {
|
|
5375
|
+
const stateText = serializeControlWhileState(state);
|
|
5376
|
+
const iterationInputs = {
|
|
5377
|
+
...inputValues,
|
|
5378
|
+
state: stateText,
|
|
5379
|
+
iteration: String(iteration),
|
|
5380
|
+
};
|
|
5381
|
+
if (whileSubflows) {
|
|
5382
|
+
const conditionFrame = await workspaceRunSubflowFrame({
|
|
5383
|
+
root,
|
|
5384
|
+
scopedRoot,
|
|
5385
|
+
payload,
|
|
5386
|
+
userCtx,
|
|
5387
|
+
opts,
|
|
5388
|
+
graph,
|
|
5389
|
+
parentNodeId: nodeId,
|
|
5390
|
+
parentDefinitionId: defId,
|
|
5391
|
+
subflowId: whileSubflows.conditionId,
|
|
5392
|
+
inputValues: {
|
|
5393
|
+
state: stateText,
|
|
5394
|
+
iteration: String(iteration),
|
|
5395
|
+
idempotencyKey,
|
|
5396
|
+
},
|
|
5397
|
+
signal: stepSignal,
|
|
5398
|
+
emit,
|
|
5399
|
+
eventContext: { iteration, whileRole: "condition" },
|
|
5400
|
+
});
|
|
5401
|
+
const conditionStep = parseControlWhileStepResult(JSON.stringify({
|
|
5402
|
+
decision: String(conditionFrame.resultValues.decision || "").trim().toLowerCase(),
|
|
5403
|
+
summary: String(conditionFrame.resultValues.summary || ""),
|
|
5404
|
+
}), state);
|
|
5405
|
+
if (conditionStep.decision !== "continue") return JSON.stringify(conditionStep);
|
|
5406
|
+
|
|
5407
|
+
const bodyFrame = await workspaceRunSubflowFrame({
|
|
5408
|
+
root,
|
|
5409
|
+
scopedRoot,
|
|
5410
|
+
payload,
|
|
5411
|
+
userCtx,
|
|
5412
|
+
opts,
|
|
5413
|
+
graph,
|
|
5414
|
+
parentNodeId: nodeId,
|
|
5415
|
+
parentDefinitionId: defId,
|
|
5416
|
+
subflowId: whileSubflows.bodyId,
|
|
5417
|
+
inputValues: {
|
|
5418
|
+
state: stateText,
|
|
5419
|
+
iteration: String(iteration),
|
|
5420
|
+
idempotencyKey,
|
|
5421
|
+
},
|
|
5422
|
+
signal: stepSignal,
|
|
5423
|
+
emit,
|
|
5424
|
+
eventContext: { iteration, whileRole: "body" },
|
|
5425
|
+
});
|
|
5426
|
+
const nextStateText = String(bodyFrame.resultValues.state || "").trim();
|
|
5427
|
+
if (!nextStateText) {
|
|
5428
|
+
throw new Error(`control.while ${nodeId} body subflow ${whileSubflows.bodyId} returned an empty state`);
|
|
5429
|
+
}
|
|
5430
|
+
const nextState = normalizeControlWhileInitialState(nextStateText);
|
|
5431
|
+
return JSON.stringify({
|
|
5432
|
+
decision: "continue",
|
|
5433
|
+
state: nextState,
|
|
5434
|
+
summary: String(bodyFrame.resultValues.summary || conditionStep.summary || ""),
|
|
5435
|
+
});
|
|
5436
|
+
}
|
|
5437
|
+
const runPackage = workspaceCreateNodeRunPackage(runTmpRoot, `${nodeId}-iteration-${iteration}`, {
|
|
5438
|
+
scopedRoot,
|
|
5439
|
+
cwd,
|
|
5440
|
+
task: stepScript || stepScriptRef,
|
|
5441
|
+
inputValues: iterationInputs,
|
|
5442
|
+
});
|
|
5443
|
+
const runtimeInputValues = { ...iterationInputs, ...(runPackage.inputValues || {}) };
|
|
5444
|
+
let activeStepChild = null;
|
|
5445
|
+
let activeStepChildOptions = {};
|
|
5446
|
+
const terminateStep = () => terminateWorkspaceChild(activeStepChild, activeStepChildOptions);
|
|
5447
|
+
stepSignal?.addEventListener("abort", terminateStep, { once: true });
|
|
5448
|
+
try {
|
|
5449
|
+
const content = await workspaceRunToolNodejsScript({
|
|
5450
|
+
scopedRoot,
|
|
5451
|
+
cwd,
|
|
5452
|
+
instance: { ...instance, script: stepScript, scriptRef: stepScript ? "" : stepScriptRef },
|
|
5453
|
+
inputValues: runtimeInputValues,
|
|
5454
|
+
runPackage,
|
|
5455
|
+
userCtx,
|
|
5456
|
+
envOverlay: {
|
|
5457
|
+
...runEnv,
|
|
5458
|
+
AGENTFLOW_WHILE_STATE: stateText,
|
|
5459
|
+
AGENTFLOW_WHILE_ITERATION: String(iteration),
|
|
5460
|
+
AGENTFLOW_WHILE_MAX_ITERATIONS: String(config.maxIterations),
|
|
5461
|
+
AGENTFLOW_WHILE_TIMEOUT_MS: String(config.timeoutMs),
|
|
5462
|
+
AGENTFLOW_WHILE_IDEMPOTENCY_KEY: idempotencyKey,
|
|
5463
|
+
},
|
|
5464
|
+
emit: (event) => emit({ ...event, nodeId, iteration }),
|
|
5465
|
+
signal: stepSignal,
|
|
5466
|
+
stderrAsStatus: true,
|
|
5467
|
+
maxStdoutBytes: MAX_CONTROL_WHILE_STEP_STDOUT_BYTES,
|
|
5468
|
+
maxStderrBytes: MAX_CONTROL_WHILE_STEP_STDERR_BYTES,
|
|
5469
|
+
onActiveChild: (child, childOptions = {}) => {
|
|
5470
|
+
activeStepChild = child || null;
|
|
5471
|
+
activeStepChildOptions = childOptions;
|
|
5472
|
+
if (stepSignal?.aborted && child) terminateStep();
|
|
5473
|
+
if (typeof opts.onActiveChild === "function") opts.onActiveChild(child, childOptions);
|
|
5474
|
+
},
|
|
5475
|
+
});
|
|
5476
|
+
return workspaceStructuredAgentOutput(content).result || content;
|
|
5477
|
+
} finally {
|
|
5478
|
+
stepSignal?.removeEventListener("abort", terminateStep);
|
|
5479
|
+
}
|
|
5480
|
+
},
|
|
5481
|
+
});
|
|
5482
|
+
const resultContent = JSON.stringify({
|
|
5483
|
+
decision: loop.decision,
|
|
5484
|
+
iterations: loop.iterations,
|
|
5485
|
+
state: loop.state,
|
|
5486
|
+
summary: loop.summary,
|
|
5487
|
+
history: loop.history,
|
|
5488
|
+
});
|
|
5489
|
+
let nextInstance = workspaceSetOutputSlot(instance, "result", resultContent);
|
|
5490
|
+
nextInstance = workspaceSetOutputSlot(nextInstance, "state", serializeControlWhileState(loop.state));
|
|
5491
|
+
nextInstance = workspaceSetOutputSlot(nextInstance, "decision", loop.decision);
|
|
5492
|
+
nextInstance = workspaceSetOutputSlot(nextInstance, "iterations", String(loop.iterations));
|
|
5493
|
+
nextInstance = workspaceSetOutputSlot(nextInstance, "summary", loop.summary);
|
|
5494
|
+
nextInstance = workspaceSetOutputSlot(nextInstance, "history", JSON.stringify(loop.history));
|
|
5495
|
+
nextInstance = workspaceSetOutputSlot(nextInstance, "checkpointFingerprint", expectedCheckpointFingerprint);
|
|
5496
|
+
graph.instances[nodeId] = nextInstance;
|
|
5497
|
+
const updatedDisplays = publishNodeOutput(nodeId, resultContent);
|
|
5498
|
+
emit({ type: "graph", nodeId, displayNodeIds: updatedDisplays, graph });
|
|
5499
|
+
if (loop.decision === "fail") {
|
|
5500
|
+
emit({
|
|
5501
|
+
type: "node-failed",
|
|
5502
|
+
nodeId,
|
|
5503
|
+
definitionId: defId,
|
|
5504
|
+
decision: loop.decision,
|
|
5505
|
+
iterations: loop.iterations,
|
|
5506
|
+
summary: loop.summary,
|
|
5507
|
+
});
|
|
5508
|
+
throw new Error(loop.summary || "control.while step failed");
|
|
5509
|
+
}
|
|
5510
|
+
emit({
|
|
5511
|
+
type: "node-done",
|
|
5512
|
+
nodeId,
|
|
5513
|
+
definitionId: defId,
|
|
5514
|
+
decision: loop.decision,
|
|
5515
|
+
iterations: loop.iterations,
|
|
5516
|
+
summary: loop.summary,
|
|
5517
|
+
});
|
|
5518
|
+
if (loop.decision === "wait") {
|
|
5519
|
+
runtimePauseNodeIds.push(nodeId);
|
|
5520
|
+
break;
|
|
5521
|
+
}
|
|
5522
|
+
continue;
|
|
5523
|
+
}
|
|
5524
|
+
|
|
4959
5525
|
if (defId === "provide_str" || defId === "provide_password") {
|
|
4960
5526
|
const content = workspaceInstanceText(instance);
|
|
4961
5527
|
publishNodeOutput(nodeId, content, { emitGraph: true });
|
|
@@ -4963,6 +5529,21 @@ export async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {},
|
|
|
4963
5529
|
continue;
|
|
4964
5530
|
}
|
|
4965
5531
|
|
|
5532
|
+
if (defId === "provide_json") {
|
|
5533
|
+
const raw = workspaceSlotValue(Array.isArray(instance.output) ? instance.output[0] : null) || workspaceInstanceText(instance);
|
|
5534
|
+
let parsed;
|
|
5535
|
+
try {
|
|
5536
|
+
parsed = JSON.parse(String(raw || "").trim());
|
|
5537
|
+
} catch (error) {
|
|
5538
|
+
throw new Error(`provide.json ${nodeId} received invalid JSON: ${error.message}`);
|
|
5539
|
+
}
|
|
5540
|
+
const content = JSON.stringify(parsed);
|
|
5541
|
+
graph.instances[nodeId] = workspaceSetOutputSlot(instance, "value", content);
|
|
5542
|
+
publishNodeOutput(nodeId, content, { emitGraph: true });
|
|
5543
|
+
emit({ type: "node-done", nodeId, definitionId: defId });
|
|
5544
|
+
continue;
|
|
5545
|
+
}
|
|
5546
|
+
|
|
4966
5547
|
if (defId === "provide_bool") {
|
|
4967
5548
|
const raw = workspaceSlotValue(Array.isArray(instance.output) ? instance.output[0] : null) || workspaceInstanceText(instance);
|
|
4968
5549
|
const content = ["true", "1", "yes", "on"].includes(String(raw || "").trim().toLowerCase()) ? "true" : "false";
|
|
@@ -5446,7 +6027,11 @@ export async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {},
|
|
|
5446
6027
|
signal,
|
|
5447
6028
|
onActiveChild: opts.onActiveChild,
|
|
5448
6029
|
});
|
|
5449
|
-
const
|
|
6030
|
+
const structuredAgentOutput = workspaceStructuredAgentOutput(content);
|
|
6031
|
+
const nodeOutput = String(instance.marketplaceRef || "").trim()
|
|
6032
|
+
? workspaceMaterializeNodePackageOutputValues(structuredAgentOutput, runPackage, instance)
|
|
6033
|
+
: structuredAgentOutput;
|
|
6034
|
+
const normalizedAgentOutput = workspacePublishAgentOutputFiles(nodeOutput, runPackage);
|
|
5450
6035
|
const resultContent = normalizedAgentOutput.result || content;
|
|
5451
6036
|
recordNodeOutput(nodeId, resultContent);
|
|
5452
6037
|
const slotUpdate = workspaceApplyAgentOutputSlots(instance, normalizedAgentOutput);
|
|
@@ -5646,11 +6231,12 @@ export async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {},
|
|
|
5646
6231
|
workspaceCleanupAutoWorktrees(autoCleanupWorktrees, graph, emit);
|
|
5647
6232
|
workspaceCleanupTmpRoot(runTmpRoot, userCtx, emit);
|
|
5648
6233
|
}
|
|
5649
|
-
|
|
5650
|
-
|
|
6234
|
+
const finalPauseNodeIds = Array.from(new Set([...pauseNodeIds, ...runtimePauseNodeIds]));
|
|
6235
|
+
if (!deferred && finalPauseNodeIds.length > 0) {
|
|
6236
|
+
emit({ type: "paused", nodeIds: finalPauseNodeIds, message: `Workspace run paused at ${finalPauseNodeIds.join(", ")}` });
|
|
5651
6237
|
}
|
|
5652
6238
|
graph.updatedAt = new Date().toISOString();
|
|
5653
|
-
return { graph, events, order, pauseNodeIds, deferred };
|
|
6239
|
+
return { graph, events, order, pauseNodeIds: finalPauseNodeIds, deferred };
|
|
5654
6240
|
}
|
|
5655
6241
|
|
|
5656
6242
|
export function isWorkspaceRunAbortError(err) {
|