@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.
- package/dist/api/validate.js +2 -0
- package/dist/cli/flow-weaver.mjs +396 -23
- package/dist/cli/templates/workflows/ai-agent.js +4 -1
- package/dist/cli/templates/workflows/ai-rag.js +1 -1
- package/dist/cli/templates/workflows/ai-react.js +3 -1
- package/dist/cli/templates/workflows/foreach.js +1 -0
- package/dist/doc-metadata/extractors/error-codes.d.ts +1 -1
- package/dist/doc-metadata/extractors/error-codes.js +50 -0
- package/dist/friendly-errors.js +65 -0
- package/dist/generated-version.d.ts +1 -1
- package/dist/generated-version.js +1 -1
- package/dist/generator/code-utils.js +24 -7
- package/dist/mcp/response-utils.js +8 -0
- package/dist/validation/design-rules.d.ts +61 -0
- package/dist/validation/design-rules.js +377 -0
- package/docs/reference/error-codes.md +74 -0
- package/package.json +5 -1
package/dist/api/validate.js
CHANGED
|
@@ -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
|
];
|
package/dist/cli/flow-weaver.mjs
CHANGED
|
@@ -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.
|
|
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
|
|
10459
|
+
const isStepPort2 = portName === "success" || portName === "failure";
|
|
10460
10460
|
const defaultValue = portName === "success" ? "true" : portName === "failure" ? "false" : "undefined";
|
|
10461
|
-
if (
|
|
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
|
-
|
|
10652
|
-
|
|
10653
|
-
|
|
10654
|
-
|
|
10655
|
-
|
|
10656
|
-
|
|
10657
|
-
|
|
10658
|
-
|
|
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
|
-
|
|
10664
|
-
|
|
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} = ${
|
|
10680
|
+
`${indent}const ${varName} = ${sourceIdx} !== undefined ? ${wrappedExpr} as ${portType} : undefined;`
|
|
10667
10681
|
);
|
|
10668
10682
|
} else {
|
|
10669
|
-
|
|
10670
|
-
|
|
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 <
|
|
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 <
|
|
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.
|
|
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 <
|
|
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 <
|
|
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
|