@fieldwangai/agentflow 0.1.159 → 0.1.162

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 (118) hide show
  1. package/bin/lib/auth.mjs +231 -14
  2. package/bin/lib/catalog-flows.mjs +12 -1
  3. package/bin/lib/cli-auth-page.mjs +103 -0
  4. package/bin/lib/control-while.mjs +336 -0
  5. package/bin/lib/flow-dsl/codegen.mjs +74 -11
  6. package/bin/lib/flow-dsl/ir.mjs +33 -2
  7. package/bin/lib/flow-dsl/lint.mjs +128 -3
  8. package/bin/lib/flow-dsl/parser.mjs +168 -4
  9. package/bin/lib/marketplace.mjs +10 -2
  10. package/bin/lib/node-package-manifest.mjs +7 -2
  11. package/bin/lib/node-ui-kit.mjs +156 -0
  12. package/bin/lib/paths.mjs +2 -0
  13. package/bin/lib/skill-runtime.mjs +6 -0
  14. package/bin/lib/table.mjs +2 -3
  15. package/bin/lib/ui-server.mjs +220 -17
  16. package/bin/lib/workspace-draft.mjs +74 -0
  17. package/bin/lib/workspace-flow-store.mjs +11 -2
  18. package/bin/lib/workspace-graph-merge.mjs +3 -0
  19. package/bin/lib/workspace-routes.mjs +307 -5
  20. package/bin/lib/workspace-server.mjs +598 -11
  21. package/bin/pipeline/validate-flow.mjs +3 -3
  22. package/builtin/nodes/control_parse_json.md +25 -0
  23. package/builtin/nodes/control_subflow_call.md +27 -0
  24. package/builtin/nodes/control_while.md +124 -0
  25. package/builtin/nodes/provide_json.md +14 -0
  26. package/builtin/nodes/workspace_subflow_input.md +14 -0
  27. package/builtin/pipelines/new/workspace.flow.js +1 -1
  28. package/builtin/pipelines/subflow-preview/workspace.flow.js +29 -0
  29. package/builtin/pipelines/subflow-preview/workspace.layout.json +38 -0
  30. package/builtin/pipelines/subflow-preview/workspace.nodes.json +19 -0
  31. package/builtin/pipelines/subflow-preview/workspace.state.json +93 -0
  32. package/builtin/pipelines/while-subflow-preview/workspace.flow.js +55 -0
  33. package/builtin/pipelines/while-subflow-preview/workspace.layout.json +66 -0
  34. package/builtin/pipelines/while-subflow-preview/workspace.nodes.json +49 -0
  35. package/builtin/pipelines/while-subflow-preview/workspace.state.json +194 -0
  36. package/builtin/web-ui/dist/assets/{WorkflowAssistantThread-CY19DsYq.js → WorkflowAssistantThread-CTrXOZ00.js} +1 -1
  37. package/builtin/web-ui/dist/assets/index-5uJFccdX.css +1 -0
  38. package/builtin/web-ui/dist/assets/index-BeUfNQRL.js +873 -0
  39. package/builtin/web-ui/dist/index.html +2 -2
  40. package/package.json +6 -1
  41. package/reference/flow-control-capabilities.md +41 -16
  42. package/shared/slot-types.js +58 -0
  43. package/skills/agentflow-author-flow/SKILL.md +81 -8
  44. package/skills/agentflow-author-flow/agents/openai.yaml +3 -3
  45. package/skills/agentflow-cli/SKILL.md +130 -25
  46. package/skills/agentflow-cli/agents/openai.yaml +2 -2
  47. package/skills/agentflow-cli/runtime/bin/lib/skill-runtime.mjs +14765 -0
  48. package/skills/agentflow-cli/runtime/builtin/nodes/agent_subAgent.md +34 -0
  49. package/skills/agentflow-cli/runtime/builtin/nodes/control_agent_toBool.md +26 -0
  50. package/skills/agentflow-cli/runtime/builtin/nodes/control_anyOne.md +19 -0
  51. package/skills/agentflow-cli/runtime/builtin/nodes/control_cancelled.md +26 -0
  52. package/skills/agentflow-cli/runtime/builtin/nodes/control_cd_workspace.md +50 -0
  53. package/skills/agentflow-cli/runtime/builtin/nodes/control_delay.md +31 -0
  54. package/skills/agentflow-cli/runtime/builtin/nodes/control_end.md +13 -0
  55. package/skills/agentflow-cli/runtime/builtin/nodes/control_if.md +21 -0
  56. package/skills/agentflow-cli/runtime/builtin/nodes/control_interval_loop.md +55 -0
  57. package/skills/agentflow-cli/runtime/builtin/nodes/control_load_mcp.md +24 -0
  58. package/skills/agentflow-cli/runtime/builtin/nodes/control_load_skills.md +35 -0
  59. package/skills/agentflow-cli/runtime/builtin/nodes/control_parse_json.md +25 -0
  60. package/skills/agentflow-cli/runtime/builtin/nodes/control_start.md +13 -0
  61. package/skills/agentflow-cli/runtime/builtin/nodes/control_subflow_call.md +27 -0
  62. package/skills/agentflow-cli/runtime/builtin/nodes/control_toBool.md +27 -0
  63. package/skills/agentflow-cli/runtime/builtin/nodes/control_user_workspace.md +24 -0
  64. package/skills/agentflow-cli/runtime/builtin/nodes/control_wait_until.md +34 -0
  65. package/skills/agentflow-cli/runtime/builtin/nodes/control_while.md +124 -0
  66. package/skills/agentflow-cli/runtime/builtin/nodes/display_ascii.md +24 -0
  67. package/skills/agentflow-cli/runtime/builtin/nodes/display_chart.md +32 -0
  68. package/skills/agentflow-cli/runtime/builtin/nodes/display_code.md +39 -0
  69. package/skills/agentflow-cli/runtime/builtin/nodes/display_html.md +32 -0
  70. package/skills/agentflow-cli/runtime/builtin/nodes/display_image.md +36 -0
  71. package/skills/agentflow-cli/runtime/builtin/nodes/display_markdown.md +24 -0
  72. package/skills/agentflow-cli/runtime/builtin/nodes/display_mermaid.md +24 -0
  73. package/skills/agentflow-cli/runtime/builtin/nodes/display_react_app.md +53 -0
  74. package/skills/agentflow-cli/runtime/builtin/nodes/display_table.md +32 -0
  75. package/skills/agentflow-cli/runtime/builtin/nodes/provide_bool.md +14 -0
  76. package/skills/agentflow-cli/runtime/builtin/nodes/provide_file.md +14 -0
  77. package/skills/agentflow-cli/runtime/builtin/nodes/provide_json.md +14 -0
  78. package/skills/agentflow-cli/runtime/builtin/nodes/provide_password.md +14 -0
  79. package/skills/agentflow-cli/runtime/builtin/nodes/provide_str.md +14 -0
  80. package/skills/agentflow-cli/runtime/builtin/nodes/tool_display_share_link.md +47 -0
  81. package/skills/agentflow-cli/runtime/builtin/nodes/tool_get_env.md +20 -0
  82. package/skills/agentflow-cli/runtime/builtin/nodes/tool_git_checkout.md +70 -0
  83. package/skills/agentflow-cli/runtime/builtin/nodes/tool_git_worktree_load.md +73 -0
  84. package/skills/agentflow-cli/runtime/builtin/nodes/tool_git_worktree_unload.md +52 -0
  85. package/skills/agentflow-cli/runtime/builtin/nodes/tool_gitlab_create_mr.md +114 -0
  86. package/skills/agentflow-cli/runtime/builtin/nodes/tool_jenkins_build.md +67 -0
  87. package/skills/agentflow-cli/runtime/builtin/nodes/tool_load_key.md +26 -0
  88. package/skills/agentflow-cli/runtime/builtin/nodes/tool_nodejs.md +52 -0
  89. package/skills/agentflow-cli/runtime/builtin/nodes/tool_print.md +21 -0
  90. package/skills/agentflow-cli/runtime/builtin/nodes/tool_save_key.md +26 -0
  91. package/skills/agentflow-cli/runtime/builtin/nodes/tool_set_run_env.md +38 -0
  92. package/skills/agentflow-cli/runtime/builtin/nodes/tool_user_ask.md +27 -0
  93. package/skills/agentflow-cli/runtime/builtin/nodes/tool_user_check.md +26 -0
  94. package/skills/agentflow-cli/runtime/builtin/nodes/tool_wecom_send_app_markdown.md +57 -0
  95. package/skills/agentflow-cli/runtime/builtin/nodes/tool_wecom_send_group_markdown.md +45 -0
  96. package/skills/agentflow-cli/runtime/builtin/nodes/workspace_one_click_task.md +44 -0
  97. package/skills/agentflow-cli/runtime/builtin/nodes/workspace_run.md +16 -0
  98. package/skills/agentflow-cli/runtime/builtin/nodes/workspace_scheduled_run.md +16 -0
  99. package/skills/agentflow-cli/runtime/builtin/nodes/workspace_subflow_input.md +14 -0
  100. package/skills/agentflow-cli/runtime/package.json +6 -0
  101. package/skills/agentflow-cli/scripts/agentflow-auth-store.mjs +113 -0
  102. package/skills/agentflow-cli/scripts/agentflow-cli.mjs +393 -28
  103. package/skills/agentflow-cli/scripts/agentflow-runtime.mjs +11 -4
  104. package/skills/agentflow-flow-add-instances/SKILL.md +2 -2
  105. package/skills/agentflow-flow-dsl/SKILL.md +83 -10
  106. package/skills/agentflow-flow-dsl/agents/openai.yaml +2 -2
  107. package/skills/agentflow-flow-dsl/references/node-calls.md +3 -0
  108. package/skills/agentflow-flow-dsl/references/subflow-authoring.md +230 -0
  109. package/skills/agentflow-flow-edit-node-fields/SKILL.md +3 -3
  110. package/skills/agentflow-flow-recipes/SKILL.md +1 -1
  111. package/skills/agentflow-flow-recipes/references/recipes.md +1 -2
  112. package/skills/agentflow-flow-sync-ui/SKILL.md +1 -1
  113. package/skills/agentflow-node-dsl/SKILL.md +44 -13
  114. package/skills/agentflow-node-reference/references/builtin-nodes.md +25 -0
  115. package/skills/agentflow-workspace-markdown/SKILL.md +1 -1
  116. package/builtin/web-ui/dist/assets/index-B9ppXv7e.css +0 -1
  117. package/builtin/web-ui/dist/assets/index-CdEQWRrp.js +0 -872
  118. package/skills/agentflow-workspace-graph/SKILL.md +0 -85
