@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.
Files changed (42) 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 +74 -11
  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/marketplace.mjs +10 -2
  8. package/bin/lib/node-package-manifest.mjs +7 -2
  9. package/bin/lib/node-ui-kit.mjs +156 -0
  10. package/bin/lib/paths.mjs +2 -0
  11. package/bin/lib/workspace-flow-store.mjs +11 -2
  12. package/bin/lib/workspace-graph-merge.mjs +3 -0
  13. package/bin/lib/workspace-routes.mjs +10 -1
  14. package/bin/lib/workspace-server.mjs +592 -11
  15. package/bin/pipeline/validate-flow.mjs +3 -3
  16. package/builtin/nodes/control_parse_json.md +25 -0
  17. package/builtin/nodes/control_subflow_call.md +27 -0
  18. package/builtin/nodes/control_while.md +124 -0
  19. package/builtin/nodes/provide_json.md +14 -0
  20. package/builtin/nodes/workspace_subflow_input.md +14 -0
  21. package/builtin/pipelines/subflow-preview/workspace.flow.js +29 -0
  22. package/builtin/pipelines/subflow-preview/workspace.layout.json +38 -0
  23. package/builtin/pipelines/subflow-preview/workspace.nodes.json +19 -0
  24. package/builtin/pipelines/subflow-preview/workspace.state.json +93 -0
  25. package/builtin/pipelines/while-subflow-preview/workspace.flow.js +55 -0
  26. package/builtin/pipelines/while-subflow-preview/workspace.layout.json +66 -0
  27. package/builtin/pipelines/while-subflow-preview/workspace.nodes.json +49 -0
  28. package/builtin/pipelines/while-subflow-preview/workspace.state.json +194 -0
  29. package/builtin/web-ui/dist/assets/{WorkflowAssistantThread-CY19DsYq.js → WorkflowAssistantThread-Bfo9Ythw.js} +1 -1
  30. package/builtin/web-ui/dist/assets/index-XbeI5foV.js +872 -0
  31. package/builtin/web-ui/dist/assets/index-e1omCEau.css +1 -0
  32. package/builtin/web-ui/dist/index.html +2 -2
  33. package/package.json +2 -1
  34. package/reference/flow-control-capabilities.md +41 -16
  35. package/shared/slot-types.js +58 -0
  36. package/skills/agentflow-flow-dsl/SKILL.md +60 -6
  37. package/skills/agentflow-flow-dsl/references/node-calls.md +3 -0
  38. package/skills/agentflow-flow-dsl/references/subflow-authoring.md +230 -0
  39. package/skills/agentflow-node-dsl/SKILL.md +39 -0
  40. package/skills/agentflow-node-reference/references/builtin-nodes.md +25 -0
  41. package/builtin/web-ui/dist/assets/index-B9ppXv7e.css +0 -1
  42. 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
- 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;
4706
4831
  });
4707
4832
  child.stderr.on("data", (chunk) => {
4708
- 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;
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: "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)}` });
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: Array.isArray(payload?.forceNodeIds) ? payload.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 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);
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
- if (!deferred && pauseNodeIds.length > 0) {
5655
- 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(", ")}` });
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) {