@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.
- package/dist/api/validate.js +2 -0
- package/dist/cli/flow-weaver.mjs +378 -13
- package/dist/cli/templates/shared/llm-types.d.ts +1 -1
- package/dist/cli/templates/shared/llm-types.js +1 -1
- 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 -1
- 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/unified.js +25 -3
- 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 +1 -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.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
|
|
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
|
);
|
|
@@ -10970,7 +10970,21 @@ function generateControlFlowWithExecutionContext(workflow, nodeTypes, isAsync2,
|
|
|
10970
10970
|
region.failureNodes.delete(instanceId);
|
|
10971
10971
|
});
|
|
10972
10972
|
});
|
|
10973
|
-
const
|
|
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
|
|
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 <
|
|
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 <
|
|
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.
|
|
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
|
|
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
|
|
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 <
|
|
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) {
|
|
@@ -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
|