@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.
Files changed (46) hide show
  1. package/bin/lib/catalog-flows.mjs +10 -1
  2. package/bin/lib/control-while.mjs +336 -0
  3. package/bin/lib/flow-dsl/codegen.mjs +75 -12
  4. package/bin/lib/flow-dsl/ir.mjs +33 -2
  5. package/bin/lib/flow-dsl/lint.mjs +128 -3
  6. package/bin/lib/flow-dsl/parser.mjs +168 -4
  7. package/bin/lib/locales/en.json +4 -0
  8. package/bin/lib/locales/zh.json +4 -0
  9. package/bin/lib/marketplace.mjs +10 -2
  10. package/bin/lib/mcp-server.mjs +1 -0
  11. package/bin/lib/node-package-manifest.mjs +7 -2
  12. package/bin/lib/node-ui-kit.mjs +156 -0
  13. package/bin/lib/paths.mjs +2 -0
  14. package/bin/lib/workspace-flow-store.mjs +11 -2
  15. package/bin/lib/workspace-graph-merge.mjs +3 -0
  16. package/bin/lib/workspace-routes.mjs +10 -1
  17. package/bin/lib/workspace-server.mjs +601 -15
  18. package/bin/pipeline/validate-flow.mjs +3 -3
  19. package/builtin/nodes/control_parse_json.md +25 -0
  20. package/builtin/nodes/control_subflow_call.md +27 -0
  21. package/builtin/nodes/control_while.md +124 -0
  22. package/builtin/nodes/display_code.md +39 -0
  23. package/builtin/nodes/provide_json.md +14 -0
  24. package/builtin/nodes/workspace_subflow_input.md +14 -0
  25. package/builtin/pipelines/subflow-preview/workspace.flow.js +29 -0
  26. package/builtin/pipelines/subflow-preview/workspace.layout.json +38 -0
  27. package/builtin/pipelines/subflow-preview/workspace.nodes.json +19 -0
  28. package/builtin/pipelines/subflow-preview/workspace.state.json +93 -0
  29. package/builtin/pipelines/while-subflow-preview/workspace.flow.js +55 -0
  30. package/builtin/pipelines/while-subflow-preview/workspace.layout.json +66 -0
  31. package/builtin/pipelines/while-subflow-preview/workspace.nodes.json +49 -0
  32. package/builtin/pipelines/while-subflow-preview/workspace.state.json +194 -0
  33. package/builtin/web-ui/dist/assets/{WorkflowAssistantThread-g_ljSViJ.js → WorkflowAssistantThread-Bfo9Ythw.js} +1 -1
  34. package/builtin/web-ui/dist/assets/index-XbeI5foV.js +872 -0
  35. package/builtin/web-ui/dist/assets/index-e1omCEau.css +1 -0
  36. package/builtin/web-ui/dist/index.html +2 -2
  37. package/package.json +2 -1
  38. package/reference/flow-control-capabilities.md +41 -16
  39. package/shared/slot-types.js +58 -0
  40. package/skills/agentflow-flow-dsl/SKILL.md +61 -7
  41. package/skills/agentflow-flow-dsl/references/node-calls.md +4 -0
  42. package/skills/agentflow-flow-dsl/references/subflow-authoring.md +230 -0
  43. package/skills/agentflow-node-dsl/SKILL.md +39 -0
  44. package/skills/agentflow-node-reference/references/builtin-nodes.md +33 -0
  45. package/builtin/web-ui/dist/assets/index-C0iq6zHl.js +0 -870
  46. 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 || String(slot?.type || "") === "text"
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 || String(slot?.type || "") === "text"
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
- stdout += String(chunk);
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
- stderr += String(chunk);
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: "natural", kind: "warning", text: `[script stderr]\n${stderr.trim().slice(-4000)}` });
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: Array.isArray(payload?.forceNodeIds) ? payload.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 normalizedAgentOutput = workspacePublishAgentOutputFiles(workspaceStructuredAgentOutput(content), runPackage);
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
- if (!deferred && pauseNodeIds.length > 0) {
5650
- emit({ type: "paused", nodeIds: pauseNodeIds, message: `Workspace run paused at ${pauseNodeIds.join(", ")}` });
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) {