@@ -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,9 +41,10 @@ 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";
47
+ import { isWorkspaceDraftDir } from "./workspace-draft.mjs";
35
48
  import { getPipelineFiles } from "./workspace-tree.mjs";
36
49
  import { spawn } from "child_process";
37
50
  import crypto from "crypto";
@@ -1122,6 +1135,7 @@ function normalizeWorkspaceGraphPayload(payload) {
1122
1135
  instances: graph?.instances && typeof graph.instances === "object" && !Array.isArray(graph.instances) ? graph.instances : {},
1123
1136
  edges: Array.isArray(graph?.edges) ? graph.edges : [],
1124
1137
  ui: graph?.ui && typeof graph.ui === "object" ? graph.ui : { nodePositions: {} },
1138
+ subflows: graph?.subflows && typeof graph.subflows === "object" && !Array.isArray(graph.subflows) ? graph.subflows : {},
1125
1139
  updatedAt: new Date().toISOString(),
1126
1140
  };
1127
1141
  }
@@ -1130,7 +1144,7 @@ export function workspaceRunTouchedNodeIds(result) {
1130
1144
  const ids = new Set();
1131
1145
  // A deferred run has only executed the prefix ending at the waiting node. Merging the
1132
1146
  // complete plan here would overwrite unrelated edits made while Jenkins is running.
1133
- if (!result?.deferred) {
1147
+ if (!result?.deferred && !(Array.isArray(result?.pauseNodeIds) && result.pauseNodeIds.length)) {
1134
1148
  for (const id of Array.isArray(result?.order) ? result.order : []) {
1135
1149
  const text = String(id || "").trim();
1136
1150
  if (text) ids.add(text);
@@ -1478,6 +1492,7 @@ export function resolveWorkspaceScopeRoot(workspaceRoot, params = {}, opts = {})
1478
1492
  requestedFlowSource: flowSource,
1479
1493
  workspaceId: collaboration?.id || workspaceId,
1480
1494
  archived,
1495
+ draft: isWorkspaceDraftDir(result.path),
1481
1496
  collaboration,
1482
1497
  collaborationAccess: workspaceCollaborationAccess(collaboration, opts.userId),
1483
1498
  };
@@ -2490,6 +2505,46 @@ export function workspacePublishAgentOutputFiles(structured, runPackage) {
2490
2505
  : base;
2491
2506
  }
2492
2507
 
2508
+ /**
2509
+ * Node package outputs use files as a transport contract, not as the semantic
2510
+ * value of every slot. Text/json/bool slots receive the file contents; only
2511
+ * file/image slots intentionally expose a published artifact path.
2512
+ */
2513
+ export function workspaceMaterializeNodePackageOutputValues(structured, runPackage, instance) {
2514
+ if (!structured || !runPackage || !instance) return structured;
2515
+ const slots = (Array.isArray(instance.output) ? instance.output : [])
2516
+ .filter((slot) => !isWorkspaceSemanticOutputSlot(slot));
2517
+ if (!slots.length) return structured;
2518
+ const outParams = { ...(structured.outParams || {}) };
2519
+ let result = structured.result;
2520
+ let resultFile = structured.resultFile;
2521
+ let changed = false;
2522
+ for (const slot of slots) {
2523
+ const name = String(slot?.name || "").trim();
2524
+ if (!name) continue;
2525
+ const type = String(slot?.type || "text").trim().toLowerCase();
2526
+ if (type === "file" || type === "image") continue;
2527
+ const primary = workspaceIsPrimaryOutputSlot(slot, instance.output);
2528
+ const fileKey = `${name}File`;
2529
+ const relPath = primary ? resultFile : outParams[fileKey];
2530
+ const clean = workspaceSafeNodeOutputRelPath(relPath);
2531
+ if (!clean) continue;
2532
+ const source = workspaceNodeOutputCandidates(runPackage, clean)
2533
+ .find((candidate) => fs.existsSync(candidate) && fs.statSync(candidate).isFile());
2534
+ if (!source) continue;
2535
+ const value = fs.readFileSync(source, "utf8");
2536
+ if (primary) {
2537
+ result = value;
2538
+ resultFile = "";
2539
+ } else {
2540
+ outParams[name] = value;
2541
+ delete outParams[fileKey];
2542
+ }
2543
+ changed = true;
2544
+ }
2545
+ return changed ? { ...structured, result, resultFile, outParams } : structured;
2546
+ }
2547
+
2493
2548
  function workspaceOutputFieldForSlot(slot, slots = null) {
2494
2549
  const name = String(slot?.name || "").trim();
2495
2550
  if (!name || workspaceIsPrimaryOutputSlot(slot, slots)) return "result";
@@ -2939,6 +2994,50 @@ function workspaceControlIfBranchToSourceHandle(branch) {
2939
2994
  // 从此对它的搜索全部静默返回空。
2940
2995
  const FINGERPRINT_SEP = "\u0000";
2941
2996
 
2997
+ function workspaceSubflowFingerprintDescriptor(graph, subflowId, stack = new Set()) {
2998
+ const id = String(subflowId || "").trim();
2999
+ const subflow = graph?.subflows?.[id];
3000
+ if (!id || !subflow) return { id, missing: true };
3001
+ if (stack.has(id)) return { id, recursive: true };
3002
+
3003
+ const nextStack = new Set(stack);
3004
+ nextStack.add(id);
3005
+ const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
3006
+ const memberSet = new Set(Array.isArray(subflow.nodeIds) ? subflow.nodeIds.map(String) : []);
3007
+ const memberShape = [...memberSet].sort().map((memberId) => {
3008
+ const member = instances[memberId] || {};
3009
+ return [
3010
+ memberId,
3011
+ String(member.definitionId || ""),
3012
+ String(member.body || ""),
3013
+ String(member.script || ""),
3014
+ String(member.scriptRef || ""),
3015
+ String(member.subflowId || ""),
3016
+ String(member.conditionSubflowId || ""),
3017
+ String(member.bodySubflowId || ""),
3018
+ (member.input || []).map((slot) => [String(slot?.name || ""), String(slot?.type || ""), workspaceSlotValue(slot)]),
3019
+ ];
3020
+ });
3021
+ const internalEdges = (graph?.edges || []).filter((edge) => (
3022
+ memberSet.has(String(edge?.source || "")) && memberSet.has(String(edge?.target || ""))
3023
+ ));
3024
+ const nestedIds = new Set();
3025
+ for (const memberId of memberSet) {
3026
+ const member = instances[memberId] || {};
3027
+ if (String(member.definitionId || "") === "control_subflow_call" && member.subflowId) {
3028
+ nestedIds.add(String(member.subflowId));
3029
+ }
3030
+ if (String(member.definitionId || "") === "control_while") {
3031
+ if (member.conditionSubflowId) nestedIds.add(String(member.conditionSubflowId));
3032
+ if (member.bodySubflowId) nestedIds.add(String(member.bodySubflowId));
3033
+ }
3034
+ }
3035
+ const nested = [...nestedIds].sort().map((nestedId) => (
3036
+ workspaceSubflowFingerprintDescriptor(graph, nestedId, nextStack)
3037
+ ));
3038
+ return { id, subflow, memberShape, internalEdges, nested };
3039
+ }
3040
+
2942
3041
  export function workspaceNodeInputFingerprint(graph, nodeId, memo = new Map(), stack = new Set()) {
2943
3042
  const id = String(nodeId || "");
2944
3043
  if (memo.has(id)) return memo.get(id);
@@ -2957,6 +3056,20 @@ export function workspaceNodeInputFingerprint(graph, nodeId, memo = new Map(), s
2957
3056
  marketplaceRef ? "" : String(instance.script || ""),
2958
3057
  String(instance.model || ""),
2959
3058
  ];
3059
+ if (String(instance.definitionId || "") === "control_subflow_call") {
3060
+ const subflowId = String(instance.subflowId || "");
3061
+ parts.push(`subflow=${JSON.stringify(workspaceSubflowFingerprintDescriptor(graph, subflowId))}`);
3062
+ }
3063
+ if (String(instance.definitionId || "") === "control_while") {
3064
+ const conditionSubflowId = String(instance.conditionSubflowId || "");
3065
+ const bodySubflowId = String(instance.bodySubflowId || "");
3066
+ if (conditionSubflowId || bodySubflowId) {
3067
+ parts.push(`whileSubflows=${JSON.stringify({
3068
+ condition: workspaceSubflowFingerprintDescriptor(graph, conditionSubflowId),
3069
+ body: workspaceSubflowFingerprintDescriptor(graph, bodySubflowId),
3070
+ })}`);
3071
+ }
3072
+ }
2960
3073
 
2961
3074
  const incoming = new Map();
2962
3075
  for (const edge of Array.isArray(graph?.edges) ? graph.edges : []) {
@@ -3005,7 +3118,7 @@ function workspaceEdgeHasCachedOutput(graph, edge, scopedRoot = "", opts = {}) {
3005
3118
  if (!source) return false;
3006
3119
  const defId = String(source.definitionId || "");
3007
3120
  // provide.* 不执行,它的值就是作者填的;没有「上次跑出来的」这回事,也就无从失效
3008
- if (defId === "provide_str" || defId === "provide_bool" || defId === "provide_file" || defId === "provide_password") {
3121
+ if (defId === "provide_str" || defId === "provide_json" || defId === "provide_bool" || defId === "provide_file" || defId === "provide_password") {
3009
3122
  return Boolean(String(workspaceInstanceText(source) || "").trim());
3010
3123
  }
3011
3124
  // 有值还不够,得是**这套输入**跑出来的值。指纹对不上说明上游或节点自身改过,重跑
@@ -4634,6 +4747,9 @@ async function workspaceRunToolNodejsScript({
4634
4747
  emit,
4635
4748
  signal,
4636
4749
  onActiveChild,
4750
+ stderrAsStatus = false,
4751
+ maxStdoutBytes = Infinity,
4752
+ maxStderrBytes = Infinity,
4637
4753
  }) {
4638
4754
  const scriptRef = String(instance?.scriptRef || "").trim();
4639
4755
  const scriptAbs = scriptRef ? workspaceResolveFlowFile(scopedRoot, scriptRef, "scriptRef") : "";
@@ -4692,6 +4808,9 @@ async function workspaceRunToolNodejsScript({
4692
4808
  if (typeof onActiveChild === "function") onActiveChild(child, { processGroup });
4693
4809
  let stdout = "";
4694
4810
  let stderr = "";
4811
+ let stdoutBytes = 0;
4812
+ let stderrBytes = 0;
4813
+ let outputLimitError = null;
4695
4814
  let settled = false;
4696
4815
  const finish = (callback) => {
4697
4816
  if (settled) return;
@@ -4702,13 +4821,33 @@ async function workspaceRunToolNodejsScript({
4702
4821
  child.stdout.setEncoding("utf-8");
4703
4822
  child.stderr.setEncoding("utf-8");
4704
4823
  child.stdout.on("data", (chunk) => {
4705
- stdout += String(chunk);
4824
+ if (outputLimitError) return;
4825
+ const text = String(chunk);
4826
+ stdoutBytes += Buffer.byteLength(text, "utf-8");
4827
+ if (stdoutBytes > maxStdoutBytes) {
4828
+ outputLimitError = new Error(`script stdout exceeded ${maxStdoutBytes} bytes`);
4829
+ terminateWorkspaceChild(child, { processGroup });
4830
+ return;
4831
+ }
4832
+ stdout += text;
4706
4833
  });
4707
4834
  child.stderr.on("data", (chunk) => {
4708
- stderr += String(chunk);
4835
+ if (outputLimitError) return;
4836
+ const text = String(chunk);
4837
+ stderrBytes += Buffer.byteLength(text, "utf-8");
4838
+ if (stderrBytes > maxStderrBytes) {
4839
+ outputLimitError = new Error(`script stderr exceeded ${maxStderrBytes} bytes`);
4840
+ terminateWorkspaceChild(child, { processGroup });
4841
+ return;
4842
+ }
4843
+ stderr += text;
4709
4844
  });
4710
4845
  child.on("error", (error) => finish(() => reject(error)));
4711
4846
  child.on("close", (code) => {
4847
+ if (outputLimitError) {
4848
+ finish(() => reject(outputLimitError));
4849
+ return;
4850
+ }
4712
4851
  if (signal?.aborted) {
4713
4852
  finish(() => {
4714
4853
  const error = new Error("Workspace run stopped");
@@ -4718,7 +4857,8 @@ async function workspaceRunToolNodejsScript({
4718
4857
  return;
4719
4858
  }
4720
4859
  if (stderr.trim()) {
4721
- emit?.({ type: "natural", kind: "warning", text: `[script stderr]\n${stderr.trim().slice(-4000)}` });
4860
+ if (stderrAsStatus) emit?.({ type: "status", line: `[step stderr] ${stderr.trim().slice(-4000)}` });
4861
+ else emit?.({ type: "natural", kind: "warning", text: `[script stderr]\n${stderr.trim().slice(-4000)}` });
4722
4862
  }
4723
4863
  if (code !== 0) {
4724
4864
  finish(() => reject(new Error(`tool_nodejs script exited ${code}${stderr.trim() ? `: ${stderr.trim().slice(-800)}` : ""}`)));
@@ -4744,6 +4884,140 @@ function workspaceNodeModelKey(instance, fallback = "") {
4744
4884
  return String(fallback || "").trim();
4745
4885
  }
4746
4886
 
4887
+ function workspaceAssertWhileSubflowContract(graph, nodeId, conditionSubflowId, bodySubflowId) {
4888
+ const conditionId = String(conditionSubflowId || "").trim();
4889
+ const bodyId = String(bodySubflowId || "").trim();
4890
+ if (!conditionId || !bodyId) {
4891
+ throw new Error(`control.while ${nodeId} requires both conditionSubflowId and bodySubflowId`);
4892
+ }
4893
+ if (conditionId === bodyId) {
4894
+ throw new Error(`control.while ${nodeId} condition and body must be different subflows`);
4895
+ }
4896
+ const condition = graph?.subflows?.[conditionId];
4897
+ const body = graph?.subflows?.[bodyId];
4898
+ if (!condition) throw new Error(`control.while ${nodeId} references missing condition subflow ${conditionId}`);
4899
+ if (!body) throw new Error(`control.while ${nodeId} references missing body subflow ${bodyId}`);
4900
+ for (const name of ["state", "iteration"]) {
4901
+ if (!condition.inputs?.[name]) throw new Error(`control.while ${nodeId} condition subflow ${conditionId} requires input ${name}`);
4902
+ }
4903
+ if (!condition.outputs?.decision) {
4904
+ throw new Error(`control.while ${nodeId} condition subflow ${conditionId} requires output decision`);
4905
+ }
4906
+ for (const name of ["state", "iteration", "idempotencyKey"]) {
4907
+ if (!body.inputs?.[name]) throw new Error(`control.while ${nodeId} body subflow ${bodyId} requires input ${name}`);
4908
+ }
4909
+ if (!body.outputs?.state) {
4910
+ throw new Error(`control.while ${nodeId} body subflow ${bodyId} requires output state`);
4911
+ }
4912
+ return { conditionId, bodyId, condition, body };
4913
+ }
4914
+
4915
+ async function workspaceRunSubflowFrame({
4916
+ root,
4917
+ scopedRoot,
4918
+ payload,
4919
+ userCtx,
4920
+ opts,
4921
+ graph,
4922
+ parentNodeId,
4923
+ parentDefinitionId,
4924
+ subflowId,
4925
+ inputValues = {},
4926
+ signal = null,
4927
+ emit,
4928
+ eventContext = {},
4929
+ }) {
4930
+ const id = String(subflowId || "").trim();
4931
+ const subflow = graph?.subflows?.[id];
4932
+ if (!subflow) throw new Error(`Subflow ${id || "(empty)"} does not exist`);
4933
+ const callStack = Array.isArray(opts?.subflowCallStack) ? opts.subflowCallStack : [];
4934
+ if (callStack.includes(id)) {
4935
+ throw new Error(`Recursive subflow call is not allowed: ${[...callStack, id].join(" -> ")}`);
4936
+ }
4937
+
4938
+ const memberSet = new Set((subflow.nodeIds || []).map(String));
4939
+ if (!memberSet.size) throw new Error(`Subflow ${id} has no member nodes`);
4940
+ const callFrameId = `${parentNodeId}:${String(eventContext?.whileRole || "call")}:${String(eventContext?.iteration || "0")}:${crypto.randomBytes(6).toString("hex")}`;
4941
+ const childGraph = typeof structuredClone === "function"
4942
+ ? structuredClone(graph)
4943
+ : JSON.parse(JSON.stringify(graph));
4944
+ const syntheticRunId = `__subflow_run_${crypto.randomBytes(6).toString("hex")}`;
4945
+ childGraph.instances[syntheticRunId] = {
4946
+ definitionId: "workspace_run",
4947
+ label: `${subflow.label || id} · call frame`,
4948
+ input: [{ type: "node", name: "prev", value: "" }],
4949
+ output: [{ type: "node", name: "next", value: "" }],
4950
+ };
4951
+ childGraph.edges = (childGraph.edges || []).filter((edge) => (
4952
+ memberSet.has(String(edge?.source || "")) && memberSet.has(String(edge?.target || ""))
4953
+ ));
4954
+ for (const rootId of subflow.roots || []) {
4955
+ const rootInstance = childGraph.instances?.[rootId];
4956
+ const prevIndex = (rootInstance?.input || []).findIndex((slot) => String(slot?.name || "") === "prev");
4957
+ if (prevIndex < 0) throw new Error(`Subflow ${id} root ${rootId} has no prev input`);
4958
+ childGraph.edges.push({
4959
+ source: syntheticRunId,
4960
+ target: rootId,
4961
+ sourceHandle: "output-0",
4962
+ targetHandle: `input-${prevIndex}`,
4963
+ });
4964
+ }
4965
+ for (const [name, binding] of Object.entries(subflow.inputs || {})) {
4966
+ const proxy = childGraph.instances?.[binding.nodeId];
4967
+ if (!proxy) throw new Error(`Subflow ${id} input ${name} references missing proxy ${binding.nodeId}`);
4968
+ childGraph.instances[binding.nodeId] = workspaceSetOutputSlot(proxy, binding.slot || "value", inputValues[name] ?? "");
4969
+ }
4970
+
4971
+ const frameContext = {
4972
+ ...eventContext,
4973
+ parentNodeId,
4974
+ subflowId: id,
4975
+ callFrameId,
4976
+ };
4977
+ emit({
4978
+ type: "subflow-start",
4979
+ nodeId: parentNodeId,
4980
+ definitionId: parentDefinitionId,
4981
+ inputs: Object.keys(subflow.inputs || {}),
4982
+ ...frameContext,
4983
+ });
4984
+ const childResult = await runWorkspaceGraph(root, scopedRoot, {
4985
+ ...payload,
4986
+ graph: childGraph,
4987
+ runNodeId: syntheticRunId,
4988
+ ignoreCache: true,
4989
+ forceNodeIds: [],
4990
+ }, userCtx, {
4991
+ ...opts,
4992
+ signal: signal || opts?.signal || null,
4993
+ subflowCallStack: [...callStack, id],
4994
+ onEvent: (event) => {
4995
+ if (event?.type === "graph") return;
4996
+ emit({ ...event, ...frameContext });
4997
+ },
4998
+ });
4999
+ if (childResult.deferred || childResult.pauseNodeIds?.length) {
5000
+ throw new Error(`Subflow ${id} paused or deferred; resumable subflow call frames are not supported yet`);
5001
+ }
5002
+ for (const memberId of memberSet) {
5003
+ if (childResult.graph.instances?.[memberId]) graph.instances[memberId] = childResult.graph.instances[memberId];
5004
+ }
5005
+ const resultValues = {};
5006
+ for (const [name, binding] of Object.entries(subflow.outputs || {})) {
5007
+ const source = childResult.graph.instances?.[binding.nodeId];
5008
+ const slot = (source?.output || []).find((item) => String(item?.name || "") === String(binding.slot || ""));
5009
+ resultValues[name] = workspaceSlotValue(slot);
5010
+ }
5011
+ emit({
5012
+ type: "subflow-done",
5013
+ nodeId: parentNodeId,
5014
+ definitionId: parentDefinitionId,
5015
+ outputs: Object.keys(resultValues),
5016
+ ...frameContext,
5017
+ });
5018
+ return { resultValues, callFrameId };
5019
+ }
5020
+
4747
5021
  export async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {}, opts = {}) {
4748
5022
  const graph = hydrateWorkspaceGraphForRuntime(root, {
4749
5023
  root: scopedRoot,
@@ -4752,8 +5026,11 @@ export async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {},
4752
5026
  archived: payload.archived === true || payload.flowArchived === true,
4753
5027
  }, payload.graph || {}, userCtx);
4754
5028
  const runNodeId = String(payload?.runNodeId || "").trim();
5029
+ const forceNodeIds = new Set((Array.isArray(payload?.forceNodeIds) ? payload.forceNodeIds : [])
5030
+ .map((nodeId) => String(nodeId || "").trim())
5031
+ .filter(Boolean));
4755
5032
  const { order, pauseNodeIds } = workspaceRunPlan(graph, runNodeId, scopedRoot, {
4756
- forceNodeIds: Array.isArray(payload?.forceNodeIds) ? payload.forceNodeIds : [],
5033
+ forceNodeIds,
4757
5034
  ignoreCache: payload?.ignoreCache === true,
4758
5035
  });
4759
5036
  // 指纹只由「节点定义 + 上游指纹 + 没接线的槽位值」决定,运行过程中这些都不变,所以
@@ -4835,6 +5112,7 @@ export async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {},
4835
5112
  const runTmpRoot = workspaceCreateRunTmpRoot(scopedRoot, runNodeId);
4836
5113
  const controlBranches = new Map();
4837
5114
  const skippedNodes = new Set();
5115
+ const runtimePauseNodeIds = [];
4838
5116
  let deferred = null;
4839
5117
  const incomingControlEdgesByTarget = new Map();
4840
5118
  for (const edge of Array.isArray(graph?.edges) ? graph.edges : []) {
@@ -4892,6 +5170,45 @@ export async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {},
4892
5170
  continue;
4893
5171
  }
4894
5172
 
5173
+ if (defId === "workspace_subflow_input") {
5174
+ const value = workspaceSlotValue((instance.output || []).find((slot) => String(slot?.name || "") === "value"));
5175
+ publishNodeOutput(nodeId, value, { emitGraph: true });
5176
+ emit({ type: "node-done", nodeId, definitionId: defId });
5177
+ continue;
5178
+ }
5179
+
5180
+ if (defId === "control_subflow_call") {
5181
+ const subflowId = String(instance.subflowId || "").trim();
5182
+ const subflow = graph?.subflows?.[subflowId];
5183
+ if (!subflow) throw new Error(`flow.call ${nodeId} references missing subflow ${subflowId || "(empty)"}`);
5184
+ const inputValues = workspaceInputValues(graph, nodeId, outputs, scopedRoot);
5185
+ const { resultValues, callFrameId } = await workspaceRunSubflowFrame({
5186
+ root,
5187
+ scopedRoot,
5188
+ payload,
5189
+ userCtx,
5190
+ opts,
5191
+ graph,
5192
+ parentNodeId: nodeId,
5193
+ parentDefinitionId: defId,
5194
+ subflowId,
5195
+ inputValues,
5196
+ signal,
5197
+ emit,
5198
+ });
5199
+ let nextInstance = instance;
5200
+ for (const [name, value] of Object.entries(resultValues)) {
5201
+ nextInstance = workspaceSetOutputSlot(nextInstance, name, value);
5202
+ }
5203
+ graph.instances[nodeId] = nextInstance;
5204
+ const primaryName = Object.keys(subflow.outputs || {})[0] || "";
5205
+ const primaryValue = primaryName ? resultValues[primaryName] : "";
5206
+ const updatedDisplays = publishNodeOutput(nodeId, primaryValue);
5207
+ emit({ type: "graph", nodeId, displayNodeIds: updatedDisplays, graph });
5208
+ emit({ type: "node-done", nodeId, definitionId: defId, subflowId, callFrameId });
5209
+ continue;
5210
+ }
5211
+
4895
5212
  if (defId === "control_load_skills") {
4896
5213
  const skillStartedAt = Date.now();
4897
5214
  const nodeSkillKeys = selectedSkillKeysFromInstance(instance);
@@ -4961,6 +5278,252 @@ export async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {},
4961
5278
  continue;
4962
5279
  }
4963
5280
 
5281
+ if (defId === "control_parse_json") {
5282
+ const inputValues = workspaceInputValues(graph, nodeId, outputs, scopedRoot);
5283
+ const raw = String(inputValues.value ?? "").trim();
5284
+ if (!raw) throw new Error(`control.parseJson ${nodeId} requires a non-empty value`);
5285
+ let parsed;
5286
+ try {
5287
+ parsed = JSON.parse(raw);
5288
+ } catch (error) {
5289
+ throw new Error(`control.parseJson ${nodeId} received invalid JSON: ${error.message}`);
5290
+ }
5291
+ const content = JSON.stringify(parsed);
5292
+ graph.instances[nodeId] = workspaceSetOutputSlot(instance, "result", content);
5293
+ publishNodeOutput(nodeId, content, { emitGraph: true });
5294
+ emit({ type: "node-done", nodeId, definitionId: defId });
5295
+ continue;
5296
+ }
5297
+
5298
+ if (defId === "control_while") {
5299
+ const inputValues = workspaceInputValues(graph, nodeId, outputs, scopedRoot);
5300
+ const config = normalizeControlWhileConfig(inputValues);
5301
+ const stepScript = String(instance.script || instance.body || "").trim();
5302
+ const stepScriptRef = String(instance.scriptRef || "").trim();
5303
+ const conditionSubflowId = String(instance.conditionSubflowId || "").trim();
5304
+ const bodySubflowId = String(instance.bodySubflowId || "").trim();
5305
+ const usesSubflows = Boolean(conditionSubflowId || bodySubflowId);
5306
+ const whileSubflows = usesSubflows
5307
+ ? workspaceAssertWhileSubflowContract(graph, nodeId, conditionSubflowId, bodySubflowId)
5308
+ : null;
5309
+ if (!usesSubflows && !stepScript && !stepScriptRef) {
5310
+ throw new Error(`control.while ${nodeId} requires condition/body subflows or a step command/scriptRef`);
5311
+ }
5312
+ const previousDecision = workspaceSlotValue(workspaceSlotByName(instance, "decision")).trim().toLowerCase();
5313
+ const checkpointState = workspaceSlotValue(
5314
+ (Array.isArray(instance.output) ? instance.output : []).find((slot) => String(slot?.name || "") === "state"),
5315
+ );
5316
+ const checkpointFingerprint = workspaceSlotValue(
5317
+ (Array.isArray(instance.output) ? instance.output : []).find((slot) => String(slot?.name || "") === "checkpointFingerprint"),
5318
+ );
5319
+ const inputState = normalizeControlWhileInitialState(inputValues.state);
5320
+ const expectedCheckpointFingerprint = controlWhileCheckpointFingerprint({
5321
+ inputFingerprint: plannedFingerprints.get(nodeId) || "",
5322
+ initialState: inputState,
5323
+ });
5324
+ const checkpoint = resolveControlWhileCheckpoint({
5325
+ previousDecision,
5326
+ state: checkpointState,
5327
+ history: workspaceSlotValue(workspaceSlotByName(instance, "history")),
5328
+ iterations: workspaceSlotValue(workspaceSlotByName(instance, "iterations")),
5329
+ fingerprint: checkpointFingerprint,
5330
+ expectedFingerprint: expectedCheckpointFingerprint,
5331
+ allowResume: payload?.ignoreCache !== true && !forceNodeIds.has(nodeId),
5332
+ });
5333
+ const initialState = checkpoint.resumable ? checkpoint.state : inputState;
5334
+ if (checkpoint.resumable) {
5335
+ emit({ type: "status", nodeId, line: "control.while resume from waiting checkpoint" });
5336
+ } else if (previousDecision === "wait") {
5337
+ emit({ type: "status", nodeId, line: `control.while reset waiting checkpoint: ${checkpoint.reason}` });
5338
+ }
5339
+ const loop = await runControlWhile({
5340
+ initialState,
5341
+ initialHistory: checkpoint.resumable ? checkpoint.history : [],
5342
+ initialElapsedMs: checkpoint.resumable ? checkpoint.elapsedMs : 0,
5343
+ startIteration: checkpoint.resumable ? checkpoint.nextIteration : 1,
5344
+ ...config,
5345
+ signal,
5346
+ idempotencyKeyForIteration: ({ iteration }) => controlWhileIdempotencyKey({
5347
+ checkpointFingerprint: expectedCheckpointFingerprint,
5348
+ nodeId,
5349
+ iteration,
5350
+ }),
5351
+ onIterationStart: ({ iteration, remainingMs, idempotencyKey }) => {
5352
+ emit({
5353
+ type: "while-iteration-start",
5354
+ nodeId,
5355
+ definitionId: defId,
5356
+ iteration,
5357
+ maxIterations: config.maxIterations,
5358
+ remainingMs,
5359
+ idempotencyKey,
5360
+ });
5361
+ emit({ type: "status", nodeId, line: `control.while iteration ${iteration}/${config.maxIterations}` });
5362
+ },
5363
+ onIterationDone: ({ iteration, decision, summary, elapsedMs, idempotencyKey }) => {
5364
+ emit({
5365
+ type: "while-iteration-done",
5366
+ nodeId,
5367
+ definitionId: defId,
5368
+ iteration,
5369
+ decision,
5370
+ summary,
5371
+ elapsedMs,
5372
+ idempotencyKey,
5373
+ });
5374
+ emit({ type: "status", nodeId, line: `control.while iteration ${iteration}: ${decision}${summary ? ` · ${summary}` : ""}` });
5375
+ },
5376
+ executeStep: async ({ iteration, state, signal: stepSignal, idempotencyKey }) => {
5377
+ const stateText = serializeControlWhileState(state);
5378
+ const iterationInputs = {
5379
+ ...inputValues,
5380
+ state: stateText,
5381
+ iteration: String(iteration),
5382
+ };
5383
+ if (whileSubflows) {
5384
+ const conditionFrame = await workspaceRunSubflowFrame({
5385
+ root,
5386
+ scopedRoot,
5387
+ payload,
5388
+ userCtx,
5389
+ opts,
5390
+ graph,
5391
+ parentNodeId: nodeId,
5392
+ parentDefinitionId: defId,
5393
+ subflowId: whileSubflows.conditionId,
5394
+ inputValues: {
5395
+ state: stateText,
5396
+ iteration: String(iteration),
5397
+ idempotencyKey,
5398
+ },
5399
+ signal: stepSignal,
5400
+ emit,
5401
+ eventContext: { iteration, whileRole: "condition" },
5402
+ });
5403
+ const conditionStep = parseControlWhileStepResult(JSON.stringify({
5404
+ decision: String(conditionFrame.resultValues.decision || "").trim().toLowerCase(),
5405
+ summary: String(conditionFrame.resultValues.summary || ""),
5406
+ }), state);
5407
+ if (conditionStep.decision !== "continue") return JSON.stringify(conditionStep);
5408
+
5409
+ const bodyFrame = await workspaceRunSubflowFrame({
5410
+ root,
5411
+ scopedRoot,
5412
+ payload,
5413
+ userCtx,
5414
+ opts,
5415
+ graph,
5416
+ parentNodeId: nodeId,
5417
+ parentDefinitionId: defId,
5418
+ subflowId: whileSubflows.bodyId,
5419
+ inputValues: {
5420
+ state: stateText,
5421
+ iteration: String(iteration),
5422
+ idempotencyKey,
5423
+ },
5424
+ signal: stepSignal,
5425
+ emit,
5426
+ eventContext: { iteration, whileRole: "body" },
5427
+ });
5428
+ const nextStateText = String(bodyFrame.resultValues.state || "").trim();
5429
+ if (!nextStateText) {
5430
+ throw new Error(`control.while ${nodeId} body subflow ${whileSubflows.bodyId} returned an empty state`);
5431
+ }
5432
+ const nextState = normalizeControlWhileInitialState(nextStateText);
5433
+ return JSON.stringify({
5434
+ decision: "continue",
5435
+ state: nextState,
5436
+ summary: String(bodyFrame.resultValues.summary || conditionStep.summary || ""),
5437
+ });
5438
+ }
5439
+ const runPackage = workspaceCreateNodeRunPackage(runTmpRoot, `${nodeId}-iteration-${iteration}`, {
5440
+ scopedRoot,
5441
+ cwd,
5442
+ task: stepScript || stepScriptRef,
5443
+ inputValues: iterationInputs,
5444
+ });
5445
+ const runtimeInputValues = { ...iterationInputs, ...(runPackage.inputValues || {}) };
5446
+ let activeStepChild = null;
5447
+ let activeStepChildOptions = {};
5448
+ const terminateStep = () => terminateWorkspaceChild(activeStepChild, activeStepChildOptions);
5449
+ stepSignal?.addEventListener("abort", terminateStep, { once: true });
5450
+ try {
5451
+ const content = await workspaceRunToolNodejsScript({
5452
+ scopedRoot,
5453
+ cwd,
5454
+ instance: { ...instance, script: stepScript, scriptRef: stepScript ? "" : stepScriptRef },
5455
+ inputValues: runtimeInputValues,
5456
+ runPackage,
5457
+ userCtx,
5458
+ envOverlay: {
5459
+ ...runEnv,
5460
+ AGENTFLOW_WHILE_STATE: stateText,
5461
+ AGENTFLOW_WHILE_ITERATION: String(iteration),
5462
+ AGENTFLOW_WHILE_MAX_ITERATIONS: String(config.maxIterations),
5463
+ AGENTFLOW_WHILE_TIMEOUT_MS: String(config.timeoutMs),
5464
+ AGENTFLOW_WHILE_IDEMPOTENCY_KEY: idempotencyKey,
5465
+ },
5466
+ emit: (event) => emit({ ...event, nodeId, iteration }),
5467
+ signal: stepSignal,
5468
+ stderrAsStatus: true,
5469
+ maxStdoutBytes: MAX_CONTROL_WHILE_STEP_STDOUT_BYTES,
5470
+ maxStderrBytes: MAX_CONTROL_WHILE_STEP_STDERR_BYTES,
5471
+ onActiveChild: (child, childOptions = {}) => {
5472
+ activeStepChild = child || null;
5473
+ activeStepChildOptions = childOptions;
5474
+ if (stepSignal?.aborted && child) terminateStep();
5475
+ if (typeof opts.onActiveChild === "function") opts.onActiveChild(child, childOptions);
5476
+ },
5477
+ });
5478
+ return workspaceStructuredAgentOutput(content).result || content;
5479
+ } finally {
5480
+ stepSignal?.removeEventListener("abort", terminateStep);
5481
+ }
5482
+ },
5483
+ });
5484
+ const resultContent = JSON.stringify({
5485
+ decision: loop.decision,
5486
+ iterations: loop.iterations,
5487
+ state: loop.state,
5488
+ summary: loop.summary,
5489
+ history: loop.history,
5490
+ });
5491
+ let nextInstance = workspaceSetOutputSlot(instance, "result", resultContent);
5492
+ nextInstance = workspaceSetOutputSlot(nextInstance, "state", serializeControlWhileState(loop.state));
5493
+ nextInstance = workspaceSetOutputSlot(nextInstance, "decision", loop.decision);
5494
+ nextInstance = workspaceSetOutputSlot(nextInstance, "iterations", String(loop.iterations));
5495
+ nextInstance = workspaceSetOutputSlot(nextInstance, "summary", loop.summary);
5496
+ nextInstance = workspaceSetOutputSlot(nextInstance, "history", JSON.stringify(loop.history));
5497
+ nextInstance = workspaceSetOutputSlot(nextInstance, "checkpointFingerprint", expectedCheckpointFingerprint);
5498
+ graph.instances[nodeId] = nextInstance;
5499
+ const updatedDisplays = publishNodeOutput(nodeId, resultContent);
5500
+ emit({ type: "graph", nodeId, displayNodeIds: updatedDisplays, graph });
5501
+ if (loop.decision === "fail") {
5502
+ emit({
5503
+ type: "node-failed",
5504
+ nodeId,
5505
+ definitionId: defId,
5506
+ decision: loop.decision,
5507
+ iterations: loop.iterations,
5508
+ summary: loop.summary,
5509
+ });
5510
+ throw new Error(loop.summary || "control.while step failed");
5511
+ }
5512
+ emit({
5513
+ type: "node-done",
5514
+ nodeId,
5515
+ definitionId: defId,
5516
+ decision: loop.decision,
5517
+ iterations: loop.iterations,
5518
+ summary: loop.summary,
5519
+ });
5520
+ if (loop.decision === "wait") {
5521
+ runtimePauseNodeIds.push(nodeId);
5522
+ break;
5523
+ }
5524
+ continue;
5525
+ }
5526
+
4964
5527
  if (defId === "provide_str" || defId === "provide_password") {
4965
5528
  const content = workspaceInstanceText(instance);
4966
5529
  publishNodeOutput(nodeId, content, { emitGraph: true });
@@ -4968,6 +5531,21 @@ export async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {},
4968
5531
  continue;
4969
5532
  }
4970
5533
 
5534
+ if (defId === "provide_json") {
5535
+ const raw = workspaceSlotValue(Array.isArray(instance.output) ? instance.output[0] : null) || workspaceInstanceText(instance);
5536
+ let parsed;
5537
+ try {
5538
+ parsed = JSON.parse(String(raw || "").trim());
5539
+ } catch (error) {
5540
+ throw new Error(`provide.json ${nodeId} received invalid JSON: ${error.message}`);
5541
+ }
5542
+ const content = JSON.stringify(parsed);
5543
+ graph.instances[nodeId] = workspaceSetOutputSlot(instance, "value", content);
5544
+ publishNodeOutput(nodeId, content, { emitGraph: true });
5545
+ emit({ type: "node-done", nodeId, definitionId: defId });
5546
+ continue;
5547
+ }
5548
+
4971
5549
  if (defId === "provide_bool") {
4972
5550
  const raw = workspaceSlotValue(Array.isArray(instance.output) ? instance.output[0] : null) || workspaceInstanceText(instance);
4973
5551
  const content = ["true", "1", "yes", "on"].includes(String(raw || "").trim().toLowerCase()) ? "true" : "false";
@@ -5451,7 +6029,11 @@ export async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {},
5451
6029
  signal,
5452
6030
  onActiveChild: opts.onActiveChild,
5453
6031
  });
5454
- const normalizedAgentOutput = workspacePublishAgentOutputFiles(workspaceStructuredAgentOutput(content), runPackage);
6032
+ const structuredAgentOutput = workspaceStructuredAgentOutput(content);
6033
+ const nodeOutput = String(instance.marketplaceRef || "").trim()
6034
+ ? workspaceMaterializeNodePackageOutputValues(structuredAgentOutput, runPackage, instance)
6035
+ : structuredAgentOutput;
6036
+ const normalizedAgentOutput = workspacePublishAgentOutputFiles(nodeOutput, runPackage);
5455
6037
  const resultContent = normalizedAgentOutput.result || content;
5456
6038
  recordNodeOutput(nodeId, resultContent);
5457
6039
  const slotUpdate = workspaceApplyAgentOutputSlots(instance, normalizedAgentOutput);
@@ -5651,11 +6233,12 @@ export async function runWorkspaceGraph(root, scopedRoot, payload, userCtx = {},
5651
6233
  workspaceCleanupAutoWorktrees(autoCleanupWorktrees, graph, emit);
5652
6234
  workspaceCleanupTmpRoot(runTmpRoot, userCtx, emit);
5653
6235
  }
5654
- if (!deferred && pauseNodeIds.length > 0) {
5655
- emit({ type: "paused", nodeIds: pauseNodeIds, message: `Workspace run paused at ${pauseNodeIds.join(", ")}` });
6236
+ const finalPauseNodeIds = Array.from(new Set([...pauseNodeIds, ...runtimePauseNodeIds]));
6237
+ if (!deferred && finalPauseNodeIds.length > 0) {
6238
+ emit({ type: "paused", nodeIds: finalPauseNodeIds, message: `Workspace run paused at ${finalPauseNodeIds.join(", ")}` });
5656
6239
  }
5657
6240
  graph.updatedAt = new Date().toISOString();
5658
- return { graph, events, order, pauseNodeIds, deferred };
6241
+ return { graph, events, order, pauseNodeIds: finalPauseNodeIds, deferred };
5659
6242
  }
5660
6243
 
5661
6244
  export function isWorkspaceRunAbortError(err) {
@@ -6145,6 +6728,10 @@ export function syncWorkspaceSchedulesForGraph(root, scoped, graph, authUser, us
6145
6728
  );
6146
6729
  if (sameSharedFlow || key.startsWith(prefix)) delete schedules[key];
6147
6730
  }
6731
+ if (isWorkspaceDraftDir(scoped?.root || "")) {
6732
+ writeWorkspaceScheduleRegistry({ version: 1, schedules });
6733
+ return [];
6734
+ }
6148
6735
  const instances = graph?.instances && typeof graph.instances === "object" ? graph.instances : {};
6149
6736
  for (const [scheduleNodeId, instance] of Object.entries(instances)) {
6150
6737
  if (String(instance?.definitionId || "") !== "workspace_scheduled_run") continue;