@synergenius/flow-weaver 0.20.4 → 0.20.6

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.4";
9674
+ VERSION = "0.20.6";
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
  );
@@ -10646,29 +10646,50 @@ function buildNodeArgumentsWithContext(opts) {
10646
10646
  const isConstSource = isStartNode(sourceNode) || sourceNode === instanceParent;
10647
10647
  const nonNullAssert = isConstSource ? "" : "!";
10648
10648
  const portType = mapToTypeScript(portConfig.dataType, portConfig.tsType);
10649
+ const needsGuard = portConfig.optional && !isConstSource;
10649
10650
  if (portConfig.dataType === "FUNCTION") {
10650
10651
  const rawVarName = `${varName}_raw`;
10651
- lines.push(
10652
- `${indent}const ${rawVarName} = ${getCall}({ id: '${sourceNode}', portName: '${sourcePort}', executionIndex: ${sourceIdx}${nonNullAssert} });`
10653
- );
10654
- lines.push(
10655
- `${indent}const ${varName}_resolved = resolveFunction(${rawVarName});`
10656
- );
10657
- lines.push(
10658
- `${indent}const ${varName} = ${varName}_resolved.fn as ${portType};`
10659
- );
10652
+ if (needsGuard) {
10653
+ lines.push(
10654
+ `${indent}const ${rawVarName} = ${sourceIdx} !== undefined ? ${getCall}({ id: '${sourceNode}', portName: '${sourcePort}', executionIndex: ${sourceIdx} }) : undefined;`
10655
+ );
10656
+ lines.push(
10657
+ `${indent}const ${varName}_resolved = ${rawVarName} !== undefined ? resolveFunction(${rawVarName}) : undefined;`
10658
+ );
10659
+ lines.push(
10660
+ `${indent}const ${varName} = ${varName}_resolved?.fn as ${portType};`
10661
+ );
10662
+ } else {
10663
+ lines.push(
10664
+ `${indent}const ${rawVarName} = ${getCall}({ id: '${sourceNode}', portName: '${sourcePort}', executionIndex: ${sourceIdx}${nonNullAssert} });`
10665
+ );
10666
+ lines.push(
10667
+ `${indent}const ${varName}_resolved = resolveFunction(${rawVarName});`
10668
+ );
10669
+ lines.push(
10670
+ `${indent}const ${varName} = ${varName}_resolved.fn as ${portType};`
10671
+ );
10672
+ }
10660
10673
  } else {
10661
10674
  const sourceDataType = resolveSourcePortDataType(workflow, sourceNode, sourcePort);
10662
10675
  const coerceExpr = getCoercionWrapper(connection, sourceDataType, portConfig.dataType);
10663
- const getExpr = `${getCall}({ id: '${sourceNode}', portName: '${sourcePort}', executionIndex: ${sourceIdx}${nonNullAssert} })`;
10664
- if (coerceExpr) {
10676
+ if (needsGuard) {
10677
+ const getExpr = `${getCall}({ id: '${sourceNode}', portName: '${sourcePort}', executionIndex: ${sourceIdx} })`;
10678
+ const wrappedExpr = coerceExpr ? `${coerceExpr}(${getExpr})` : getExpr;
10665
10679
  lines.push(
10666
- `${indent}const ${varName} = ${coerceExpr}(${getExpr}) as ${portType};`
10680
+ `${indent}const ${varName} = ${sourceIdx} !== undefined ? ${wrappedExpr} as ${portType} : undefined;`
10667
10681
  );
10668
10682
  } else {
10669
- lines.push(
10670
- `${indent}const ${varName} = ${getExpr} as ${portType};`
10671
- );
10683
+ const getExpr = `${getCall}({ id: '${sourceNode}', portName: '${sourcePort}', executionIndex: ${sourceIdx}${nonNullAssert} })`;
10684
+ if (coerceExpr) {
10685
+ lines.push(
10686
+ `${indent}const ${varName} = ${coerceExpr}(${getExpr}) as ${portType};`
10687
+ );
10688
+ } else {
10689
+ lines.push(
10690
+ `${indent}const ${varName} = ${getExpr} as ${portType};`
10691
+ );
10692
+ }
10672
10693
  }
10673
10694
  }
10674
10695
  } else {
@@ -39966,6 +39987,277 @@ var init_agent_rules = __esm({
39966
39987
  }
39967
39988
  });
39968
39989
 
39990
+ // src/validation/design-rules.ts
39991
+ function resolveNodeType2(ast, instance) {
39992
+ return ast.nodeTypes.find(
39993
+ (nt) => nt.name === instance.nodeType || nt.functionName === instance.nodeType
39994
+ );
39995
+ }
39996
+ function getOutgoing2(ast, nodeId, portName) {
39997
+ return ast.connections.filter((c) => {
39998
+ if (c.from.node !== nodeId) return false;
39999
+ if (portName && c.from.port !== portName) return false;
40000
+ return true;
40001
+ });
40002
+ }
40003
+ function getIncoming(ast, nodeId, portName) {
40004
+ return ast.connections.filter((c) => {
40005
+ if (c.to.node !== nodeId) return false;
40006
+ if (portName && c.to.port !== portName) return false;
40007
+ return true;
40008
+ });
40009
+ }
40010
+ function isStepPort(portName, portDef) {
40011
+ if (portDef?.dataType === "STEP") return true;
40012
+ return STEP_PORTS.has(portName);
40013
+ }
40014
+ function getReachableNodes(ast, startNode) {
40015
+ const visited = /* @__PURE__ */ new Set();
40016
+ const queue = [startNode];
40017
+ while (queue.length > 0) {
40018
+ const current2 = queue.shift();
40019
+ if (visited.has(current2)) continue;
40020
+ visited.add(current2);
40021
+ for (const conn of ast.connections) {
40022
+ if (conn.from.node === current2 && !visited.has(conn.to.node)) {
40023
+ queue.push(conn.to.node);
40024
+ }
40025
+ }
40026
+ }
40027
+ return visited;
40028
+ }
40029
+ function getDesignValidationRules() {
40030
+ return designValidationRules;
40031
+ }
40032
+ var STEP_PORTS, asyncNoErrorPathRule, scopeNoFailureExitRule, unboundedRetryRule, fanoutNoFaninRule, exitDataUnreachableRule, pullCandidateRule, pullUnusedRule, designValidationRules;
40033
+ var init_design_rules = __esm({
40034
+ "src/validation/design-rules.ts"() {
40035
+ "use strict";
40036
+ STEP_PORTS = /* @__PURE__ */ new Set(["execute", "onSuccess", "onFailure", "start", "success", "failure"]);
40037
+ asyncNoErrorPathRule = {
40038
+ name: "DESIGN_ASYNC_NO_ERROR_PATH",
40039
+ validate(ast) {
40040
+ const errors2 = [];
40041
+ for (const instance of ast.instances) {
40042
+ const nt = resolveNodeType2(ast, instance);
40043
+ if (!nt) continue;
40044
+ if (!nt.isAsync) continue;
40045
+ if (!nt.hasFailurePort) continue;
40046
+ const failureConns = getOutgoing2(ast, instance.id, "onFailure");
40047
+ if (failureConns.length === 0) {
40048
+ errors2.push({
40049
+ type: "warning",
40050
+ code: "DESIGN_ASYNC_NO_ERROR_PATH",
40051
+ message: `Async node '${instance.id}' has no onFailure connection. Async operations (network, disk, AI) can fail, and errors will be silently lost.`,
40052
+ node: instance.id
40053
+ });
40054
+ }
40055
+ }
40056
+ return errors2;
40057
+ }
40058
+ };
40059
+ scopeNoFailureExitRule = {
40060
+ name: "DESIGN_SCOPE_NO_FAILURE_EXIT",
40061
+ validate(ast) {
40062
+ const errors2 = [];
40063
+ for (const instance of ast.instances) {
40064
+ const nt = resolveNodeType2(ast, instance);
40065
+ if (!nt) continue;
40066
+ const scopeNames = nt.scopes ?? (nt.scope ? [nt.scope] : []);
40067
+ if (scopeNames.length === 0) continue;
40068
+ if (!nt.hasFailurePort) continue;
40069
+ const failureConns = getOutgoing2(ast, instance.id, "onFailure");
40070
+ const failureConns2 = getOutgoing2(ast, instance.id, "failure");
40071
+ if (failureConns.length === 0 && failureConns2.length === 0) {
40072
+ errors2.push({
40073
+ type: "warning",
40074
+ code: "DESIGN_SCOPE_NO_FAILURE_EXIT",
40075
+ message: `Scope node '${instance.id}' has no failure path out. If all iterations fail, execution stalls with no error surfaced.`,
40076
+ node: instance.id
40077
+ });
40078
+ }
40079
+ }
40080
+ return errors2;
40081
+ }
40082
+ };
40083
+ unboundedRetryRule = {
40084
+ name: "DESIGN_UNBOUNDED_RETRY",
40085
+ validate(ast) {
40086
+ const errors2 = [];
40087
+ const retryPatterns = /retry|repeat|loop|poll|backoff/i;
40088
+ for (const instance of ast.instances) {
40089
+ const nt = resolveNodeType2(ast, instance);
40090
+ if (!nt) continue;
40091
+ const scopeNames = nt.scopes ?? (nt.scope ? [nt.scope] : []);
40092
+ if (scopeNames.length === 0) continue;
40093
+ const nameHint = `${nt.name} ${nt.functionName} ${nt.label ?? ""}`;
40094
+ if (!retryPatterns.test(nameHint)) continue;
40095
+ const limitInputs = Object.keys(nt.inputs).filter(
40096
+ (p) => /max|limit|attempts|retries|count/i.test(p)
40097
+ );
40098
+ if (limitInputs.length === 0) {
40099
+ errors2.push({
40100
+ type: "warning",
40101
+ code: "DESIGN_UNBOUNDED_RETRY",
40102
+ message: `Scope node '${instance.id}' appears to be a retry loop but has no visible attempt limit input. This could loop indefinitely.`,
40103
+ node: instance.id
40104
+ });
40105
+ }
40106
+ }
40107
+ return errors2;
40108
+ }
40109
+ };
40110
+ fanoutNoFaninRule = {
40111
+ name: "DESIGN_FANOUT_NO_FANIN",
40112
+ validate(ast) {
40113
+ const errors2 = [];
40114
+ for (const instance of ast.instances) {
40115
+ const nt = resolveNodeType2(ast, instance);
40116
+ const stepOutConns = getOutgoing2(ast, instance.id).filter((c) => {
40117
+ if (nt) {
40118
+ const portDef = nt.outputs[c.from.port];
40119
+ return isStepPort(c.from.port, portDef);
40120
+ }
40121
+ return isStepPort(c.from.port);
40122
+ });
40123
+ const stepTargets = [...new Set(stepOutConns.map((c) => c.to.node))].filter(
40124
+ (n) => n !== "Exit"
40125
+ );
40126
+ if (stepTargets.length < 3) continue;
40127
+ const reachableSets = stepTargets.map((target) => getReachableNodes(ast, target));
40128
+ const allNodes = /* @__PURE__ */ new Set();
40129
+ let hasMerge = false;
40130
+ for (const reachable of reachableSets) {
40131
+ for (const node of reachable) {
40132
+ if (allNodes.has(node)) {
40133
+ hasMerge = true;
40134
+ break;
40135
+ }
40136
+ }
40137
+ if (hasMerge) break;
40138
+ for (const node of reachable) {
40139
+ allNodes.add(node);
40140
+ }
40141
+ }
40142
+ if (!hasMerge) {
40143
+ for (const target of stepTargets) {
40144
+ const targetInst = ast.instances.find((i) => i.id === target);
40145
+ if (!targetInst) continue;
40146
+ const targetNt = resolveNodeType2(ast, targetInst);
40147
+ if (!targetNt) continue;
40148
+ const hasMergePort = Object.values(targetNt.inputs).some((p) => p.mergeStrategy);
40149
+ if (hasMergePort) {
40150
+ hasMerge = true;
40151
+ break;
40152
+ }
40153
+ }
40154
+ }
40155
+ if (!hasMerge) {
40156
+ errors2.push({
40157
+ type: "warning",
40158
+ code: "DESIGN_FANOUT_NO_FANIN",
40159
+ 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.`,
40160
+ node: instance.id
40161
+ });
40162
+ }
40163
+ }
40164
+ return errors2;
40165
+ }
40166
+ };
40167
+ exitDataUnreachableRule = {
40168
+ name: "DESIGN_EXIT_DATA_UNREACHABLE",
40169
+ validate(ast) {
40170
+ const errors2 = [];
40171
+ const stepPorts = /* @__PURE__ */ new Set(["onSuccess", "onFailure"]);
40172
+ for (const [portName, portDef] of Object.entries(ast.exitPorts)) {
40173
+ if (portDef.dataType === "STEP") continue;
40174
+ const incoming = getIncoming(ast, "Exit", portName);
40175
+ if (incoming.length > 0) continue;
40176
+ const hasPullProvider = ast.instances.some((inst) => {
40177
+ const isPull = inst.config?.pullExecution || resolveNodeType2(ast, inst)?.defaultConfig?.pullExecution;
40178
+ if (!isPull) return false;
40179
+ return getOutgoing2(ast, inst.id).some(
40180
+ (c) => c.to.node === "Exit" && c.to.port === portName
40181
+ );
40182
+ });
40183
+ if (!hasPullProvider) {
40184
+ errors2.push({
40185
+ type: "warning",
40186
+ code: "DESIGN_EXIT_DATA_UNREACHABLE",
40187
+ message: `Exit port '${portName}' has no incoming connection and no pull-execution node provides it. The output will be undefined.`
40188
+ });
40189
+ }
40190
+ }
40191
+ return errors2;
40192
+ }
40193
+ };
40194
+ pullCandidateRule = {
40195
+ name: "DESIGN_PULL_CANDIDATE",
40196
+ validate(ast) {
40197
+ const errors2 = [];
40198
+ for (const instance of ast.instances) {
40199
+ const nt = resolveNodeType2(ast, instance);
40200
+ if (!nt) continue;
40201
+ if (instance.config?.pullExecution || nt.defaultConfig?.pullExecution) continue;
40202
+ if (nt.expression) continue;
40203
+ const incomingStep = getIncoming(ast, instance.id).filter((c) => {
40204
+ const portDef = nt.inputs[c.to.port];
40205
+ return isStepPort(c.to.port, portDef);
40206
+ });
40207
+ if (incomingStep.length > 0) continue;
40208
+ const dataOutputs = Object.keys(nt.outputs).filter((p) => !STEP_PORTS.has(p));
40209
+ const hasConsumedOutput = dataOutputs.some(
40210
+ (port) => getOutgoing2(ast, instance.id, port).length > 0
40211
+ );
40212
+ if (hasConsumedOutput) {
40213
+ errors2.push({
40214
+ type: "warning",
40215
+ code: "DESIGN_PULL_CANDIDATE",
40216
+ 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.`,
40217
+ node: instance.id
40218
+ });
40219
+ }
40220
+ }
40221
+ return errors2;
40222
+ }
40223
+ };
40224
+ pullUnusedRule = {
40225
+ name: "DESIGN_PULL_UNUSED",
40226
+ validate(ast) {
40227
+ const errors2 = [];
40228
+ for (const instance of ast.instances) {
40229
+ const nt = resolveNodeType2(ast, instance);
40230
+ if (!nt) continue;
40231
+ const isPull = instance.config?.pullExecution || nt.defaultConfig?.pullExecution;
40232
+ if (!isPull) continue;
40233
+ const dataOutputs = Object.keys(nt.outputs).filter((p) => !STEP_PORTS.has(p));
40234
+ const hasConnectedOutput = dataOutputs.some(
40235
+ (port) => getOutgoing2(ast, instance.id, port).length > 0
40236
+ );
40237
+ if (!hasConnectedOutput) {
40238
+ errors2.push({
40239
+ type: "warning",
40240
+ code: "DESIGN_PULL_UNUSED",
40241
+ message: `Node '${instance.id}' is marked with pullExecution but no downstream node reads its data output. It will never execute.`,
40242
+ node: instance.id
40243
+ });
40244
+ }
40245
+ }
40246
+ return errors2;
40247
+ }
40248
+ };
40249
+ designValidationRules = [
40250
+ asyncNoErrorPathRule,
40251
+ scopeNoFailureExitRule,
40252
+ unboundedRetryRule,
40253
+ fanoutNoFaninRule,
40254
+ exitDataUnreachableRule,
40255
+ pullCandidateRule,
40256
+ pullUnusedRule
40257
+ ];
40258
+ }
40259
+ });
40260
+
39969
40261
  // src/api/validate.ts
39970
40262
  var validate_exports = {};
39971
40263
  __export(validate_exports, {
@@ -39975,6 +40267,7 @@ function validateWorkflow(ast, options) {
39975
40267
  const result = validator.validate(ast, { mode: options?.mode });
39976
40268
  const allRules = [
39977
40269
  ...getAgentValidationRules(),
40270
+ ...getDesignValidationRules(),
39978
40271
  ...validationRuleRegistry.getApplicableRules(ast),
39979
40272
  ...options?.customRules || []
39980
40273
  ];
@@ -40009,6 +40302,7 @@ var init_validate = __esm({
40009
40302
  "use strict";
40010
40303
  init_validator();
40011
40304
  init_agent_rules();
40305
+ init_design_rules();
40012
40306
  init_validation_registry();
40013
40307
  }
40014
40308
  });
@@ -46491,6 +46785,7 @@ function aggregateResults(
46491
46785
  * @connect iterator.results -> Exit.results
46492
46786
  * @connect iterator.results -> aggregator.results
46493
46787
  * @connect iterator.onSuccess -> aggregator.execute
46788
+ * @connect iterator.onFailure -> aggregator.execute
46494
46789
  * @connect aggregator.successCount -> Exit.successCount
46495
46790
  * @connect aggregator.failedCount -> Exit.failedCount
46496
46791
  * @connect aggregator.onSuccess -> Exit.onSuccess
@@ -47128,6 +47423,7 @@ const TOOL_IMPLEMENTATIONS: Record<string, ToolFn> = {
47128
47423
  * @flowWeaver nodeType
47129
47424
  * @label Agent Loop
47130
47425
  * @input userMessage [order:1] - User's input message
47426
+ * @input [maxIterations] [order:2] - Maximum loop iterations (default: 10)
47131
47427
  * @input success scope:iteration [order:0] - From LLM onSuccess
47132
47428
  * @input failure scope:iteration [order:1] - From LLM onFailure
47133
47429
  * @input llmResponse scope:iteration [order:2] - LLM response
@@ -47142,6 +47438,7 @@ const TOOL_IMPLEMENTATIONS: Record<string, ToolFn> = {
47142
47438
  async function agentLoop(
47143
47439
  execute: boolean,
47144
47440
  userMessage: string,
47441
+ maxIterations: number = MAX_ITERATIONS,
47145
47442
  iteration: (start: boolean, state: AgentState) => Promise<{
47146
47443
  success: boolean;
47147
47444
  failure: boolean;
@@ -47160,7 +47457,7 @@ async function agentLoop(
47160
47457
  terminated: false,
47161
47458
  };
47162
47459
 
47163
- while (state.iteration < MAX_ITERATIONS) {
47460
+ while (state.iteration < maxIterations) {
47164
47461
  const result = await iteration(true, state);
47165
47462
 
47166
47463
  state.iteration++;
@@ -47298,6 +47595,7 @@ async function executeTools(
47298
47595
  * @connect llm.onSuccess -> loop.success:iteration
47299
47596
  * @connect llm.onFailure -> loop.failure:iteration
47300
47597
  * @connect tools.messages -> loop.toolMessages:iteration
47598
+ * @connect tools.onFailure -> loop.failure:iteration
47301
47599
  * @connect loop.response -> Exit.response
47302
47600
  * @connect loop.onSuccess -> Exit.onSuccess
47303
47601
  * @connect loop.onFailure -> Exit.onFailure
@@ -47427,6 +47725,7 @@ const TOOL_IMPLEMENTATIONS: Record<string, (input: string) => Promise<string>> =
47427
47725
  * @label ReAct Loop
47428
47726
  * @input execute [order:0] - Execute
47429
47727
  * @input task [order:1] - Task for the agent
47728
+ * @input [maxSteps] [order:2] - Maximum reasoning steps (default: 10)
47430
47729
  * @input success scope:step [order:0] - Iteration succeeded
47431
47730
  * @input failure scope:step [order:1] - Iteration failed
47432
47731
  * @input thought scope:step [order:2] - Agent's reasoning
@@ -47442,6 +47741,7 @@ const TOOL_IMPLEMENTATIONS: Record<string, (input: string) => Promise<string>> =
47442
47741
  async function reactLoop(
47443
47742
  execute: boolean,
47444
47743
  task: string,
47744
+ maxSteps: number = MAX_STEPS,
47445
47745
  step: (start: boolean, messages: LLMMessage[]) => Promise<{
47446
47746
  success: boolean;
47447
47747
  failure: boolean;
@@ -47457,7 +47757,7 @@ async function reactLoop(
47457
47757
 
47458
47758
  const messages: LLMMessage[] = [{ role: 'user', content: task }];
47459
47759
 
47460
- for (let i = 0; i < MAX_STEPS; i++) {
47760
+ for (let i = 0; i < maxSteps; i++) {
47461
47761
  const result = await step(true, messages);
47462
47762
 
47463
47763
  if (result.failure) {
@@ -47770,7 +48070,7 @@ Answer:\`;
47770
48070
  * RAG Pipeline for knowledge-based Q&A
47771
48071
  *
47772
48072
  * @flowWeaver workflow
47773
- * @node retriever retrieve [position: -50 0] [color: "teal"] [icon: "search"] [suppress: "UNUSED_OUTPUT_PORT"]
48073
+ * @node retriever retrieve [position: -50 0] [color: "teal"] [icon: "search"] [suppress: "UNUSED_OUTPUT_PORT", "DESIGN_ASYNC_NO_ERROR_PATH"]
47774
48074
  * @node generator generate [position: 200 0] [color: "purple"] [icon: "autoAwesome"]
47775
48075
  * @position Start -300 0
47776
48076
  * @position Exit 400 0
@@ -78182,6 +78482,71 @@ var errorMappers = {
78182
78482
  code: error2.code
78183
78483
  };
78184
78484
  },
78485
+ // ── Design quality rules ─────────────────────────────────────────────
78486
+ DESIGN_ASYNC_NO_ERROR_PATH(error2) {
78487
+ const nodeName = error2.node || "unknown";
78488
+ return {
78489
+ title: "Async Node Missing Error Path",
78490
+ 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.`,
78491
+ fix: `Connect ${nodeName}.onFailure to an error handler, retry node, or Exit.onFailure.`,
78492
+ code: error2.code
78493
+ };
78494
+ },
78495
+ DESIGN_SCOPE_NO_FAILURE_EXIT(error2) {
78496
+ const nodeName = error2.node || "unknown";
78497
+ return {
78498
+ title: "Scope Missing Failure Exit",
78499
+ explanation: `Scope node '${nodeName}' has no failure path out. If all iterations fail, execution stalls with no error surfaced upstream.`,
78500
+ fix: `Connect ${nodeName}.onFailure to an error handler or Exit.onFailure so scope failures propagate.`,
78501
+ code: error2.code
78502
+ };
78503
+ },
78504
+ DESIGN_UNBOUNDED_RETRY(error2) {
78505
+ const nodeName = error2.node || "unknown";
78506
+ return {
78507
+ title: "Unbounded Retry Loop",
78508
+ explanation: `Scope node '${nodeName}' looks like a retry loop but has no visible attempt limit input. This could loop indefinitely on persistent failures.`,
78509
+ 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.`,
78510
+ code: error2.code
78511
+ };
78512
+ },
78513
+ DESIGN_FANOUT_NO_FANIN(error2) {
78514
+ const nodeName = error2.node || "unknown";
78515
+ return {
78516
+ title: "Fan-Out Without Fan-In",
78517
+ 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.`,
78518
+ fix: `Add a merge node downstream where the parallel branches converge, or wire them to a shared node before Exit.`,
78519
+ code: error2.code
78520
+ };
78521
+ },
78522
+ DESIGN_EXIT_DATA_UNREACHABLE(error2) {
78523
+ const quoted = extractQuoted(error2.message);
78524
+ const portName = quoted[0] || "unknown";
78525
+ return {
78526
+ title: "Exit Data Unreachable",
78527
+ explanation: `Exit port '${portName}' has no incoming data connection and no pull-execution node provides it. The workflow will return undefined for this output.`,
78528
+ fix: `Connect a node's data output to Exit.${portName}, or add a pull-execution node that computes this value on demand.`,
78529
+ code: error2.code
78530
+ };
78531
+ },
78532
+ DESIGN_PULL_CANDIDATE(error2) {
78533
+ const nodeName = error2.node || "unknown";
78534
+ return {
78535
+ title: "Pull Execution Candidate",
78536
+ 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.`,
78537
+ fix: `Add [pullExecution: execute] to the @node annotation so the node runs on demand when downstream nodes read its output.`,
78538
+ code: error2.code
78539
+ };
78540
+ },
78541
+ DESIGN_PULL_UNUSED(error2) {
78542
+ const nodeName = error2.node || "unknown";
78543
+ return {
78544
+ title: "Unused Pull Execution",
78545
+ 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.`,
78546
+ fix: `Connect a data output from '${nodeName}' to a downstream node, or remove the pullExecution config if the node isn't needed.`,
78547
+ code: error2.code
78548
+ };
78549
+ },
78185
78550
  COERCE_TYPE_MISMATCH(error2) {
78186
78551
  const coerceMatch = error2.message.match(/`as (\w+)`/);
78187
78552
  const coerceType = coerceMatch?.[1] || "unknown";
@@ -100052,7 +100417,15 @@ var ERROR_HINTS = {
100052
100417
  AGENT_UNGUARDED_TOOL_EXECUTOR: 'Add a human-approval node before the tool executor. Use fw_scaffold(template="human-approval") to create one',
100053
100418
  AGENT_MISSING_MEMORY_IN_LOOP: 'Add a conversation-memory node inside the loop scope. Use fw_scaffold(template="conversation-memory") to create one',
100054
100419
  AGENT_LLM_NO_FALLBACK: "Add a retry or fallback node between the LLM onFailure and Exit. Consider a second LLM provider as fallback",
100055
- AGENT_TOOL_NO_OUTPUT_HANDLING: 'Use fw_modify(operation="addConnection") to wire tool output ports to downstream nodes'
100420
+ AGENT_TOOL_NO_OUTPUT_HANDLING: 'Use fw_modify(operation="addConnection") to wire tool output ports to downstream nodes',
100421
+ // Design quality rules
100422
+ DESIGN_ASYNC_NO_ERROR_PATH: 'Use fw_modify(operation="addConnection", params={from:"<nodeId>.onFailure", to:"Exit.onFailure"}) or add a retry/error handler',
100423
+ DESIGN_SCOPE_NO_FAILURE_EXIT: 'Use fw_modify(operation="addConnection", params={from:"<nodeId>.onFailure", to:"Exit.onFailure"}) to surface scope failures',
100424
+ DESIGN_UNBOUNDED_RETRY: "Add a maxAttempts or retries input port to the retry node type to bound the loop",
100425
+ DESIGN_FANOUT_NO_FANIN: "Add a merge node downstream where parallel branches converge before Exit",
100426
+ DESIGN_EXIT_DATA_UNREACHABLE: 'Use fw_modify(operation="addConnection") to connect a node output to this Exit port, or add a pull-execution node',
100427
+ DESIGN_PULL_CANDIDATE: "Add [pullExecution: execute] to the @node annotation so the node runs on demand",
100428
+ DESIGN_PULL_UNUSED: "Connect a data output from this node to a downstream consumer, or remove pullExecution"
100056
100429
  };
100057
100430
  function addHintsToItems(items, friendlyErrorFn) {
100058
100431
  return items.map((item) => {
@@ -106710,7 +107083,7 @@ function displayInstalledPackage(pkg) {
106710
107083
  // src/cli/index.ts
106711
107084
  init_logger();
106712
107085
  init_error_utils();
106713
- var version2 = true ? "0.20.4" : "0.0.0-dev";
107086
+ var version2 = true ? "0.20.6" : "0.0.0-dev";
106714
107087
  var program2 = new Command();
106715
107088
  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", () => {
106716
107089
  logger.banner(version2);
@@ -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) {
@@ -116,6 +116,7 @@ function aggregateResults(
116
116
  * @connect iterator.results -> Exit.results
117
117
  * @connect iterator.results -> aggregator.results
118
118
  * @connect iterator.onSuccess -> aggregator.execute
119
+ * @connect iterator.onFailure -> aggregator.execute
119
120
  * @connect aggregator.successCount -> Exit.successCount
120
121
  * @connect aggregator.failedCount -> Exit.failedCount
121
122
  * @connect aggregator.onSuccess -> Exit.onSuccess
@@ -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