@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
|
@@ -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);
|
|
@@ -2490,6 +2503,46 @@ export function workspacePublishAgentOutputFiles(structured, runPackage) {
|
|
|
2490
2503
|
: base;
|
|
2491
2504
|
}
|
|
2492
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
|
+
|
|
2493
2546
|
function workspaceOutputFieldForSlot(slot, slots = null) {
|
|
2494
2547
|
const name = String(slot?.name || "").trim();
|
|
2495
2548
|
if (!name || workspaceIsPrimaryOutputSlot(slot, slots)) return "result";
|
|
@@ -2939,6 +2992,50 @@ function workspaceControlIfBranchToSourceHandle(branch) {
|
|
|
2939
2992
|
// 从此对它的搜索全部静默返回空。
|
|
2940
2993
|
const FINGERPRINT_SEP = "\u0000";
|
|
2941
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
|
+
|
|
2942
3039
|
export function workspaceNodeInputFingerprint(graph, nodeId, memo = new Map(), stack = new Set()) {
|
|
2943
3040
|
const id = String(nodeId || "");
|
|
2944
3041
|
if (memo.has(id)) return memo.get(id);
|
|
@@ -2957,6 +3054,20 @@ export function workspaceNodeInputFingerprint(graph, nodeId, memo = new Map(), s
|
|
|
2957
3054
|
marketplaceRef ? "" : String(instance.script || ""),
|
|
2958
3055
|
String(instance.model || ""),
|
|
2959
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
|
+
}
|
|
2960
3071
|
|
|
2961
3072
|
const incoming = new Map();
|
|
2962
3073
|
for (const edge of Array.isArray(graph?.edges) ? graph.edges : []) {
|
|
@@ -3005,7 +3116,7 @@ function workspaceEdgeHasCachedOutput(graph, edge, scopedRoot = "", opts = {}) {
|
|
|
3005
3116
|
if (!source) return false;
|
|
3006
3117
|
const defId = String(source.definitionId || "");
|
|
3007
3118
|
// provide.* 不执行,它的值就是作者填的;没有「上次跑出来的」这回事,也就无从失效
|
|
3008
|
-
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") {
|
|
3009
3120
|
return Boolean(String(workspaceInstanceText(source) || "").trim());
|
|
3010
3121
|
}
|
|
3011
3122
|
// 有值还不够,得是**这套输入**跑出来的值。指纹对不上说明上游或节点自身改过,重跑
|
|
@@ -4634,6 +4745,9 @@ async function workspaceRunToolNodejsScript({
|
|
|
4634
4745
|
emit,
|
|
4635
4746
|
signal,
|
|
4636
4747
|
onActiveChild,
|
|
4748
|
+
stderrAsStatus = false,
|
|
4749
|
+
maxStdoutBytes = Infinity,
|
|
4750
|
+
maxStderrBytes = Infinity,
|
|
4637
4751
|
}) {
|
|
4638
4752
|
const scriptRef = String(instance?.scriptRef || "").trim();
|
|
4639
4753
|
const scriptAbs = scriptRef ? workspaceResolveFlowFile(scopedRoot, scriptRef, "scriptRef") : "";
|
|
@@ -4692,6 +4806,9 @@ async function workspaceRunToolNodejsScript({
|
|
|
4692
4806
|
if (typeof onActiveChild === "function") onActiveChild(child, { processGroup });
|
|
4693
4807
|
let stdout = "";
|
|
4694
4808
|
let stderr = "";
|
|
4809
|
+
let stdoutBytes = 0;
|
|
4810
|
+
let stderrBytes = 0;
|
|
4811
|
+
let outputLimitError = null;
|
|
4695
4812
|
let settled = false;
|
|
4696
4813
|
const finish = (callback) => {
|
|
4697
4814
|
if (settled) return;
|
|
@@ -4702,13 +4819,33 @@ async function workspaceRunToolNodejsScript({
|
|
|
4702
4819
|
child.stdout.setEncoding("utf-8");
|
|
4703
4820
|
child.stderr.setEncoding("utf-8");
|
|
4704
4821
|
child.stdout.on("data", (chunk) => {
|
|
4705
|
-
|
|
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;
|
|
4706
4831
|
});
|
|
4707
4832
|
child.stderr.on("data", (chunk) => {
|
|
4708
|
-
|
|
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;
|
|
4709
4842
|
});
|
|
4710
4843
|
child.on("error", (error) => finish(() => reject(error)));
|
|
4711
4844
|
child.on("close", (code) => {
|
|
4845
|
+
if (outputLimitError) {
|
|
4846
|
+
finish(() => reject(outputLimitError));
|
|
4847
|
+
return;
|
|
4848
|
+
}
|
|
4712
4849
|
if (signal?.aborted) {
|
|
4713
4850
|
finish(() => {
|
|
4714
4851
|
const error = new Error("Workspace run stopped");
|
|
@@ -4718,7 +4855,8 @@ async function workspaceRunToolNodejsScript({
|
|
|
4718
4855
|
return;
|
|
4719
4856
|
}
|
|
4720
4857
|
if (stderr.trim()) {
|
|
4721
|
-
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)}` });
|
|
4722
4860
|
}
|
|
4723
4861
|
if (code !== 0) {
|
|
4724
4862
|
finish(() => reject(new Error(`tool_nodejs script exited ${code}${stderr.trim() ? `: ${stderr.trim().slice(-800)}` : ""}`)));
|
|
@@ -4744,6 +4882,140 @@ function workspaceNodeModelKey(instance, fallback = "") {
|
|
|
4744
4882
|
return String(fallback || "").trim();
|
|
4745
4883
|
}
|
|
4746
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
|
+
|
|
4747
5019
|
export async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts = {}) {
|
|
4748
5020
|
const graph = hydrateWorkspaceGraphForRuntime(root, {
|
|
4749
5021
|
root: scopedRoot,
|
|
@@ -4752,8 +5024,11 @@ export async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {},
|
|
|
4752
5024
|
archived: payload.archived === true || payload.flowArchived === true,
|
|
4753
5025
|
}, payload.graph || {}, userCtx);
|
|
4754
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));
|
|
4755
5030
|
const { order, pauseNodeIds } = workspaceRunPlan(graph, runNodeId, scopedRoot, {
|
|
4756
|
-
forceNodeIds
|
|
5031
|
+
forceNodeIds,
|
|
4757
5032
|
ignoreCache: payload?.ignoreCache === true,
|
|
4758
5033
|
});
|
|
4759
5034
|
// 指纹只由「节点定义 + 上游指纹 + 没接线的槽位值」决定,运行过程中这些都不变,所以
|
|
@@ -4835,6 +5110,7 @@ export async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {},
|
|
|
4835
5110
|
const runTmpRoot = workspaceCreateRunTmpRoot(scopedRoot, runNodeId);
|
|
4836
5111
|
const controlBranches = new Map();
|
|
4837
5112
|
const skippedNodes = new Set();
|
|
5113
|
+
const runtimePauseNodeIds = [];
|
|
4838
5114
|
let deferred = null;
|
|
4839
5115
|
const incomingControlEdgesByTarget = new Map();
|
|
4840
5116
|
for (const edge of Array.isArray(graph?.edges) ? graph.edges : []) {
|
|
@@ -4892,6 +5168,45 @@ export async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {},
|
|
|
4892
5168
|
continue;
|
|
4893
5169
|
}
|
|
4894
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
|
+
|
|
4895
5210
|
if (defId === "control_load_skills") {
|
|
4896
5211
|
const skillStartedAt = Date.now();
|
|
4897
5212
|
const nodeSkillKeys = selectedSkillKeysFromInstance(instance);
|
|
@@ -4961,6 +5276,252 @@ export async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {},
|
|
|
4961
5276
|
continue;
|
|
4962
5277
|
}
|
|
4963
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
|
+
|
|
4964
5525
|
if (defId === "provide_str" || defId === "provide_password") {
|
|
4965
5526
|
const content = workspaceInstanceText(instance);
|
|
4966
5527
|
publishNodeOutput(nodeId, content, { emitGraph: true });
|
|
@@ -4968,6 +5529,21 @@ export async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {},
|
|
|
4968
5529
|
continue;
|
|
4969
5530
|
}
|
|
4970
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
|
+
|
|
4971
5547
|
if (defId === "provide_bool") {
|
|
4972
5548
|
const raw = workspaceSlotValue(Array.isArray(instance.output) ? instance.output[0] : null) || workspaceInstanceText(instance);
|
|
4973
5549
|
const content = ["true", "1", "yes", "on"].includes(String(raw || "").trim().toLowerCase()) ? "true" : "false";
|
|
@@ -5451,7 +6027,11 @@ export async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {},
|
|
|
5451
6027
|
signal,
|
|
5452
6028
|
onActiveChild: opts.onActiveChild,
|
|
5453
6029
|
});
|
|
5454
|
-
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);
|
|
5455
6035
|
const resultContent = normalizedAgentOutput.result || content;
|
|
5456
6036
|
recordNodeOutput(nodeId, resultContent);
|
|
5457
6037
|
const slotUpdate = workspaceApplyAgentOutputSlots(instance, normalizedAgentOutput);
|
|
@@ -5651,11 +6231,12 @@ export async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {},
|
|
|
5651
6231
|
workspaceCleanupAutoWorktrees(autoCleanupWorktrees, graph, emit);
|
|
5652
6232
|
workspaceCleanupTmpRoot(runTmpRoot, userCtx, emit);
|
|
5653
6233
|
}
|
|
5654
|
-
|
|
5655
|
-
|
|
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(", ")}` });
|
|
5656
6237
|
}
|
|
5657
6238
|
graph.updatedAt = new Date().toISOString();
|
|
5658
|
-
return { graph, events, order, pauseNodeIds, deferred };
|
|
6239
|
+
return { graph, events, order, pauseNodeIds: finalPauseNodeIds, deferred };
|
|
5659
6240
|
}
|
|
5660
6241
|
|
|
5661
6242
|
export function isWorkspaceRunAbortError(err) {
|