@synergenius/flow-weaver 0.20.5 → 0.20.7

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.
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import { validator } from "../validator.js";
8
8
  import { getAgentValidationRules } from "../validation/agent-rules.js";
9
+ import { getDesignValidationRules } from "../validation/design-rules.js";
9
10
  import { validationRuleRegistry } from "./validation-registry.js";
10
11
  /**
11
12
  * Validates a workflow AST
@@ -24,6 +25,7 @@ export function validateWorkflow(ast, options) {
24
25
  // including CI/CD when applicable), and custom rules
25
26
  const allRules = [
26
27
  ...getAgentValidationRules(),
28
+ ...getDesignValidationRules(),
27
29
  ...validationRuleRegistry.getApplicableRules(ast),
28
30
  ...(options?.customRules || []),
29
31
  ];
@@ -9671,7 +9671,7 @@ var VERSION;
9671
9671
  var init_generated_version = __esm({
9672
9672
  "src/generated-version.ts"() {
9673
9673
  "use strict";
9674
- VERSION = "0.20.5";
9674
+ VERSION = "0.20.7";
9675
9675
  }
9676
9676
  });
9677
9677
 
@@ -10456,9 +10456,9 @@ function generateScopeFunctionClosure(scopeName, parentNodeId, parentNodeType, w
10456
10456
  const childInstance = childInstances.find((c) => c.id === sourceNode);
10457
10457
  const sourceNodeTypeName = childInstance?.nodeType ?? "";
10458
10458
  const varAddr = `{ id: '${sourceNode}', portName: '${sourcePort}', executionIndex: ${toValidIdentifier(sourceNode)}Idx, nodeTypeName: '${sourceNodeTypeName}' }`;
10459
- const isStepPort = portName === "success" || portName === "failure";
10459
+ const isStepPort2 = portName === "success" || portName === "failure";
10460
10460
  const defaultValue = portName === "success" ? "true" : portName === "failure" ? "false" : "undefined";
10461
- if (isStepPort) {
10461
+ if (isStepPort2) {
10462
10462
  lines.push(
10463
10463
  ` const ${varName} = ctx.hasVariable(${varAddr}) ? ${getCallAfterMerge}(${varAddr}) as ${portType} : ${defaultValue};`
10464
10464
  );
@@ -10970,7 +10970,21 @@ function generateControlFlowWithExecutionContext(workflow, nodeTypes, isAsync2,
10970
10970
  region.failureNodes.delete(instanceId);
10971
10971
  });
10972
10972
  });
10973
- const nodesPromotedFromBranches = /* @__PURE__ */ new Set();
10973
+ const nodesInBothBranches = /* @__PURE__ */ new Set();
10974
+ branchRegions.forEach((region) => {
10975
+ region.successNodes.forEach((nodeId) => {
10976
+ if (region.failureNodes.has(nodeId)) {
10977
+ nodesInBothBranches.add(nodeId);
10978
+ }
10979
+ });
10980
+ });
10981
+ branchRegions.forEach((region) => {
10982
+ nodesInBothBranches.forEach((nodeId) => {
10983
+ region.successNodes.delete(nodeId);
10984
+ region.failureNodes.delete(nodeId);
10985
+ });
10986
+ });
10987
+ const nodesPromotedFromBranches = new Set(nodesInBothBranches);
10974
10988
  branchRegions.forEach((region, branchNodeId) => {
10975
10989
  const allBranchNodes = /* @__PURE__ */ new Set([...region.successNodes, ...region.failureNodes]);
10976
10990
  allBranchNodes.forEach((nodeId) => {
@@ -11134,7 +11148,7 @@ function generateControlFlowWithExecutionContext(workflow, nodeTypes, isAsync2,
11134
11148
  }
11135
11149
  });
11136
11150
  if (stepSourceConditions.length > 0) {
11137
- const condition = stepSourceConditions.join(" && ");
11151
+ const condition = stepSourceConditions.length === 1 ? stepSourceConditions[0] : stepSourceConditions.join(" || ");
11138
11152
  lines.push(` if (${condition}) {`);
11139
11153
  chainIndent = " ";
11140
11154
  chainNeedsClose = true;
@@ -11174,7 +11188,7 @@ function generateControlFlowWithExecutionContext(workflow, nodeTypes, isAsync2,
11174
11188
  }
11175
11189
  });
11176
11190
  if (stepSourceConditions.length > 0) {
11177
- const condition = stepSourceConditions.join(" && ");
11191
+ const condition = stepSourceConditions.length === 1 ? stepSourceConditions[0] : stepSourceConditions.join(" || ");
11178
11192
  lines.push(` if (${condition}) {`);
11179
11193
  branchIndent = " ";
11180
11194
  branchNeedsClose = true;
@@ -39987,6 +40001,277 @@ var init_agent_rules = __esm({
39987
40001
  }
39988
40002
  });
39989
40003
 
40004
+ // src/validation/design-rules.ts
40005
+ function resolveNodeType2(ast, instance) {
40006
+ return ast.nodeTypes.find(
40007
+ (nt) => nt.name === instance.nodeType || nt.functionName === instance.nodeType
40008
+ );
40009
+ }
40010
+ function getOutgoing2(ast, nodeId, portName) {
40011
+ return ast.connections.filter((c) => {
40012
+ if (c.from.node !== nodeId) return false;
40013
+ if (portName && c.from.port !== portName) return false;
40014
+ return true;
40015
+ });
40016
+ }
40017
+ function getIncoming(ast, nodeId, portName) {
40018
+ return ast.connections.filter((c) => {
40019
+ if (c.to.node !== nodeId) return false;
40020
+ if (portName && c.to.port !== portName) return false;
40021
+ return true;
40022
+ });
40023
+ }
40024
+ function isStepPort(portName, portDef) {
40025
+ if (portDef?.dataType === "STEP") return true;
40026
+ return STEP_PORTS.has(portName);
40027
+ }
40028
+ function getReachableNodes(ast, startNode) {
40029
+ const visited = /* @__PURE__ */ new Set();
40030
+ const queue = [startNode];
40031
+ while (queue.length > 0) {
40032
+ const current2 = queue.shift();
40033
+ if (visited.has(current2)) continue;
40034
+ visited.add(current2);
40035
+ for (const conn of ast.connections) {
40036
+ if (conn.from.node === current2 && !visited.has(conn.to.node)) {
40037
+ queue.push(conn.to.node);
40038
+ }
40039
+ }
40040
+ }
40041
+ return visited;
40042
+ }
40043
+ function getDesignValidationRules() {
40044
+ return designValidationRules;
40045
+ }
40046
+ var STEP_PORTS, asyncNoErrorPathRule, scopeNoFailureExitRule, unboundedRetryRule, fanoutNoFaninRule, exitDataUnreachableRule, pullCandidateRule, pullUnusedRule, designValidationRules;
40047
+ var init_design_rules = __esm({
40048
+ "src/validation/design-rules.ts"() {
40049
+ "use strict";
40050
+ STEP_PORTS = /* @__PURE__ */ new Set(["execute", "onSuccess", "onFailure", "start", "success", "failure"]);
40051
+ asyncNoErrorPathRule = {
40052
+ name: "DESIGN_ASYNC_NO_ERROR_PATH",
40053
+ validate(ast) {
40054
+ const errors2 = [];
40055
+ for (const instance of ast.instances) {
40056
+ const nt = resolveNodeType2(ast, instance);
40057
+ if (!nt) continue;
40058
+ if (!nt.isAsync) continue;
40059
+ if (!nt.hasFailurePort) continue;
40060
+ const failureConns = getOutgoing2(ast, instance.id, "onFailure");
40061
+ if (failureConns.length === 0) {
40062
+ errors2.push({
40063
+ type: "warning",
40064
+ code: "DESIGN_ASYNC_NO_ERROR_PATH",
40065
+ message: `Async node '${instance.id}' has no onFailure connection. Async operations (network, disk, AI) can fail, and errors will be silently lost.`,
40066
+ node: instance.id
40067
+ });
40068
+ }
40069
+ }
40070
+ return errors2;
40071
+ }
40072
+ };
40073
+ scopeNoFailureExitRule = {
40074
+ name: "DESIGN_SCOPE_NO_FAILURE_EXIT",
40075
+ validate(ast) {
40076
+ const errors2 = [];
40077
+ for (const instance of ast.instances) {
40078
+ const nt = resolveNodeType2(ast, instance);
40079
+ if (!nt) continue;
40080
+ const scopeNames = nt.scopes ?? (nt.scope ? [nt.scope] : []);
40081
+ if (scopeNames.length === 0) continue;
40082
+ if (!nt.hasFailurePort) continue;
40083
+ const failureConns = getOutgoing2(ast, instance.id, "onFailure");
40084
+ const failureConns2 = getOutgoing2(ast, instance.id, "failure");
40085
+ if (failureConns.length === 0 && failureConns2.length === 0) {
40086
+ errors2.push({
40087
+ type: "warning",
40088
+ code: "DESIGN_SCOPE_NO_FAILURE_EXIT",
40089
+ message: `Scope node '${instance.id}' has no failure path out. If all iterations fail, execution stalls with no error surfaced.`,
40090
+ node: instance.id
40091
+ });
40092
+ }
40093
+ }
40094
+ return errors2;
40095
+ }
40096
+ };
40097
+ unboundedRetryRule = {
40098
+ name: "DESIGN_UNBOUNDED_RETRY",
40099
+ validate(ast) {
40100
+ const errors2 = [];
40101
+ const retryPatterns = /retry|repeat|loop|poll|backoff/i;
40102
+ for (const instance of ast.instances) {
40103
+ const nt = resolveNodeType2(ast, instance);
40104
+ if (!nt) continue;
40105
+ const scopeNames = nt.scopes ?? (nt.scope ? [nt.scope] : []);
40106
+ if (scopeNames.length === 0) continue;
40107
+ const nameHint = `${nt.name} ${nt.functionName} ${nt.label ?? ""}`;
40108
+ if (!retryPatterns.test(nameHint)) continue;
40109
+ const limitInputs = Object.keys(nt.inputs).filter(
40110
+ (p) => /max|limit|attempts|retries|count/i.test(p)
40111
+ );
40112
+ if (limitInputs.length === 0) {
40113
+ errors2.push({
40114
+ type: "warning",
40115
+ code: "DESIGN_UNBOUNDED_RETRY",
40116
+ message: `Scope node '${instance.id}' appears to be a retry loop but has no visible attempt limit input. This could loop indefinitely.`,
40117
+ node: instance.id
40118
+ });
40119
+ }
40120
+ }
40121
+ return errors2;
40122
+ }
40123
+ };
40124
+ fanoutNoFaninRule = {
40125
+ name: "DESIGN_FANOUT_NO_FANIN",
40126
+ validate(ast) {
40127
+ const errors2 = [];
40128
+ for (const instance of ast.instances) {
40129
+ const nt = resolveNodeType2(ast, instance);
40130
+ const stepOutConns = getOutgoing2(ast, instance.id).filter((c) => {
40131
+ if (nt) {
40132
+ const portDef = nt.outputs[c.from.port];
40133
+ return isStepPort(c.from.port, portDef);
40134
+ }
40135
+ return isStepPort(c.from.port);
40136
+ });
40137
+ const stepTargets = [...new Set(stepOutConns.map((c) => c.to.node))].filter(
40138
+ (n) => n !== "Exit"
40139
+ );
40140
+ if (stepTargets.length < 3) continue;
40141
+ const reachableSets = stepTargets.map((target) => getReachableNodes(ast, target));
40142
+ const allNodes = /* @__PURE__ */ new Set();
40143
+ let hasMerge = false;
40144
+ for (const reachable of reachableSets) {
40145
+ for (const node of reachable) {
40146
+ if (allNodes.has(node)) {
40147
+ hasMerge = true;
40148
+ break;
40149
+ }
40150
+ }
40151
+ if (hasMerge) break;
40152
+ for (const node of reachable) {
40153
+ allNodes.add(node);
40154
+ }
40155
+ }
40156
+ if (!hasMerge) {
40157
+ for (const target of stepTargets) {
40158
+ const targetInst = ast.instances.find((i) => i.id === target);
40159
+ if (!targetInst) continue;
40160
+ const targetNt = resolveNodeType2(ast, targetInst);
40161
+ if (!targetNt) continue;
40162
+ const hasMergePort = Object.values(targetNt.inputs).some((p) => p.mergeStrategy);
40163
+ if (hasMergePort) {
40164
+ hasMerge = true;
40165
+ break;
40166
+ }
40167
+ }
40168
+ }
40169
+ if (!hasMerge) {
40170
+ errors2.push({
40171
+ type: "warning",
40172
+ code: "DESIGN_FANOUT_NO_FANIN",
40173
+ message: `Node '${instance.id}' fans out to ${stepTargets.length} step targets (${stepTargets.join(", ")}) but those paths never merge back. Data from parallel branches may be lost.`,
40174
+ node: instance.id
40175
+ });
40176
+ }
40177
+ }
40178
+ return errors2;
40179
+ }
40180
+ };
40181
+ exitDataUnreachableRule = {
40182
+ name: "DESIGN_EXIT_DATA_UNREACHABLE",
40183
+ validate(ast) {
40184
+ const errors2 = [];
40185
+ const stepPorts = /* @__PURE__ */ new Set(["onSuccess", "onFailure"]);
40186
+ for (const [portName, portDef] of Object.entries(ast.exitPorts)) {
40187
+ if (portDef.dataType === "STEP") continue;
40188
+ const incoming = getIncoming(ast, "Exit", portName);
40189
+ if (incoming.length > 0) continue;
40190
+ const hasPullProvider = ast.instances.some((inst) => {
40191
+ const isPull = inst.config?.pullExecution || resolveNodeType2(ast, inst)?.defaultConfig?.pullExecution;
40192
+ if (!isPull) return false;
40193
+ return getOutgoing2(ast, inst.id).some(
40194
+ (c) => c.to.node === "Exit" && c.to.port === portName
40195
+ );
40196
+ });
40197
+ if (!hasPullProvider) {
40198
+ errors2.push({
40199
+ type: "warning",
40200
+ code: "DESIGN_EXIT_DATA_UNREACHABLE",
40201
+ message: `Exit port '${portName}' has no incoming connection and no pull-execution node provides it. The output will be undefined.`
40202
+ });
40203
+ }
40204
+ }
40205
+ return errors2;
40206
+ }
40207
+ };
40208
+ pullCandidateRule = {
40209
+ name: "DESIGN_PULL_CANDIDATE",
40210
+ validate(ast) {
40211
+ const errors2 = [];
40212
+ for (const instance of ast.instances) {
40213
+ const nt = resolveNodeType2(ast, instance);
40214
+ if (!nt) continue;
40215
+ if (instance.config?.pullExecution || nt.defaultConfig?.pullExecution) continue;
40216
+ if (nt.expression) continue;
40217
+ const incomingStep = getIncoming(ast, instance.id).filter((c) => {
40218
+ const portDef = nt.inputs[c.to.port];
40219
+ return isStepPort(c.to.port, portDef);
40220
+ });
40221
+ if (incomingStep.length > 0) continue;
40222
+ const dataOutputs = Object.keys(nt.outputs).filter((p) => !STEP_PORTS.has(p));
40223
+ const hasConsumedOutput = dataOutputs.some(
40224
+ (port) => getOutgoing2(ast, instance.id, port).length > 0
40225
+ );
40226
+ if (hasConsumedOutput) {
40227
+ errors2.push({
40228
+ type: "warning",
40229
+ code: "DESIGN_PULL_CANDIDATE",
40230
+ message: `Node '${instance.id}' has no incoming step connection but its data outputs are consumed downstream. Consider adding [pullExecution: execute] so it executes on demand.`,
40231
+ node: instance.id
40232
+ });
40233
+ }
40234
+ }
40235
+ return errors2;
40236
+ }
40237
+ };
40238
+ pullUnusedRule = {
40239
+ name: "DESIGN_PULL_UNUSED",
40240
+ validate(ast) {
40241
+ const errors2 = [];
40242
+ for (const instance of ast.instances) {
40243
+ const nt = resolveNodeType2(ast, instance);
40244
+ if (!nt) continue;
40245
+ const isPull = instance.config?.pullExecution || nt.defaultConfig?.pullExecution;
40246
+ if (!isPull) continue;
40247
+ const dataOutputs = Object.keys(nt.outputs).filter((p) => !STEP_PORTS.has(p));
40248
+ const hasConnectedOutput = dataOutputs.some(
40249
+ (port) => getOutgoing2(ast, instance.id, port).length > 0
40250
+ );
40251
+ if (!hasConnectedOutput) {
40252
+ errors2.push({
40253
+ type: "warning",
40254
+ code: "DESIGN_PULL_UNUSED",
40255
+ message: `Node '${instance.id}' is marked with pullExecution but no downstream node reads its data output. It will never execute.`,
40256
+ node: instance.id
40257
+ });
40258
+ }
40259
+ }
40260
+ return errors2;
40261
+ }
40262
+ };
40263
+ designValidationRules = [
40264
+ asyncNoErrorPathRule,
40265
+ scopeNoFailureExitRule,
40266
+ unboundedRetryRule,
40267
+ fanoutNoFaninRule,
40268
+ exitDataUnreachableRule,
40269
+ pullCandidateRule,
40270
+ pullUnusedRule
40271
+ ];
40272
+ }
40273
+ });
40274
+
39990
40275
  // src/api/validate.ts
39991
40276
  var validate_exports = {};
39992
40277
  __export(validate_exports, {
@@ -39996,6 +40281,7 @@ function validateWorkflow(ast, options) {
39996
40281
  const result = validator.validate(ast, { mode: options?.mode });
39997
40282
  const allRules = [
39998
40283
  ...getAgentValidationRules(),
40284
+ ...getDesignValidationRules(),
39999
40285
  ...validationRuleRegistry.getApplicableRules(ast),
40000
40286
  ...options?.customRules || []
40001
40287
  ];
@@ -40030,6 +40316,7 @@ var init_validate = __esm({
40030
40316
  "use strict";
40031
40317
  init_validator();
40032
40318
  init_agent_rules();
40319
+ init_design_rules();
40033
40320
  init_validation_registry();
40034
40321
  }
40035
40322
  });
@@ -46497,7 +46784,7 @@ function aggregateResults(
46497
46784
 
46498
46785
  /**
46499
46786
  * @flowWeaver workflow
46500
- * @node iterator forEachItem [position: -90 0] [color: "purple"] [icon: "repeat"]
46787
+ * @node iterator forEachItem [position: -90 0] [color: "purple"] [icon: "repeat"] [suppress: "DESIGN_SCOPE_NO_FAILURE_EXIT"]
46501
46788
  * @node processor processItem iterator.processItem [color: "blue"] [icon: "settings"]
46502
46789
  * @node aggregator aggregateResults [position: 270 0] [color: "teal"] [icon: "inventory"]
46503
46790
  * @position Start -450 0
@@ -46978,7 +47265,7 @@ interface LLMProvider {
46978
47265
  async chat(messages) {
46979
47266
  const lastMessage = messages[messages.length - 1];
46980
47267
  return {
46981
- content: \`[Mock response to: \${lastMessage.content.slice(0, 50)}...]\`,
47268
+ content: \`[Mock response to: \${(lastMessage?.content ?? '').slice(0, 50)}...]\`,
46982
47269
  toolCalls: [],
46983
47270
  finishReason: 'stop',
46984
47271
  usage: { promptTokens: 10, completionTokens: 20 },
@@ -47149,6 +47436,7 @@ const TOOL_IMPLEMENTATIONS: Record<string, ToolFn> = {
47149
47436
  * @flowWeaver nodeType
47150
47437
  * @label Agent Loop
47151
47438
  * @input userMessage [order:1] - User's input message
47439
+ * @input [maxIterations] [order:2] - Maximum loop iterations (default: 10)
47152
47440
  * @input success scope:iteration [order:0] - From LLM onSuccess
47153
47441
  * @input failure scope:iteration [order:1] - From LLM onFailure
47154
47442
  * @input llmResponse scope:iteration [order:2] - LLM response
@@ -47163,6 +47451,7 @@ const TOOL_IMPLEMENTATIONS: Record<string, ToolFn> = {
47163
47451
  async function agentLoop(
47164
47452
  execute: boolean,
47165
47453
  userMessage: string,
47454
+ maxIterations: number = MAX_ITERATIONS,
47166
47455
  iteration: (start: boolean, state: AgentState) => Promise<{
47167
47456
  success: boolean;
47168
47457
  failure: boolean;
@@ -47181,7 +47470,7 @@ async function agentLoop(
47181
47470
  terminated: false,
47182
47471
  };
47183
47472
 
47184
- while (state.iteration < MAX_ITERATIONS) {
47473
+ while (state.iteration < maxIterations) {
47185
47474
  const result = await iteration(true, state);
47186
47475
 
47187
47476
  state.iteration++;
@@ -47319,6 +47608,7 @@ async function executeTools(
47319
47608
  * @connect llm.onSuccess -> loop.success:iteration
47320
47609
  * @connect llm.onFailure -> loop.failure:iteration
47321
47610
  * @connect tools.messages -> loop.toolMessages:iteration
47611
+ * @connect tools.onFailure -> loop.failure:iteration
47322
47612
  * @connect loop.response -> Exit.response
47323
47613
  * @connect loop.onSuccess -> Exit.onSuccess
47324
47614
  * @connect loop.onFailure -> Exit.onFailure
@@ -47448,6 +47738,7 @@ const TOOL_IMPLEMENTATIONS: Record<string, (input: string) => Promise<string>> =
47448
47738
  * @label ReAct Loop
47449
47739
  * @input execute [order:0] - Execute
47450
47740
  * @input task [order:1] - Task for the agent
47741
+ * @input [maxSteps] [order:2] - Maximum reasoning steps (default: 10)
47451
47742
  * @input success scope:step [order:0] - Iteration succeeded
47452
47743
  * @input failure scope:step [order:1] - Iteration failed
47453
47744
  * @input thought scope:step [order:2] - Agent's reasoning
@@ -47463,6 +47754,7 @@ const TOOL_IMPLEMENTATIONS: Record<string, (input: string) => Promise<string>> =
47463
47754
  async function reactLoop(
47464
47755
  execute: boolean,
47465
47756
  task: string,
47757
+ maxSteps: number = MAX_STEPS,
47466
47758
  step: (start: boolean, messages: LLMMessage[]) => Promise<{
47467
47759
  success: boolean;
47468
47760
  failure: boolean;
@@ -47478,7 +47770,7 @@ async function reactLoop(
47478
47770
 
47479
47771
  const messages: LLMMessage[] = [{ role: 'user', content: task }];
47480
47772
 
47481
- for (let i = 0; i < MAX_STEPS; i++) {
47773
+ for (let i = 0; i < maxSteps; i++) {
47482
47774
  const result = await step(true, messages);
47483
47775
 
47484
47776
  if (result.failure) {
@@ -47791,7 +48083,7 @@ Answer:\`;
47791
48083
  * RAG Pipeline for knowledge-based Q&A
47792
48084
  *
47793
48085
  * @flowWeaver workflow
47794
- * @node retriever retrieve [position: -50 0] [color: "teal"] [icon: "search"] [suppress: "UNUSED_OUTPUT_PORT"]
48086
+ * @node retriever retrieve [position: -50 0] [color: "teal"] [icon: "search"] [suppress: "UNUSED_OUTPUT_PORT", "DESIGN_ASYNC_NO_ERROR_PATH"]
47795
48087
  * @node generator generate [position: 200 0] [color: "purple"] [icon: "autoAwesome"]
47796
48088
  * @position Start -300 0
47797
48089
  * @position Exit 400 0
@@ -78203,6 +78495,71 @@ var errorMappers = {
78203
78495
  code: error2.code
78204
78496
  };
78205
78497
  },
78498
+ // ── Design quality rules ─────────────────────────────────────────────
78499
+ DESIGN_ASYNC_NO_ERROR_PATH(error2) {
78500
+ const nodeName = error2.node || "unknown";
78501
+ return {
78502
+ title: "Async Node Missing Error Path",
78503
+ explanation: `Async node '${nodeName}' has no onFailure connection. Async operations (network calls, file I/O, AI calls) can fail, and errors will be silently lost.`,
78504
+ fix: `Connect ${nodeName}.onFailure to an error handler, retry node, or Exit.onFailure.`,
78505
+ code: error2.code
78506
+ };
78507
+ },
78508
+ DESIGN_SCOPE_NO_FAILURE_EXIT(error2) {
78509
+ const nodeName = error2.node || "unknown";
78510
+ return {
78511
+ title: "Scope Missing Failure Exit",
78512
+ explanation: `Scope node '${nodeName}' has no failure path out. If all iterations fail, execution stalls with no error surfaced upstream.`,
78513
+ fix: `Connect ${nodeName}.onFailure to an error handler or Exit.onFailure so scope failures propagate.`,
78514
+ code: error2.code
78515
+ };
78516
+ },
78517
+ DESIGN_UNBOUNDED_RETRY(error2) {
78518
+ const nodeName = error2.node || "unknown";
78519
+ return {
78520
+ title: "Unbounded Retry Loop",
78521
+ explanation: `Scope node '${nodeName}' looks like a retry loop but has no visible attempt limit input. This could loop indefinitely on persistent failures.`,
78522
+ fix: `Add a maxAttempts or retries input port to the node type, or use a counter to break out of the loop after N attempts.`,
78523
+ code: error2.code
78524
+ };
78525
+ },
78526
+ DESIGN_FANOUT_NO_FANIN(error2) {
78527
+ const nodeName = error2.node || "unknown";
78528
+ return {
78529
+ title: "Fan-Out Without Fan-In",
78530
+ explanation: `Node '${nodeName}' fans out to multiple step targets, but those paths never merge back to a shared downstream node. Data from parallel branches may be lost.`,
78531
+ fix: `Add a merge node downstream where the parallel branches converge, or wire them to a shared node before Exit.`,
78532
+ code: error2.code
78533
+ };
78534
+ },
78535
+ DESIGN_EXIT_DATA_UNREACHABLE(error2) {
78536
+ const quoted = extractQuoted(error2.message);
78537
+ const portName = quoted[0] || "unknown";
78538
+ return {
78539
+ title: "Exit Data Unreachable",
78540
+ explanation: `Exit port '${portName}' has no incoming data connection and no pull-execution node provides it. The workflow will return undefined for this output.`,
78541
+ fix: `Connect a node's data output to Exit.${portName}, or add a pull-execution node that computes this value on demand.`,
78542
+ code: error2.code
78543
+ };
78544
+ },
78545
+ DESIGN_PULL_CANDIDATE(error2) {
78546
+ const nodeName = error2.node || "unknown";
78547
+ return {
78548
+ title: "Pull Execution Candidate",
78549
+ explanation: `Node '${nodeName}' has no incoming step connection but its data outputs are consumed downstream. Without a step trigger or pullExecution, this node may never execute.`,
78550
+ fix: `Add [pullExecution: execute] to the @node annotation so the node runs on demand when downstream nodes read its output.`,
78551
+ code: error2.code
78552
+ };
78553
+ },
78554
+ DESIGN_PULL_UNUSED(error2) {
78555
+ const nodeName = error2.node || "unknown";
78556
+ return {
78557
+ title: "Unused Pull Execution",
78558
+ explanation: `Node '${nodeName}' is marked with pullExecution but no downstream node reads its data output. The node will never execute since pull execution requires a consumer.`,
78559
+ fix: `Connect a data output from '${nodeName}' to a downstream node, or remove the pullExecution config if the node isn't needed.`,
78560
+ code: error2.code
78561
+ };
78562
+ },
78206
78563
  COERCE_TYPE_MISMATCH(error2) {
78207
78564
  const coerceMatch = error2.message.match(/`as (\w+)`/);
78208
78565
  const coerceType = coerceMatch?.[1] || "unknown";
@@ -100073,7 +100430,15 @@ var ERROR_HINTS = {
100073
100430
  AGENT_UNGUARDED_TOOL_EXECUTOR: 'Add a human-approval node before the tool executor. Use fw_scaffold(template="human-approval") to create one',
100074
100431
  AGENT_MISSING_MEMORY_IN_LOOP: 'Add a conversation-memory node inside the loop scope. Use fw_scaffold(template="conversation-memory") to create one',
100075
100432
  AGENT_LLM_NO_FALLBACK: "Add a retry or fallback node between the LLM onFailure and Exit. Consider a second LLM provider as fallback",
100076
- AGENT_TOOL_NO_OUTPUT_HANDLING: 'Use fw_modify(operation="addConnection") to wire tool output ports to downstream nodes'
100433
+ AGENT_TOOL_NO_OUTPUT_HANDLING: 'Use fw_modify(operation="addConnection") to wire tool output ports to downstream nodes',
100434
+ // Design quality rules
100435
+ DESIGN_ASYNC_NO_ERROR_PATH: 'Use fw_modify(operation="addConnection", params={from:"<nodeId>.onFailure", to:"Exit.onFailure"}) or add a retry/error handler',
100436
+ DESIGN_SCOPE_NO_FAILURE_EXIT: 'Use fw_modify(operation="addConnection", params={from:"<nodeId>.onFailure", to:"Exit.onFailure"}) to surface scope failures',
100437
+ DESIGN_UNBOUNDED_RETRY: "Add a maxAttempts or retries input port to the retry node type to bound the loop",
100438
+ DESIGN_FANOUT_NO_FANIN: "Add a merge node downstream where parallel branches converge before Exit",
100439
+ DESIGN_EXIT_DATA_UNREACHABLE: 'Use fw_modify(operation="addConnection") to connect a node output to this Exit port, or add a pull-execution node',
100440
+ DESIGN_PULL_CANDIDATE: "Add [pullExecution: execute] to the @node annotation so the node runs on demand",
100441
+ DESIGN_PULL_UNUSED: "Connect a data output from this node to a downstream consumer, or remove pullExecution"
100077
100442
  };
100078
100443
  function addHintsToItems(items, friendlyErrorFn) {
100079
100444
  return items.map((item) => {
@@ -106731,7 +107096,7 @@ function displayInstalledPackage(pkg) {
106731
107096
  // src/cli/index.ts
106732
107097
  init_logger();
106733
107098
  init_error_utils();
106734
- var version2 = true ? "0.20.5" : "0.0.0-dev";
107099
+ var version2 = true ? "0.20.7" : "0.0.0-dev";
106735
107100
  var program2 = new Command();
106736
107101
  program2.name("flow-weaver").description("Flow Weaver Annotations - Compile and validate workflow files").option("-v, --version", "Output the current version").option("--no-color", "Disable colors").option("--color", "Force colors").on("option:version", () => {
106737
107102
  logger.banner(version2);
@@ -9,7 +9,7 @@ export declare const LLM_CORE_TYPES = "interface LLMMessage {\n role: 'system'
9
9
  /** Simplified LLM types — for templates that don't need tool calling */
10
10
  export declare const LLM_SIMPLE_TYPES = "interface LLMMessage {\n role: 'system' | 'user' | 'assistant' | 'tool';\n content: string;\n toolCallId?: string;\n}\n\ninterface LLMToolCall {\n id: string;\n name: string;\n arguments: Record<string, unknown>;\n}\n\ninterface LLMResponse {\n content: string | null;\n toolCalls: LLMToolCall[];\n finishReason: 'stop' | 'tool_calls' | 'length' | 'error';\n usage?: { promptTokens: number; completionTokens: number };\n}\n\ninterface LLMProvider {\n chat(messages: LLMMessage[], options?: { systemPrompt?: string; model?: string; temperature?: number; maxTokens?: number }): Promise<LLMResponse>;\n}";
11
11
  /** Mock provider factory code */
12
- export declare const LLM_MOCK_PROVIDER = "const createMockProvider = (): LLMProvider => ({\n async chat(messages) {\n const lastMessage = messages[messages.length - 1];\n return {\n content: `[Mock response to: ${lastMessage.content.slice(0, 50)}...]`,\n toolCalls: [],\n finishReason: 'stop',\n usage: { promptTokens: 10, completionTokens: 20 },\n };\n },\n});\n\nconst llmProvider: LLMProvider = (globalThis as unknown as { __fw_llm_provider__?: LLMProvider }).__fw_llm_provider__ ?? createMockProvider();";
12
+ export declare const LLM_MOCK_PROVIDER = "const createMockProvider = (): LLMProvider => ({\n async chat(messages) {\n const lastMessage = messages[messages.length - 1];\n return {\n content: `[Mock response to: ${(lastMessage?.content ?? '').slice(0, 50)}...]`,\n toolCalls: [],\n finishReason: 'stop',\n usage: { promptTokens: 10, completionTokens: 20 },\n };\n },\n});\n\nconst llmProvider: LLMProvider = (globalThis as unknown as { __fw_llm_provider__?: LLMProvider }).__fw_llm_provider__ ?? createMockProvider();";
13
13
  /** Mock provider with tool calling support (for ai-agent) */
14
14
  export declare const LLM_MOCK_PROVIDER_WITH_TOOLS = "const createMockProvider = (): LLMProvider => ({\n async chat(messages, options) {\n const last = messages[messages.length - 1];\n if (options?.tools && last.content.toLowerCase().includes('search')) {\n return {\n content: null,\n toolCalls: [\n {\n id: 'call_' + Date.now(),\n name: 'search',\n arguments: { query: last.content },\n },\n ],\n finishReason: 'tool_calls',\n usage: { promptTokens: 15, completionTokens: 30 },\n };\n }\n\n return {\n content: '[Mock answer] ' + last.content,\n toolCalls: [],\n finishReason: 'stop',\n usage: { promptTokens: 10, completionTokens: 20 },\n };\n },\n});\n\nconst llmProvider: LLMProvider = (globalThis as unknown as { __fw_llm_provider__?: LLMProvider }).__fw_llm_provider__ ?? createMockProvider();";
15
15
  //# sourceMappingURL=llm-types.d.ts.map
@@ -68,7 +68,7 @@ export const LLM_MOCK_PROVIDER = `const createMockProvider = (): LLMProvider =>
68
68
  async chat(messages) {
69
69
  const lastMessage = messages[messages.length - 1];
70
70
  return {
71
- content: \`[Mock response to: \${lastMessage.content.slice(0, 50)}...]\`,
71
+ content: \`[Mock response to: \${(lastMessage?.content ?? '').slice(0, 50)}...]\`,
72
72
  toolCalls: [],
73
73
  finishReason: 'stop',
74
74
  usage: { promptTokens: 10, completionTokens: 20 },
@@ -128,6 +128,7 @@ const TOOL_IMPLEMENTATIONS: Record<string, ToolFn> = {
128
128
  * @flowWeaver nodeType
129
129
  * @label Agent Loop
130
130
  * @input userMessage [order:1] - User's input message
131
+ * @input [maxIterations] [order:2] - Maximum loop iterations (default: 10)
131
132
  * @input success scope:iteration [order:0] - From LLM onSuccess
132
133
  * @input failure scope:iteration [order:1] - From LLM onFailure
133
134
  * @input llmResponse scope:iteration [order:2] - LLM response
@@ -142,6 +143,7 @@ const TOOL_IMPLEMENTATIONS: Record<string, ToolFn> = {
142
143
  async function agentLoop(
143
144
  execute: boolean,
144
145
  userMessage: string,
146
+ maxIterations: number = MAX_ITERATIONS,
145
147
  iteration: (start: boolean, state: AgentState) => Promise<{
146
148
  success: boolean;
147
149
  failure: boolean;
@@ -160,7 +162,7 @@ async function agentLoop(
160
162
  terminated: false,
161
163
  };
162
164
 
163
- while (state.iteration < MAX_ITERATIONS) {
165
+ while (state.iteration < maxIterations) {
164
166
  const result = await iteration(true, state);
165
167
 
166
168
  state.iteration++;
@@ -298,6 +300,7 @@ async function executeTools(
298
300
  * @connect llm.onSuccess -> loop.success:iteration
299
301
  * @connect llm.onFailure -> loop.failure:iteration
300
302
  * @connect tools.messages -> loop.toolMessages:iteration
303
+ * @connect tools.onFailure -> loop.failure:iteration
301
304
  * @connect loop.response -> Exit.response
302
305
  * @connect loop.onSuccess -> Exit.onSuccess
303
306
  * @connect loop.onFailure -> Exit.onFailure
@@ -145,7 +145,7 @@ Answer:\`;
145
145
  * RAG Pipeline for knowledge-based Q&A
146
146
  *
147
147
  * @flowWeaver workflow
148
- * @node retriever retrieve [position: -50 0] [color: "teal"] [icon: "search"] [suppress: "UNUSED_OUTPUT_PORT"]
148
+ * @node retriever retrieve [position: -50 0] [color: "teal"] [icon: "search"] [suppress: "UNUSED_OUTPUT_PORT", "DESIGN_ASYNC_NO_ERROR_PATH"]
149
149
  * @node generator generate [position: 200 0] [color: "purple"] [icon: "autoAwesome"]
150
150
  * @position Start -300 0
151
151
  * @position Exit 400 0
@@ -97,6 +97,7 @@ const TOOL_IMPLEMENTATIONS: Record<string, (input: string) => Promise<string>> =
97
97
  * @label ReAct Loop
98
98
  * @input execute [order:0] - Execute
99
99
  * @input task [order:1] - Task for the agent
100
+ * @input [maxSteps] [order:2] - Maximum reasoning steps (default: 10)
100
101
  * @input success scope:step [order:0] - Iteration succeeded
101
102
  * @input failure scope:step [order:1] - Iteration failed
102
103
  * @input thought scope:step [order:2] - Agent's reasoning
@@ -112,6 +113,7 @@ const TOOL_IMPLEMENTATIONS: Record<string, (input: string) => Promise<string>> =
112
113
  async function reactLoop(
113
114
  execute: boolean,
114
115
  task: string,
116
+ maxSteps: number = MAX_STEPS,
115
117
  step: (start: boolean, messages: LLMMessage[]) => Promise<{
116
118
  success: boolean;
117
119
  failure: boolean;
@@ -127,7 +129,7 @@ async function reactLoop(
127
129
 
128
130
  const messages: LLMMessage[] = [{ role: 'user', content: task }];
129
131
 
130
- for (let i = 0; i < MAX_STEPS; i++) {
132
+ for (let i = 0; i < maxSteps; i++) {
131
133
  const result = await step(true, messages);
132
134
 
133
135
  if (result.failure) {
@@ -101,7 +101,7 @@ function aggregateResults(
101
101
 
102
102
  /**
103
103
  * @flowWeaver workflow
104
- * @node iterator forEachItem [position: -90 0] [color: "purple"] [icon: "repeat"]
104
+ * @node iterator forEachItem [position: -90 0] [color: "purple"] [icon: "repeat"] [suppress: "DESIGN_SCOPE_NO_FAILURE_EXIT"]
105
105
  * @node processor processItem iterator.processItem [color: "blue"] [icon: "settings"]
106
106
  * @node aggregator aggregateResults [position: 270 0] [color: "teal"] [icon: "inventory"]
107
107
  * @position Start -450 0
@@ -12,7 +12,7 @@ export interface TValidationCodeDoc {
12
12
  severity: 'error' | 'warning';
13
13
  title: string;
14
14
  description: string;
15
- category: 'structural' | 'naming' | 'connection' | 'type' | 'node-ref' | 'graph' | 'data-flow' | 'agent';
15
+ category: 'structural' | 'naming' | 'connection' | 'type' | 'node-ref' | 'graph' | 'data-flow' | 'agent' | 'design';
16
16
  }
17
17
  export declare const VALIDATION_CODES: TValidationCodeDoc[];
18
18
  //# sourceMappingURL=error-codes.d.ts.map