@isaacwasserman/remora 0.1.0
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/LICENSE +674 -0
- package/README.md +99 -0
- package/dist/compiler/index.d.ts +8 -0
- package/dist/compiler/index.d.ts.map +1 -0
- package/dist/compiler/passes/apply-best-practices.d.ts +19 -0
- package/dist/compiler/passes/apply-best-practices.d.ts.map +1 -0
- package/dist/compiler/passes/build-graph.d.ts +7 -0
- package/dist/compiler/passes/build-graph.d.ts.map +1 -0
- package/dist/compiler/passes/generate-constrained-tool-schemas.d.ts +4 -0
- package/dist/compiler/passes/generate-constrained-tool-schemas.d.ts.map +1 -0
- package/dist/compiler/passes/validate-control-flow.d.ts +4 -0
- package/dist/compiler/passes/validate-control-flow.d.ts.map +1 -0
- package/dist/compiler/passes/validate-foreach-target.d.ts +6 -0
- package/dist/compiler/passes/validate-foreach-target.d.ts.map +1 -0
- package/dist/compiler/passes/validate-jmespath.d.ts +4 -0
- package/dist/compiler/passes/validate-jmespath.d.ts.map +1 -0
- package/dist/compiler/passes/validate-references.d.ts +4 -0
- package/dist/compiler/passes/validate-references.d.ts.map +1 -0
- package/dist/compiler/passes/validate-tools.d.ts +4 -0
- package/dist/compiler/passes/validate-tools.d.ts.map +1 -0
- package/dist/compiler/types.d.ts +49 -0
- package/dist/compiler/types.d.ts.map +1 -0
- package/dist/compiler/utils/graph.d.ts +35 -0
- package/dist/compiler/utils/graph.d.ts.map +1 -0
- package/dist/compiler/utils/jmespath-helpers.d.ts +34 -0
- package/dist/compiler/utils/jmespath-helpers.d.ts.map +1 -0
- package/dist/executor/errors.d.ts +32 -0
- package/dist/executor/errors.d.ts.map +1 -0
- package/dist/executor/index.d.ts +20 -0
- package/dist/executor/index.d.ts.map +1 -0
- package/dist/generator/index.d.ts +24 -0
- package/dist/generator/index.d.ts.map +1 -0
- package/dist/generator/prompt.d.ts +6 -0
- package/dist/generator/prompt.d.ts.map +1 -0
- package/dist/lib.d.ts +11 -0
- package/dist/lib.d.ts.map +1 -0
- package/dist/lib.js +1973 -0
- package/dist/lib.js.map +25 -0
- package/dist/types.d.ts +234 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/viewer/edges/workflow-edge.d.ts +3 -0
- package/dist/viewer/edges/workflow-edge.d.ts.map +1 -0
- package/dist/viewer/graph-layout.d.ts +13 -0
- package/dist/viewer/graph-layout.d.ts.map +1 -0
- package/dist/viewer/index.d.ts +5 -0
- package/dist/viewer/index.d.ts.map +1 -0
- package/dist/viewer/index.js +1554 -0
- package/dist/viewer/index.js.map +23 -0
- package/dist/viewer/nodes/base-node.d.ts +17 -0
- package/dist/viewer/nodes/base-node.d.ts.map +1 -0
- package/dist/viewer/nodes/end-node.d.ts +3 -0
- package/dist/viewer/nodes/end-node.d.ts.map +1 -0
- package/dist/viewer/nodes/extract-data-node.d.ts +3 -0
- package/dist/viewer/nodes/extract-data-node.d.ts.map +1 -0
- package/dist/viewer/nodes/for-each-node.d.ts +3 -0
- package/dist/viewer/nodes/for-each-node.d.ts.map +1 -0
- package/dist/viewer/nodes/group-header-node.d.ts +3 -0
- package/dist/viewer/nodes/group-header-node.d.ts.map +1 -0
- package/dist/viewer/nodes/llm-prompt-node.d.ts +3 -0
- package/dist/viewer/nodes/llm-prompt-node.d.ts.map +1 -0
- package/dist/viewer/nodes/start-node.d.ts +3 -0
- package/dist/viewer/nodes/start-node.d.ts.map +1 -0
- package/dist/viewer/nodes/start-step-node.d.ts +3 -0
- package/dist/viewer/nodes/start-step-node.d.ts.map +1 -0
- package/dist/viewer/nodes/switch-case-node.d.ts +3 -0
- package/dist/viewer/nodes/switch-case-node.d.ts.map +1 -0
- package/dist/viewer/nodes/tool-call-node.d.ts +3 -0
- package/dist/viewer/nodes/tool-call-node.d.ts.map +1 -0
- package/dist/viewer/panels/step-detail-panel.d.ts +10 -0
- package/dist/viewer/panels/step-detail-panel.d.ts.map +1 -0
- package/dist/viewer/workflow-viewer.d.ts +9 -0
- package/dist/viewer/workflow-viewer.d.ts.map +1 -0
- package/package.json +78 -0
package/dist/lib.js
ADDED
|
@@ -0,0 +1,1973 @@
|
|
|
1
|
+
// src/compiler/index.ts
|
|
2
|
+
import { asSchema } from "ai";
|
|
3
|
+
|
|
4
|
+
// src/compiler/passes/apply-best-practices.ts
|
|
5
|
+
var rules = [addMissingStartStep, addMissingEndSteps];
|
|
6
|
+
function applyBestPractices(workflow, graph) {
|
|
7
|
+
let result = structuredClone(workflow);
|
|
8
|
+
const allDiagnostics = [];
|
|
9
|
+
for (const rule of rules) {
|
|
10
|
+
const ruleResult = rule(result, graph);
|
|
11
|
+
result = ruleResult.workflow;
|
|
12
|
+
allDiagnostics.push(...ruleResult.diagnostics);
|
|
13
|
+
}
|
|
14
|
+
return { workflow: result, diagnostics: allDiagnostics };
|
|
15
|
+
}
|
|
16
|
+
function addMissingStartStep(workflow, _graph) {
|
|
17
|
+
const diagnostics = [];
|
|
18
|
+
const hasStartStep = workflow.steps.some((s) => s.type === "start");
|
|
19
|
+
if (hasStartStep) {
|
|
20
|
+
return { workflow, diagnostics };
|
|
21
|
+
}
|
|
22
|
+
const startStepId = "__start";
|
|
23
|
+
const oldInitialStepId = workflow.initialStepId;
|
|
24
|
+
const startStep = {
|
|
25
|
+
id: startStepId,
|
|
26
|
+
name: "Start",
|
|
27
|
+
description: "Auto-generated start step",
|
|
28
|
+
type: "start",
|
|
29
|
+
params: {
|
|
30
|
+
inputSchema: {}
|
|
31
|
+
},
|
|
32
|
+
nextStepId: oldInitialStepId
|
|
33
|
+
};
|
|
34
|
+
workflow.steps.unshift(startStep);
|
|
35
|
+
workflow.initialStepId = startStepId;
|
|
36
|
+
diagnostics.push({
|
|
37
|
+
severity: "warning",
|
|
38
|
+
location: { stepId: null, field: "initialStepId" },
|
|
39
|
+
message: "Workflow has no start step; one was automatically added with an empty input schema",
|
|
40
|
+
code: "MISSING_START_STEP"
|
|
41
|
+
});
|
|
42
|
+
return { workflow, diagnostics };
|
|
43
|
+
}
|
|
44
|
+
function addMissingEndSteps(workflow, _graph) {
|
|
45
|
+
const newEndSteps = [];
|
|
46
|
+
for (const step of workflow.steps) {
|
|
47
|
+
if (step.type === "end")
|
|
48
|
+
continue;
|
|
49
|
+
if (step.nextStepId)
|
|
50
|
+
continue;
|
|
51
|
+
const endStepId = `${step.id}_end`;
|
|
52
|
+
step.nextStepId = endStepId;
|
|
53
|
+
newEndSteps.push({
|
|
54
|
+
id: endStepId,
|
|
55
|
+
name: "End",
|
|
56
|
+
description: `End of chain after ${step.id}`,
|
|
57
|
+
type: "end"
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
workflow.steps.push(...newEndSteps);
|
|
61
|
+
return { workflow, diagnostics: [] };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// src/compiler/utils/graph.ts
|
|
65
|
+
function buildStepIndex(steps) {
|
|
66
|
+
const index = new Map;
|
|
67
|
+
const duplicates = [];
|
|
68
|
+
for (const step of steps) {
|
|
69
|
+
if (index.has(step.id)) {
|
|
70
|
+
duplicates.push(step.id);
|
|
71
|
+
} else {
|
|
72
|
+
index.set(step.id, step);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return { index, duplicates };
|
|
76
|
+
}
|
|
77
|
+
function computeSuccessors(stepIndex) {
|
|
78
|
+
const successors = new Map;
|
|
79
|
+
for (const [id, step] of stepIndex) {
|
|
80
|
+
const succs = new Set;
|
|
81
|
+
if (step.nextStepId) {
|
|
82
|
+
succs.add(step.nextStepId);
|
|
83
|
+
}
|
|
84
|
+
if (step.type === "switch-case") {
|
|
85
|
+
for (const c of step.params.cases) {
|
|
86
|
+
succs.add(c.branchBodyStepId);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (step.type === "for-each") {
|
|
90
|
+
succs.add(step.params.loopBodyStepId);
|
|
91
|
+
}
|
|
92
|
+
successors.set(id, succs);
|
|
93
|
+
}
|
|
94
|
+
return successors;
|
|
95
|
+
}
|
|
96
|
+
function computeReachability(initialStepId, successors) {
|
|
97
|
+
const reachable = new Set;
|
|
98
|
+
const queue = [initialStepId];
|
|
99
|
+
while (queue.length > 0) {
|
|
100
|
+
const current = queue.shift();
|
|
101
|
+
if (current === undefined)
|
|
102
|
+
break;
|
|
103
|
+
if (reachable.has(current))
|
|
104
|
+
continue;
|
|
105
|
+
reachable.add(current);
|
|
106
|
+
const succs = successors.get(current);
|
|
107
|
+
if (succs) {
|
|
108
|
+
for (const s of succs) {
|
|
109
|
+
if (!reachable.has(s)) {
|
|
110
|
+
queue.push(s);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return reachable;
|
|
116
|
+
}
|
|
117
|
+
function detectCycles(stepIndex, successors) {
|
|
118
|
+
const WHITE = 0;
|
|
119
|
+
const GRAY = 1;
|
|
120
|
+
const BLACK = 2;
|
|
121
|
+
const color = new Map;
|
|
122
|
+
for (const id of stepIndex.keys()) {
|
|
123
|
+
color.set(id, WHITE);
|
|
124
|
+
}
|
|
125
|
+
const cycles = [];
|
|
126
|
+
const path = [];
|
|
127
|
+
function dfs(node) {
|
|
128
|
+
if (!stepIndex.has(node))
|
|
129
|
+
return;
|
|
130
|
+
color.set(node, GRAY);
|
|
131
|
+
path.push(node);
|
|
132
|
+
const succs = successors.get(node);
|
|
133
|
+
if (succs) {
|
|
134
|
+
for (const next of succs) {
|
|
135
|
+
if (!stepIndex.has(next))
|
|
136
|
+
continue;
|
|
137
|
+
const c = color.get(next);
|
|
138
|
+
if (c === undefined)
|
|
139
|
+
continue;
|
|
140
|
+
if (c === GRAY) {
|
|
141
|
+
const cycleStart = path.indexOf(next);
|
|
142
|
+
cycles.push(path.slice(cycleStart));
|
|
143
|
+
} else if (c === WHITE) {
|
|
144
|
+
dfs(next);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
path.pop();
|
|
149
|
+
color.set(node, BLACK);
|
|
150
|
+
}
|
|
151
|
+
for (const id of stepIndex.keys()) {
|
|
152
|
+
if (color.get(id) === WHITE) {
|
|
153
|
+
dfs(id);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return cycles;
|
|
157
|
+
}
|
|
158
|
+
function computePredecessors(topologicalOrder, successors) {
|
|
159
|
+
const predecessors = new Map;
|
|
160
|
+
for (const id of topologicalOrder) {
|
|
161
|
+
predecessors.set(id, new Set);
|
|
162
|
+
}
|
|
163
|
+
for (const id of topologicalOrder) {
|
|
164
|
+
const succs = successors.get(id);
|
|
165
|
+
if (!succs)
|
|
166
|
+
continue;
|
|
167
|
+
for (const succ of succs) {
|
|
168
|
+
const predSet = predecessors.get(succ);
|
|
169
|
+
if (!predSet)
|
|
170
|
+
continue;
|
|
171
|
+
predSet.add(id);
|
|
172
|
+
const idPreds = predecessors.get(id);
|
|
173
|
+
if (idPreds) {
|
|
174
|
+
for (const p of idPreds) {
|
|
175
|
+
predSet.add(p);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return predecessors;
|
|
181
|
+
}
|
|
182
|
+
function topologicalSort(stepIds, successors) {
|
|
183
|
+
const inDegree = new Map;
|
|
184
|
+
const stepIdSet = new Set(stepIds);
|
|
185
|
+
for (const id of stepIds) {
|
|
186
|
+
inDegree.set(id, 0);
|
|
187
|
+
}
|
|
188
|
+
for (const id of stepIds) {
|
|
189
|
+
const succs = successors.get(id);
|
|
190
|
+
if (!succs)
|
|
191
|
+
continue;
|
|
192
|
+
for (const s of succs) {
|
|
193
|
+
if (stepIdSet.has(s)) {
|
|
194
|
+
inDegree.set(s, (inDegree.get(s) ?? 0) + 1);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
const queue = [];
|
|
199
|
+
for (const [id, deg] of inDegree) {
|
|
200
|
+
if (deg === 0)
|
|
201
|
+
queue.push(id);
|
|
202
|
+
}
|
|
203
|
+
const order = [];
|
|
204
|
+
while (queue.length > 0) {
|
|
205
|
+
const node = queue.shift();
|
|
206
|
+
if (node === undefined)
|
|
207
|
+
break;
|
|
208
|
+
order.push(node);
|
|
209
|
+
const succs = successors.get(node);
|
|
210
|
+
if (!succs)
|
|
211
|
+
continue;
|
|
212
|
+
for (const s of succs) {
|
|
213
|
+
if (!stepIdSet.has(s))
|
|
214
|
+
continue;
|
|
215
|
+
const currentDeg = inDegree.get(s);
|
|
216
|
+
if (currentDeg === undefined)
|
|
217
|
+
continue;
|
|
218
|
+
const newDeg = currentDeg - 1;
|
|
219
|
+
inDegree.set(s, newDeg);
|
|
220
|
+
if (newDeg === 0) {
|
|
221
|
+
queue.push(s);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return order.length === stepIds.length ? order : null;
|
|
226
|
+
}
|
|
227
|
+
function computeLoopScopesAndOwnership(initialStepId, stepIndex) {
|
|
228
|
+
const loopVariablesInScope = new Map;
|
|
229
|
+
const bodyOwnership = new Map;
|
|
230
|
+
function walkChain(startId, loopVars, ownerStepId) {
|
|
231
|
+
let currentId = startId;
|
|
232
|
+
const chainSteps = [];
|
|
233
|
+
while (currentId) {
|
|
234
|
+
if (loopVariablesInScope.has(currentId))
|
|
235
|
+
break;
|
|
236
|
+
const step = stepIndex.get(currentId);
|
|
237
|
+
if (!step)
|
|
238
|
+
break;
|
|
239
|
+
loopVariablesInScope.set(currentId, new Set(loopVars));
|
|
240
|
+
if (ownerStepId !== null) {
|
|
241
|
+
bodyOwnership.set(currentId, ownerStepId);
|
|
242
|
+
}
|
|
243
|
+
chainSteps.push(step);
|
|
244
|
+
currentId = step.nextStepId;
|
|
245
|
+
}
|
|
246
|
+
for (const step of chainSteps) {
|
|
247
|
+
if (step.type === "for-each") {
|
|
248
|
+
const innerVars = new Set(loopVars);
|
|
249
|
+
innerVars.add(step.params.itemName);
|
|
250
|
+
walkChain(step.params.loopBodyStepId, innerVars, step.id);
|
|
251
|
+
}
|
|
252
|
+
if (step.type === "switch-case") {
|
|
253
|
+
for (const c of step.params.cases) {
|
|
254
|
+
walkChain(c.branchBodyStepId, loopVars, step.id);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
walkChain(initialStepId, new Set, null);
|
|
260
|
+
return { loopVariablesInScope, bodyOwnership };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// src/compiler/passes/build-graph.ts
|
|
264
|
+
function buildGraph(workflow) {
|
|
265
|
+
const diagnostics = [];
|
|
266
|
+
const VALID_STEP_ID = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
267
|
+
for (const step of workflow.steps) {
|
|
268
|
+
if (!VALID_STEP_ID.test(step.id)) {
|
|
269
|
+
diagnostics.push({
|
|
270
|
+
severity: "error",
|
|
271
|
+
location: { stepId: step.id, field: "id" },
|
|
272
|
+
message: `Step ID '${step.id}' is invalid — must match [a-zA-Z_][a-zA-Z0-9_]* (use underscores, not hyphens)`,
|
|
273
|
+
code: "INVALID_STEP_ID"
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
const stepIdSet = new Set(workflow.steps.map((s) => s.id));
|
|
278
|
+
for (const step of workflow.steps) {
|
|
279
|
+
if (step.type === "for-each") {
|
|
280
|
+
const { itemName } = step.params;
|
|
281
|
+
if (!VALID_STEP_ID.test(itemName)) {
|
|
282
|
+
diagnostics.push({
|
|
283
|
+
severity: "error",
|
|
284
|
+
location: { stepId: step.id, field: "params.itemName" },
|
|
285
|
+
message: `Item name '${itemName}' is invalid — must match [a-zA-Z_][a-zA-Z0-9_]*`,
|
|
286
|
+
code: "INVALID_ITEM_NAME"
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
if (stepIdSet.has(itemName)) {
|
|
290
|
+
diagnostics.push({
|
|
291
|
+
severity: "warning",
|
|
292
|
+
location: { stepId: step.id, field: "params.itemName" },
|
|
293
|
+
message: `Item name '${itemName}' shadows step ID '${itemName}' — references to '${itemName}' inside the loop body will resolve to the loop variable, not the step output`,
|
|
294
|
+
code: "ITEM_NAME_SHADOWS_STEP_ID"
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
const { index: stepIndex, duplicates } = buildStepIndex(workflow.steps);
|
|
300
|
+
for (const dupId of duplicates) {
|
|
301
|
+
diagnostics.push({
|
|
302
|
+
severity: "error",
|
|
303
|
+
location: { stepId: dupId, field: "id" },
|
|
304
|
+
message: `Duplicate step ID '${dupId}'`,
|
|
305
|
+
code: "DUPLICATE_STEP_ID"
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
if (!stepIndex.has(workflow.initialStepId)) {
|
|
309
|
+
diagnostics.push({
|
|
310
|
+
severity: "error",
|
|
311
|
+
location: { stepId: null, field: "initialStepId" },
|
|
312
|
+
message: `Initial step '${workflow.initialStepId}' does not exist`,
|
|
313
|
+
code: "MISSING_INITIAL_STEP"
|
|
314
|
+
});
|
|
315
|
+
return { graph: null, diagnostics };
|
|
316
|
+
}
|
|
317
|
+
if (duplicates.length > 0) {
|
|
318
|
+
return { graph: null, diagnostics };
|
|
319
|
+
}
|
|
320
|
+
const successors = computeSuccessors(stepIndex);
|
|
321
|
+
const cycles = detectCycles(stepIndex, successors);
|
|
322
|
+
for (const cycle of cycles) {
|
|
323
|
+
const firstStep = cycle[0];
|
|
324
|
+
if (!firstStep)
|
|
325
|
+
continue;
|
|
326
|
+
diagnostics.push({
|
|
327
|
+
severity: "error",
|
|
328
|
+
location: { stepId: firstStep, field: "nextStepId" },
|
|
329
|
+
message: `Cycle detected: ${cycle.join(" → ")} → ${firstStep}`,
|
|
330
|
+
code: "CYCLE_DETECTED"
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
const reachableSteps = computeReachability(workflow.initialStepId, successors);
|
|
334
|
+
for (const [id] of stepIndex) {
|
|
335
|
+
if (!reachableSteps.has(id)) {
|
|
336
|
+
diagnostics.push({
|
|
337
|
+
severity: "warning",
|
|
338
|
+
location: { stepId: id, field: "id" },
|
|
339
|
+
message: `Step '${id}' is not reachable from initial step '${workflow.initialStepId}'`,
|
|
340
|
+
code: "UNREACHABLE_STEP"
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
if (cycles.length > 0) {
|
|
345
|
+
return { graph: null, diagnostics };
|
|
346
|
+
}
|
|
347
|
+
const reachableIds = [...reachableSteps];
|
|
348
|
+
const topologicalOrder = topologicalSort(reachableIds, successors);
|
|
349
|
+
if (!topologicalOrder) {
|
|
350
|
+
return { graph: null, diagnostics };
|
|
351
|
+
}
|
|
352
|
+
const predecessors = computePredecessors(topologicalOrder, successors);
|
|
353
|
+
const { loopVariablesInScope, bodyOwnership } = computeLoopScopesAndOwnership(workflow.initialStepId, stepIndex);
|
|
354
|
+
return {
|
|
355
|
+
graph: {
|
|
356
|
+
stepIndex,
|
|
357
|
+
successors,
|
|
358
|
+
predecessors,
|
|
359
|
+
topologicalOrder,
|
|
360
|
+
reachableSteps,
|
|
361
|
+
loopVariablesInScope,
|
|
362
|
+
bodyOwnership
|
|
363
|
+
},
|
|
364
|
+
diagnostics
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// src/compiler/passes/generate-constrained-tool-schemas.ts
|
|
369
|
+
function collectCallSites(workflow) {
|
|
370
|
+
const callSitesByTool = new Map;
|
|
371
|
+
for (const step of workflow.steps) {
|
|
372
|
+
if (step.type !== "tool-call")
|
|
373
|
+
continue;
|
|
374
|
+
const { toolName, toolInput } = step.params;
|
|
375
|
+
const keys = new Map;
|
|
376
|
+
for (const [key, expr] of Object.entries(toolInput)) {
|
|
377
|
+
const expression = expr;
|
|
378
|
+
if (expression.type === "literal") {
|
|
379
|
+
keys.set(key, { type: "literal", value: expression.value });
|
|
380
|
+
} else {
|
|
381
|
+
keys.set(key, { type: "jmespath" });
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
let sites = callSitesByTool.get(toolName);
|
|
385
|
+
if (!sites) {
|
|
386
|
+
sites = [];
|
|
387
|
+
callSitesByTool.set(toolName, sites);
|
|
388
|
+
}
|
|
389
|
+
sites.push({ stepId: step.id, keys });
|
|
390
|
+
}
|
|
391
|
+
return callSitesByTool;
|
|
392
|
+
}
|
|
393
|
+
function constrainProperty(callSites, key, originalPropertySchema) {
|
|
394
|
+
let providedCount = 0;
|
|
395
|
+
let allLiteral = true;
|
|
396
|
+
const literalValues = [];
|
|
397
|
+
const seen = new Set;
|
|
398
|
+
for (const site of callSites) {
|
|
399
|
+
const entry = site.keys.get(key);
|
|
400
|
+
if (!entry)
|
|
401
|
+
continue;
|
|
402
|
+
providedCount++;
|
|
403
|
+
if (entry.type === "jmespath") {
|
|
404
|
+
allLiteral = false;
|
|
405
|
+
} else {
|
|
406
|
+
const serialized = JSON.stringify(entry.value);
|
|
407
|
+
if (!seen.has(serialized)) {
|
|
408
|
+
seen.add(serialized);
|
|
409
|
+
literalValues.push(entry.value);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
const providedInAll = providedCount === callSites.length;
|
|
414
|
+
if (!allLiteral) {
|
|
415
|
+
return { schema: originalPropertySchema, allLiteral: false, providedInAll };
|
|
416
|
+
}
|
|
417
|
+
const original = originalPropertySchema && typeof originalPropertySchema === "object" ? originalPropertySchema : {};
|
|
418
|
+
if (literalValues.length === 1) {
|
|
419
|
+
return {
|
|
420
|
+
schema: { ...original, const: literalValues[0] },
|
|
421
|
+
allLiteral: true,
|
|
422
|
+
providedInAll
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
return {
|
|
426
|
+
schema: { ...original, enum: literalValues },
|
|
427
|
+
allLiteral: true,
|
|
428
|
+
providedInAll
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
function generateConstrainedToolSchemas(workflow, tools) {
|
|
432
|
+
const callSitesByTool = collectCallSites(workflow);
|
|
433
|
+
const result = {};
|
|
434
|
+
for (const [toolName, callSites] of callSitesByTool) {
|
|
435
|
+
const toolDef = tools[toolName];
|
|
436
|
+
if (!toolDef)
|
|
437
|
+
continue;
|
|
438
|
+
const originalProperties = toolDef.inputSchema.properties ?? {};
|
|
439
|
+
const allUsedKeys = new Set;
|
|
440
|
+
for (const site of callSites) {
|
|
441
|
+
for (const key of site.keys.keys()) {
|
|
442
|
+
allUsedKeys.add(key);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
const constrainedProperties = {};
|
|
446
|
+
const requiredKeys = [];
|
|
447
|
+
let fullyStatic = true;
|
|
448
|
+
for (const key of allUsedKeys) {
|
|
449
|
+
const originalProp = originalProperties[key];
|
|
450
|
+
if (originalProp === undefined)
|
|
451
|
+
continue;
|
|
452
|
+
const { schema, allLiteral, providedInAll } = constrainProperty(callSites, key, originalProp);
|
|
453
|
+
constrainedProperties[key] = schema;
|
|
454
|
+
if (providedInAll) {
|
|
455
|
+
requiredKeys.push(key);
|
|
456
|
+
}
|
|
457
|
+
if (!allLiteral) {
|
|
458
|
+
fullyStatic = false;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
const constrained = {
|
|
462
|
+
inputSchema: {
|
|
463
|
+
required: requiredKeys.sort(),
|
|
464
|
+
properties: constrainedProperties
|
|
465
|
+
},
|
|
466
|
+
fullyStatic,
|
|
467
|
+
callSites: callSites.map((s) => s.stepId).sort()
|
|
468
|
+
};
|
|
469
|
+
if (toolDef.outputSchema) {
|
|
470
|
+
constrained.outputSchema = toolDef.outputSchema;
|
|
471
|
+
}
|
|
472
|
+
result[toolName] = constrained;
|
|
473
|
+
}
|
|
474
|
+
return result;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// src/compiler/passes/validate-control-flow.ts
|
|
478
|
+
function validateControlFlow(workflow, graph) {
|
|
479
|
+
const diagnostics = [];
|
|
480
|
+
for (const step of workflow.steps) {
|
|
481
|
+
if (step.type === "end" && step.nextStepId) {
|
|
482
|
+
diagnostics.push({
|
|
483
|
+
severity: "error",
|
|
484
|
+
location: { stepId: step.id, field: "nextStepId" },
|
|
485
|
+
message: `End step '${step.id}' should not have a nextStepId`,
|
|
486
|
+
code: "END_STEP_HAS_NEXT"
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
if (step.type === "switch-case") {
|
|
490
|
+
const defaultCount = step.params.cases.filter((c) => c.value.type === "default").length;
|
|
491
|
+
if (defaultCount > 1) {
|
|
492
|
+
diagnostics.push({
|
|
493
|
+
severity: "error",
|
|
494
|
+
location: { stepId: step.id, field: "params.cases" },
|
|
495
|
+
message: `Switch-case step '${step.id}' has ${defaultCount} default cases (expected at most 1)`,
|
|
496
|
+
code: "MULTIPLE_DEFAULT_CASES"
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
for (const [i, c] of step.params.cases.entries()) {
|
|
500
|
+
const escapes = checkBodyEscapes(c.branchBodyStepId, step.id, graph);
|
|
501
|
+
if (escapes) {
|
|
502
|
+
diagnostics.push({
|
|
503
|
+
severity: "warning",
|
|
504
|
+
location: {
|
|
505
|
+
stepId: step.id,
|
|
506
|
+
field: `params.cases[${i}].branchBodyStepId`
|
|
507
|
+
},
|
|
508
|
+
message: `Branch body starting at '${c.branchBodyStepId}' in step '${step.id}' has a step ('${escapes.escapingStep}') whose nextStepId points outside the branch body to '${escapes.target}'`,
|
|
509
|
+
code: "BRANCH_BODY_ESCAPES"
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
if (step.type === "for-each") {
|
|
515
|
+
const escapes = checkBodyEscapes(step.params.loopBodyStepId, step.id, graph);
|
|
516
|
+
if (escapes) {
|
|
517
|
+
diagnostics.push({
|
|
518
|
+
severity: "warning",
|
|
519
|
+
location: {
|
|
520
|
+
stepId: step.id,
|
|
521
|
+
field: "params.loopBodyStepId"
|
|
522
|
+
},
|
|
523
|
+
message: `Loop body starting at '${step.params.loopBodyStepId}' in step '${step.id}' has a step ('${escapes.escapingStep}') whose nextStepId points outside the loop body to '${escapes.target}'`,
|
|
524
|
+
code: "LOOP_BODY_ESCAPES"
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
if (workflow.outputSchema) {
|
|
530
|
+
for (const step of workflow.steps) {
|
|
531
|
+
if (step.type === "end" && !step.params?.output) {
|
|
532
|
+
diagnostics.push({
|
|
533
|
+
severity: "warning",
|
|
534
|
+
location: { stepId: step.id, field: "params" },
|
|
535
|
+
message: `End step '${step.id}' has no output expression, but the workflow declares an outputSchema`,
|
|
536
|
+
code: "END_STEP_MISSING_OUTPUT"
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
} else {
|
|
541
|
+
for (const step of workflow.steps) {
|
|
542
|
+
if (step.type === "end" && step.params?.output) {
|
|
543
|
+
diagnostics.push({
|
|
544
|
+
severity: "warning",
|
|
545
|
+
location: { stepId: step.id, field: "params.output" },
|
|
546
|
+
message: `End step '${step.id}' has an output expression, but the workflow does not declare an outputSchema`,
|
|
547
|
+
code: "END_STEP_UNEXPECTED_OUTPUT"
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
return diagnostics;
|
|
553
|
+
}
|
|
554
|
+
function checkBodyEscapes(startId, parentStepId, graph) {
|
|
555
|
+
let currentId = startId;
|
|
556
|
+
const visited = new Set;
|
|
557
|
+
while (currentId) {
|
|
558
|
+
if (visited.has(currentId))
|
|
559
|
+
break;
|
|
560
|
+
visited.add(currentId);
|
|
561
|
+
const step = graph.stepIndex.get(currentId);
|
|
562
|
+
if (!step)
|
|
563
|
+
break;
|
|
564
|
+
if (step.nextStepId) {
|
|
565
|
+
const nextOwner = graph.bodyOwnership.get(step.nextStepId);
|
|
566
|
+
const currentOwner = graph.bodyOwnership.get(currentId);
|
|
567
|
+
if ((currentOwner === parentStepId || currentId === startId) && nextOwner !== parentStepId) {
|
|
568
|
+
return { escapingStep: currentId, target: step.nextStepId };
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
currentId = step.nextStepId;
|
|
572
|
+
}
|
|
573
|
+
return null;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// src/compiler/passes/validate-foreach-target.ts
|
|
577
|
+
function validateForeachTarget(workflow, graph, tools) {
|
|
578
|
+
const diagnostics = [];
|
|
579
|
+
for (const step of workflow.steps) {
|
|
580
|
+
if (step.type !== "for-each")
|
|
581
|
+
continue;
|
|
582
|
+
if (step.params.target.type !== "jmespath")
|
|
583
|
+
continue;
|
|
584
|
+
const expression = step.params.target.expression;
|
|
585
|
+
const segments = parseSimplePath(expression);
|
|
586
|
+
if (!segments)
|
|
587
|
+
continue;
|
|
588
|
+
const [rootId, ...fieldPath] = segments;
|
|
589
|
+
if (!rootId)
|
|
590
|
+
continue;
|
|
591
|
+
const referencedStep = graph.stepIndex.get(rootId);
|
|
592
|
+
if (!referencedStep)
|
|
593
|
+
continue;
|
|
594
|
+
const outputSchema = getStepOutputSchema(referencedStep, tools);
|
|
595
|
+
if (!outputSchema)
|
|
596
|
+
continue;
|
|
597
|
+
const resolvedSchema = resolvePath(outputSchema, fieldPath);
|
|
598
|
+
if (!resolvedSchema)
|
|
599
|
+
continue;
|
|
600
|
+
const resolvedType = getSchemaType(resolvedSchema);
|
|
601
|
+
if (resolvedType === "array")
|
|
602
|
+
continue;
|
|
603
|
+
if (resolvedType === "object") {
|
|
604
|
+
const suggestions = findArrayProperties(resolvedSchema);
|
|
605
|
+
const hint = suggestions.length > 0 ? ` Did you mean ${suggestions.map((s) => `'${rootId}.${s}'`).join(" or ")}?` : "";
|
|
606
|
+
diagnostics.push({
|
|
607
|
+
severity: "error",
|
|
608
|
+
location: { stepId: step.id, field: "params.target" },
|
|
609
|
+
message: `for-each target '${expression}' resolves to an object, not an array.${hint}`,
|
|
610
|
+
code: "FOREACH_TARGET_NOT_ARRAY"
|
|
611
|
+
});
|
|
612
|
+
} else if (resolvedType !== null) {
|
|
613
|
+
diagnostics.push({
|
|
614
|
+
severity: "error",
|
|
615
|
+
location: { stepId: step.id, field: "params.target" },
|
|
616
|
+
message: `for-each target '${expression}' resolves to type '${resolvedType}', not an array.`,
|
|
617
|
+
code: "FOREACH_TARGET_NOT_ARRAY"
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
return diagnostics;
|
|
622
|
+
}
|
|
623
|
+
function parseSimplePath(expression) {
|
|
624
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)*$/.test(expression)) {
|
|
625
|
+
return null;
|
|
626
|
+
}
|
|
627
|
+
return expression.split(".");
|
|
628
|
+
}
|
|
629
|
+
function getStepOutputSchema(step, tools) {
|
|
630
|
+
if (step.type === "tool-call") {
|
|
631
|
+
const toolDef = tools[step.params.toolName];
|
|
632
|
+
return toolDef?.outputSchema ?? null;
|
|
633
|
+
}
|
|
634
|
+
if (step.type === "start" && step.params.inputSchema) {
|
|
635
|
+
return step.params.inputSchema;
|
|
636
|
+
}
|
|
637
|
+
return null;
|
|
638
|
+
}
|
|
639
|
+
function resolvePath(schema, path) {
|
|
640
|
+
let current = schema;
|
|
641
|
+
for (const segment of path) {
|
|
642
|
+
const properties = current.properties;
|
|
643
|
+
if (!properties?.[segment])
|
|
644
|
+
return null;
|
|
645
|
+
current = properties[segment];
|
|
646
|
+
}
|
|
647
|
+
return current;
|
|
648
|
+
}
|
|
649
|
+
function getSchemaType(schema) {
|
|
650
|
+
if (typeof schema.type === "string")
|
|
651
|
+
return schema.type;
|
|
652
|
+
return null;
|
|
653
|
+
}
|
|
654
|
+
function findArrayProperties(schema) {
|
|
655
|
+
const properties = schema.properties;
|
|
656
|
+
if (!properties)
|
|
657
|
+
return [];
|
|
658
|
+
return Object.entries(properties).filter(([_, propSchema]) => propSchema.type === "array").map(([name]) => name);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// src/compiler/utils/jmespath-helpers.ts
|
|
662
|
+
import { compile } from "@jmespath-community/jmespath";
|
|
663
|
+
function validateJmespathSyntax(expression) {
|
|
664
|
+
try {
|
|
665
|
+
compile(expression);
|
|
666
|
+
return { valid: true };
|
|
667
|
+
} catch (e) {
|
|
668
|
+
return { valid: false, error: e.message };
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
function extractRootIdentifiers(expression) {
|
|
672
|
+
let ast;
|
|
673
|
+
try {
|
|
674
|
+
ast = compile(expression);
|
|
675
|
+
} catch {
|
|
676
|
+
return [];
|
|
677
|
+
}
|
|
678
|
+
const roots = new Set;
|
|
679
|
+
collectRoots(ast, roots);
|
|
680
|
+
return [...roots];
|
|
681
|
+
}
|
|
682
|
+
function collectRoots(node, roots) {
|
|
683
|
+
if (!node || typeof node !== "object")
|
|
684
|
+
return;
|
|
685
|
+
switch (node.type) {
|
|
686
|
+
case "Field":
|
|
687
|
+
if (node.name)
|
|
688
|
+
roots.add(node.name);
|
|
689
|
+
break;
|
|
690
|
+
case "Subexpression":
|
|
691
|
+
if (node.left)
|
|
692
|
+
collectLeftmostRoot(node.left, roots);
|
|
693
|
+
break;
|
|
694
|
+
case "FilterProjection":
|
|
695
|
+
if (node.left)
|
|
696
|
+
collectLeftmostRoot(node.left, roots);
|
|
697
|
+
break;
|
|
698
|
+
case "Projection":
|
|
699
|
+
case "Flatten":
|
|
700
|
+
if (node.left)
|
|
701
|
+
collectLeftmostRoot(node.left, roots);
|
|
702
|
+
break;
|
|
703
|
+
case "Function":
|
|
704
|
+
if (node.children) {
|
|
705
|
+
for (const child of node.children) {
|
|
706
|
+
collectRoots(child, roots);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
break;
|
|
710
|
+
case "MultiSelectList":
|
|
711
|
+
case "MultiSelectHash":
|
|
712
|
+
if (node.children) {
|
|
713
|
+
for (const child of node.children) {
|
|
714
|
+
collectRoots(child, roots);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
break;
|
|
718
|
+
case "Comparator":
|
|
719
|
+
case "And":
|
|
720
|
+
case "Or":
|
|
721
|
+
case "Arithmetic":
|
|
722
|
+
if (node.left)
|
|
723
|
+
collectRoots(node.left, roots);
|
|
724
|
+
if (node.right)
|
|
725
|
+
collectRoots(node.right, roots);
|
|
726
|
+
break;
|
|
727
|
+
case "Not":
|
|
728
|
+
case "Negate":
|
|
729
|
+
if (node.children?.[0])
|
|
730
|
+
collectRoots(node.children[0], roots);
|
|
731
|
+
break;
|
|
732
|
+
case "Pipe":
|
|
733
|
+
if (node.left)
|
|
734
|
+
collectRoots(node.left, roots);
|
|
735
|
+
break;
|
|
736
|
+
case "IndexExpression":
|
|
737
|
+
if (node.left)
|
|
738
|
+
collectLeftmostRoot(node.left, roots);
|
|
739
|
+
break;
|
|
740
|
+
case "ValueProjection":
|
|
741
|
+
case "Slice":
|
|
742
|
+
if (node.left)
|
|
743
|
+
collectLeftmostRoot(node.left, roots);
|
|
744
|
+
break;
|
|
745
|
+
case "Literal":
|
|
746
|
+
case "Current":
|
|
747
|
+
case "Identity":
|
|
748
|
+
case "Expref":
|
|
749
|
+
break;
|
|
750
|
+
case "KeyValuePair": {
|
|
751
|
+
const kvValue = node.value;
|
|
752
|
+
if (kvValue) {
|
|
753
|
+
collectRoots(kvValue, roots);
|
|
754
|
+
}
|
|
755
|
+
break;
|
|
756
|
+
}
|
|
757
|
+
default:
|
|
758
|
+
if (node.left)
|
|
759
|
+
collectRoots(node.left, roots);
|
|
760
|
+
if (node.right)
|
|
761
|
+
collectRoots(node.right, roots);
|
|
762
|
+
if (node.children) {
|
|
763
|
+
for (const child of node.children) {
|
|
764
|
+
collectRoots(child, roots);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
break;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
function collectLeftmostRoot(node, roots) {
|
|
771
|
+
if (!node)
|
|
772
|
+
return;
|
|
773
|
+
switch (node.type) {
|
|
774
|
+
case "Field":
|
|
775
|
+
if (node.name)
|
|
776
|
+
roots.add(node.name);
|
|
777
|
+
break;
|
|
778
|
+
case "Subexpression":
|
|
779
|
+
if (node.left)
|
|
780
|
+
collectLeftmostRoot(node.left, roots);
|
|
781
|
+
break;
|
|
782
|
+
case "FilterProjection":
|
|
783
|
+
case "Projection":
|
|
784
|
+
case "Flatten":
|
|
785
|
+
case "IndexExpression":
|
|
786
|
+
case "ValueProjection":
|
|
787
|
+
case "Slice":
|
|
788
|
+
if (node.left)
|
|
789
|
+
collectLeftmostRoot(node.left, roots);
|
|
790
|
+
break;
|
|
791
|
+
default:
|
|
792
|
+
collectRoots(node, roots);
|
|
793
|
+
break;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
function extractTemplateExpressions(template) {
|
|
797
|
+
const expressions = [];
|
|
798
|
+
const unclosed = [];
|
|
799
|
+
let i = 0;
|
|
800
|
+
while (i < template.length) {
|
|
801
|
+
if (template[i] === "$" && template[i + 1] === "{") {
|
|
802
|
+
const start = i;
|
|
803
|
+
i += 2;
|
|
804
|
+
let depth = 1;
|
|
805
|
+
const exprStart = i;
|
|
806
|
+
while (i < template.length && depth > 0) {
|
|
807
|
+
if (template[i] === "{")
|
|
808
|
+
depth++;
|
|
809
|
+
else if (template[i] === "}")
|
|
810
|
+
depth--;
|
|
811
|
+
if (depth > 0)
|
|
812
|
+
i++;
|
|
813
|
+
}
|
|
814
|
+
if (depth === 0) {
|
|
815
|
+
const expression = template.slice(exprStart, i);
|
|
816
|
+
expressions.push({ expression, start, end: i + 1 });
|
|
817
|
+
i++;
|
|
818
|
+
} else {
|
|
819
|
+
unclosed.push(start);
|
|
820
|
+
}
|
|
821
|
+
} else {
|
|
822
|
+
i++;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
return { expressions, unclosed };
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// src/compiler/passes/validate-jmespath.ts
|
|
829
|
+
function validateJmespath(workflow, graph) {
|
|
830
|
+
const diagnostics = [];
|
|
831
|
+
const { expressions, templateDiagnostics } = collectExpressions(workflow);
|
|
832
|
+
diagnostics.push(...templateDiagnostics);
|
|
833
|
+
for (const expr of expressions) {
|
|
834
|
+
const syntaxResult = validateJmespathSyntax(expr.expression);
|
|
835
|
+
if (!syntaxResult.valid) {
|
|
836
|
+
diagnostics.push({
|
|
837
|
+
severity: "error",
|
|
838
|
+
location: { stepId: expr.stepId, field: expr.field },
|
|
839
|
+
message: `Invalid JMESPath syntax in '${expr.expression}': ${syntaxResult.error}`,
|
|
840
|
+
code: "JMESPATH_SYNTAX_ERROR"
|
|
841
|
+
});
|
|
842
|
+
continue;
|
|
843
|
+
}
|
|
844
|
+
validateExpressionScope(expr, graph, diagnostics);
|
|
845
|
+
}
|
|
846
|
+
return diagnostics;
|
|
847
|
+
}
|
|
848
|
+
function collectExpressions(workflow) {
|
|
849
|
+
const expressions = [];
|
|
850
|
+
const templateDiagnostics = [];
|
|
851
|
+
for (const step of workflow.steps) {
|
|
852
|
+
switch (step.type) {
|
|
853
|
+
case "tool-call":
|
|
854
|
+
for (const [key, val] of Object.entries(step.params.toolInput)) {
|
|
855
|
+
if (val.type === "jmespath") {
|
|
856
|
+
expressions.push({
|
|
857
|
+
expression: val.expression,
|
|
858
|
+
stepId: step.id,
|
|
859
|
+
field: `params.toolInput.${key}.expression`
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
break;
|
|
864
|
+
case "switch-case":
|
|
865
|
+
if (step.params.switchOn.type === "jmespath") {
|
|
866
|
+
expressions.push({
|
|
867
|
+
expression: step.params.switchOn.expression,
|
|
868
|
+
stepId: step.id,
|
|
869
|
+
field: "params.switchOn.expression"
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
for (const [i, c] of step.params.cases.entries()) {
|
|
873
|
+
if (c.value.type === "jmespath") {
|
|
874
|
+
expressions.push({
|
|
875
|
+
expression: c.value.expression,
|
|
876
|
+
stepId: step.id,
|
|
877
|
+
field: `params.cases[${i}].value.expression`
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
break;
|
|
882
|
+
case "for-each":
|
|
883
|
+
if (step.params.target.type === "jmespath") {
|
|
884
|
+
expressions.push({
|
|
885
|
+
expression: step.params.target.expression,
|
|
886
|
+
stepId: step.id,
|
|
887
|
+
field: "params.target.expression"
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
break;
|
|
891
|
+
case "extract-data":
|
|
892
|
+
if (step.params.sourceData.type === "jmespath") {
|
|
893
|
+
expressions.push({
|
|
894
|
+
expression: step.params.sourceData.expression,
|
|
895
|
+
stepId: step.id,
|
|
896
|
+
field: "params.sourceData.expression"
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
break;
|
|
900
|
+
case "start":
|
|
901
|
+
break;
|
|
902
|
+
case "end":
|
|
903
|
+
if (step.params?.output && step.params.output.type === "jmespath") {
|
|
904
|
+
expressions.push({
|
|
905
|
+
expression: step.params.output.expression,
|
|
906
|
+
stepId: step.id,
|
|
907
|
+
field: "params.output.expression"
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
break;
|
|
911
|
+
case "llm-prompt": {
|
|
912
|
+
const { expressions: templateExprs, unclosed } = extractTemplateExpressions(step.params.prompt);
|
|
913
|
+
for (const te of templateExprs) {
|
|
914
|
+
expressions.push({
|
|
915
|
+
expression: te.expression,
|
|
916
|
+
stepId: step.id,
|
|
917
|
+
field: `params.prompt[${te.start}:${te.end}]`
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
for (const pos of unclosed) {
|
|
921
|
+
templateDiagnostics.push({
|
|
922
|
+
severity: "error",
|
|
923
|
+
location: {
|
|
924
|
+
stepId: step.id,
|
|
925
|
+
field: `params.prompt[${pos}]`
|
|
926
|
+
},
|
|
927
|
+
message: `Unclosed template expression at position ${pos} in prompt`,
|
|
928
|
+
code: "UNCLOSED_TEMPLATE_EXPRESSION"
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
break;
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
return { expressions, templateDiagnostics };
|
|
936
|
+
}
|
|
937
|
+
function validateExpressionScope(expr, graph, diagnostics) {
|
|
938
|
+
const astRoots = extractRootIdentifiers(expr.expression);
|
|
939
|
+
const loopVars = graph.loopVariablesInScope.get(expr.stepId);
|
|
940
|
+
const predecessors = graph.predecessors.get(expr.stepId);
|
|
941
|
+
for (const root of astRoots) {
|
|
942
|
+
if (loopVars?.has(root))
|
|
943
|
+
continue;
|
|
944
|
+
if (graph.stepIndex.has(root)) {
|
|
945
|
+
if (predecessors && !predecessors.has(root)) {
|
|
946
|
+
diagnostics.push({
|
|
947
|
+
severity: "warning",
|
|
948
|
+
location: { stepId: expr.stepId, field: expr.field },
|
|
949
|
+
message: `Expression '${expr.expression}' references step '${root}' which may not have executed before step '${expr.stepId}'`,
|
|
950
|
+
code: "JMESPATH_FORWARD_REFERENCE"
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
continue;
|
|
954
|
+
}
|
|
955
|
+
diagnostics.push({
|
|
956
|
+
severity: "error",
|
|
957
|
+
location: { stepId: expr.stepId, field: expr.field },
|
|
958
|
+
message: `Expression '${expr.expression}' references '${root}' which is not a known step ID or loop variable in scope`,
|
|
959
|
+
code: "JMESPATH_INVALID_ROOT_REFERENCE"
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
// src/compiler/passes/validate-references.ts
|
|
965
|
+
function validateReferences(workflow) {
|
|
966
|
+
const diagnostics = [];
|
|
967
|
+
const stepIds = new Set(workflow.steps.map((s) => s.id));
|
|
968
|
+
if (!stepIds.has(workflow.initialStepId)) {
|
|
969
|
+
diagnostics.push({
|
|
970
|
+
severity: "error",
|
|
971
|
+
location: { stepId: null, field: "initialStepId" },
|
|
972
|
+
message: `Initial step '${workflow.initialStepId}' does not exist`,
|
|
973
|
+
code: "MISSING_INITIAL_STEP"
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
for (const step of workflow.steps) {
|
|
977
|
+
if (step.nextStepId && !stepIds.has(step.nextStepId)) {
|
|
978
|
+
diagnostics.push({
|
|
979
|
+
severity: "error",
|
|
980
|
+
location: { stepId: step.id, field: "nextStepId" },
|
|
981
|
+
message: `Step '${step.id}' references non-existent next step '${step.nextStepId}'`,
|
|
982
|
+
code: "MISSING_NEXT_STEP"
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
if (step.type === "switch-case") {
|
|
986
|
+
for (const [i, c] of step.params.cases.entries()) {
|
|
987
|
+
if (!stepIds.has(c.branchBodyStepId)) {
|
|
988
|
+
diagnostics.push({
|
|
989
|
+
severity: "error",
|
|
990
|
+
location: {
|
|
991
|
+
stepId: step.id,
|
|
992
|
+
field: `params.cases[${i}].branchBodyStepId`
|
|
993
|
+
},
|
|
994
|
+
message: `Step '${step.id}' case ${i} references non-existent branch body step '${c.branchBodyStepId}'`,
|
|
995
|
+
code: "MISSING_BRANCH_BODY_STEP"
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
if (step.type === "for-each") {
|
|
1001
|
+
if (!stepIds.has(step.params.loopBodyStepId)) {
|
|
1002
|
+
diagnostics.push({
|
|
1003
|
+
severity: "error",
|
|
1004
|
+
location: {
|
|
1005
|
+
stepId: step.id,
|
|
1006
|
+
field: "params.loopBodyStepId"
|
|
1007
|
+
},
|
|
1008
|
+
message: `Step '${step.id}' references non-existent loop body step '${step.params.loopBodyStepId}'`,
|
|
1009
|
+
code: "MISSING_LOOP_BODY_STEP"
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
return diagnostics;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
// src/compiler/passes/validate-tools.ts
|
|
1018
|
+
function validateTools(workflow, tools) {
|
|
1019
|
+
const diagnostics = [];
|
|
1020
|
+
for (const step of workflow.steps) {
|
|
1021
|
+
if (step.type !== "tool-call")
|
|
1022
|
+
continue;
|
|
1023
|
+
const { toolName, toolInput } = step.params;
|
|
1024
|
+
const toolDef = tools[toolName];
|
|
1025
|
+
if (!toolDef) {
|
|
1026
|
+
diagnostics.push({
|
|
1027
|
+
severity: "error",
|
|
1028
|
+
location: { stepId: step.id, field: "params.toolName" },
|
|
1029
|
+
message: `Step '${step.id}' references unknown tool '${toolName}'`,
|
|
1030
|
+
code: "UNKNOWN_TOOL"
|
|
1031
|
+
});
|
|
1032
|
+
continue;
|
|
1033
|
+
}
|
|
1034
|
+
const schemaProperties = toolDef.inputSchema.properties ?? {};
|
|
1035
|
+
const requiredKeys = new Set(toolDef.inputSchema.required ?? []);
|
|
1036
|
+
const definedKeys = new Set(Object.keys(schemaProperties));
|
|
1037
|
+
const providedKeys = new Set(Object.keys(toolInput));
|
|
1038
|
+
for (const key of providedKeys) {
|
|
1039
|
+
if (!definedKeys.has(key)) {
|
|
1040
|
+
diagnostics.push({
|
|
1041
|
+
severity: "warning",
|
|
1042
|
+
location: {
|
|
1043
|
+
stepId: step.id,
|
|
1044
|
+
field: `params.toolInput.${key}`
|
|
1045
|
+
},
|
|
1046
|
+
message: `Step '${step.id}' provides input key '${key}' which is not defined in tool '${toolName}' schema`,
|
|
1047
|
+
code: "EXTRA_TOOL_INPUT_KEY"
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
for (const key of requiredKeys) {
|
|
1052
|
+
if (!providedKeys.has(key)) {
|
|
1053
|
+
diagnostics.push({
|
|
1054
|
+
severity: "error",
|
|
1055
|
+
location: { stepId: step.id, field: "params.toolInput" },
|
|
1056
|
+
message: `Step '${step.id}' is missing required input key '${key}' for tool '${toolName}'`,
|
|
1057
|
+
code: "MISSING_TOOL_INPUT_KEY"
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
return diagnostics;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
// src/compiler/index.ts
|
|
1066
|
+
async function compileWorkflow(workflow, options) {
|
|
1067
|
+
const diagnostics = [];
|
|
1068
|
+
const graphResult = buildGraph(workflow);
|
|
1069
|
+
diagnostics.push(...graphResult.diagnostics);
|
|
1070
|
+
const refDiagnostics = validateReferences(workflow);
|
|
1071
|
+
for (const d of refDiagnostics) {
|
|
1072
|
+
if (d.code === "MISSING_INITIAL_STEP" && diagnostics.some((e) => e.code === "MISSING_INITIAL_STEP")) {
|
|
1073
|
+
continue;
|
|
1074
|
+
}
|
|
1075
|
+
diagnostics.push(d);
|
|
1076
|
+
}
|
|
1077
|
+
if (graphResult.graph) {
|
|
1078
|
+
diagnostics.push(...validateControlFlow(workflow, graphResult.graph));
|
|
1079
|
+
diagnostics.push(...validateJmespath(workflow, graphResult.graph));
|
|
1080
|
+
}
|
|
1081
|
+
let constrainedToolSchemas = null;
|
|
1082
|
+
if (options?.tools) {
|
|
1083
|
+
const toolSchemas = await extractToolSchemas(options.tools);
|
|
1084
|
+
diagnostics.push(...validateTools(workflow, toolSchemas));
|
|
1085
|
+
constrainedToolSchemas = generateConstrainedToolSchemas(workflow, toolSchemas);
|
|
1086
|
+
if (graphResult.graph) {
|
|
1087
|
+
diagnostics.push(...validateForeachTarget(workflow, graphResult.graph, toolSchemas));
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
const hasErrors = diagnostics.some((d) => d.severity === "error");
|
|
1091
|
+
let optimizedWorkflow = null;
|
|
1092
|
+
if (graphResult.graph && !hasErrors) {
|
|
1093
|
+
const bpResult = applyBestPractices(workflow, graphResult.graph);
|
|
1094
|
+
optimizedWorkflow = bpResult.workflow;
|
|
1095
|
+
diagnostics.push(...bpResult.diagnostics);
|
|
1096
|
+
}
|
|
1097
|
+
return {
|
|
1098
|
+
diagnostics,
|
|
1099
|
+
graph: graphResult.graph,
|
|
1100
|
+
workflow: optimizedWorkflow,
|
|
1101
|
+
constrainedToolSchemas
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1104
|
+
async function extractToolSchemas(tools) {
|
|
1105
|
+
const schemas = {};
|
|
1106
|
+
for (const [name, toolDef] of Object.entries(tools)) {
|
|
1107
|
+
const jsonSchema = await asSchema(toolDef.inputSchema).jsonSchema;
|
|
1108
|
+
schemas[name] = {
|
|
1109
|
+
inputSchema: jsonSchema
|
|
1110
|
+
};
|
|
1111
|
+
if (toolDef.outputSchema) {
|
|
1112
|
+
schemas[name].outputSchema = await asSchema(toolDef.outputSchema).jsonSchema;
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
return schemas;
|
|
1116
|
+
}
|
|
1117
|
+
// src/executor/index.ts
|
|
1118
|
+
import { safeValidateTypes } from "@ai-sdk/provider-utils";
|
|
1119
|
+
import { search } from "@jmespath-community/jmespath";
|
|
1120
|
+
import {
|
|
1121
|
+
APICallError,
|
|
1122
|
+
JSONParseError,
|
|
1123
|
+
NoContentGeneratedError,
|
|
1124
|
+
RetryError,
|
|
1125
|
+
stepCountIs,
|
|
1126
|
+
ToolLoopAgent,
|
|
1127
|
+
TypeValidationError
|
|
1128
|
+
} from "ai";
|
|
1129
|
+
|
|
1130
|
+
// src/executor/errors.ts
|
|
1131
|
+
class StepExecutionError extends Error {
|
|
1132
|
+
stepId;
|
|
1133
|
+
code;
|
|
1134
|
+
category;
|
|
1135
|
+
cause;
|
|
1136
|
+
name = "StepExecutionError";
|
|
1137
|
+
constructor(stepId, code, category, message, cause) {
|
|
1138
|
+
super(message);
|
|
1139
|
+
this.stepId = stepId;
|
|
1140
|
+
this.code = code;
|
|
1141
|
+
this.category = category;
|
|
1142
|
+
this.cause = cause;
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
class ConfigurationError extends StepExecutionError {
|
|
1147
|
+
constructor(stepId, code, message) {
|
|
1148
|
+
super(stepId, code, "configuration", message);
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
class ValidationError extends StepExecutionError {
|
|
1153
|
+
input;
|
|
1154
|
+
constructor(stepId, code, message, input, cause) {
|
|
1155
|
+
super(stepId, code, "validation", message, cause);
|
|
1156
|
+
this.input = input;
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
class ExternalServiceError extends StepExecutionError {
|
|
1161
|
+
statusCode;
|
|
1162
|
+
isRetryable;
|
|
1163
|
+
constructor(stepId, code, message, cause, statusCode, isRetryable = true) {
|
|
1164
|
+
super(stepId, code, "external-service", message, cause);
|
|
1165
|
+
this.statusCode = statusCode;
|
|
1166
|
+
this.isRetryable = isRetryable;
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
class ExpressionError extends StepExecutionError {
|
|
1171
|
+
expression;
|
|
1172
|
+
constructor(stepId, code, message, expression, cause) {
|
|
1173
|
+
super(stepId, code, "expression", message, cause);
|
|
1174
|
+
this.expression = expression;
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
class OutputQualityError extends StepExecutionError {
|
|
1179
|
+
rawOutput;
|
|
1180
|
+
constructor(stepId, code, message, rawOutput, cause) {
|
|
1181
|
+
super(stepId, code, "output-quality", message, cause);
|
|
1182
|
+
this.rawOutput = rawOutput;
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
// src/executor/index.ts
|
|
1187
|
+
function stripCodeFence(text) {
|
|
1188
|
+
const match = text.match(/^```(?:\w*)\s*\n?([\s\S]*?)\n?\s*```\s*$/);
|
|
1189
|
+
return match?.[1] ?? text;
|
|
1190
|
+
}
|
|
1191
|
+
function isAgent(value) {
|
|
1192
|
+
return typeof value === "object" && value !== null && "generate" in value;
|
|
1193
|
+
}
|
|
1194
|
+
function evaluateExpression(expr, scope, stepId) {
|
|
1195
|
+
if (expr.type === "literal") {
|
|
1196
|
+
return expr.value;
|
|
1197
|
+
}
|
|
1198
|
+
try {
|
|
1199
|
+
return search(scope, expr.expression);
|
|
1200
|
+
} catch (e) {
|
|
1201
|
+
throw new ExpressionError(stepId, "JMESPATH_EVALUATION_ERROR", `JMESPath expression '${expr.expression}' failed: ${e instanceof Error ? e.message : String(e)}`, expr.expression, e);
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
function stringifyValue(value) {
|
|
1205
|
+
if (value === null || value === undefined)
|
|
1206
|
+
return "";
|
|
1207
|
+
if (typeof value === "object")
|
|
1208
|
+
return JSON.stringify(value);
|
|
1209
|
+
return String(value);
|
|
1210
|
+
}
|
|
1211
|
+
function interpolateTemplate(template, scope, stepId) {
|
|
1212
|
+
const { expressions } = extractTemplateExpressions(template);
|
|
1213
|
+
if (expressions.length === 0)
|
|
1214
|
+
return template;
|
|
1215
|
+
let result = "";
|
|
1216
|
+
let lastEnd = 0;
|
|
1217
|
+
for (const expr of expressions) {
|
|
1218
|
+
result += template.slice(lastEnd, expr.start);
|
|
1219
|
+
try {
|
|
1220
|
+
const value = search(scope, expr.expression);
|
|
1221
|
+
result += stringifyValue(value);
|
|
1222
|
+
} catch (e) {
|
|
1223
|
+
throw new ExpressionError(stepId, "TEMPLATE_INTERPOLATION_ERROR", `Template expression '${expr.expression}' failed: ${e instanceof Error ? e.message : String(e)}`, expr.expression, e);
|
|
1224
|
+
}
|
|
1225
|
+
lastEnd = expr.end;
|
|
1226
|
+
}
|
|
1227
|
+
result += template.slice(lastEnd);
|
|
1228
|
+
return result;
|
|
1229
|
+
}
|
|
1230
|
+
function classifyLlmError(stepId, e) {
|
|
1231
|
+
if (APICallError.isInstance(e)) {
|
|
1232
|
+
const code = e.statusCode === 429 ? "LLM_RATE_LIMITED" : "LLM_API_ERROR";
|
|
1233
|
+
return new ExternalServiceError(stepId, code, e.message, e, e.statusCode, e.isRetryable ?? true);
|
|
1234
|
+
}
|
|
1235
|
+
if (RetryError.isInstance(e)) {
|
|
1236
|
+
return new ExternalServiceError(stepId, "LLM_API_ERROR", e.message, e, undefined, false);
|
|
1237
|
+
}
|
|
1238
|
+
if (NoContentGeneratedError.isInstance(e)) {
|
|
1239
|
+
return new ExternalServiceError(stepId, "LLM_NO_CONTENT", e.message, e, undefined, true);
|
|
1240
|
+
}
|
|
1241
|
+
if (TypeValidationError.isInstance(e)) {
|
|
1242
|
+
return new OutputQualityError(stepId, "LLM_OUTPUT_PARSE_ERROR", `LLM output could not be parsed: ${e.message}`, e.value, e);
|
|
1243
|
+
}
|
|
1244
|
+
if (JSONParseError.isInstance(e)) {
|
|
1245
|
+
return new OutputQualityError(stepId, "LLM_OUTPUT_PARSE_ERROR", `LLM output could not be parsed: ${e.message}`, e.text, e);
|
|
1246
|
+
}
|
|
1247
|
+
return new ExternalServiceError(stepId, "LLM_NETWORK_ERROR", e instanceof Error ? e.message : String(e), e, undefined, true);
|
|
1248
|
+
}
|
|
1249
|
+
function validateWorkflowInputs(step, inputs) {
|
|
1250
|
+
const schema = step.params.inputSchema;
|
|
1251
|
+
if (!schema || typeof schema !== "object")
|
|
1252
|
+
return;
|
|
1253
|
+
const required = schema.required;
|
|
1254
|
+
if (Array.isArray(required)) {
|
|
1255
|
+
const missing = required.filter((key) => typeof key === "string" && !(key in inputs));
|
|
1256
|
+
if (missing.length > 0) {
|
|
1257
|
+
throw new ValidationError(step.id, "TOOL_INPUT_VALIDATION_FAILED", `Workflow input validation failed: missing required input(s): ${missing.join(", ")}`, inputs);
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
const properties = schema.properties;
|
|
1261
|
+
if (properties && typeof properties === "object") {
|
|
1262
|
+
for (const [key, value] of Object.entries(inputs)) {
|
|
1263
|
+
const propSchema = properties[key];
|
|
1264
|
+
if (propSchema && typeof propSchema === "object" && "type" in propSchema) {
|
|
1265
|
+
const expectedType = propSchema.type;
|
|
1266
|
+
const actualType = typeof value;
|
|
1267
|
+
if (expectedType === "integer" || expectedType === "number") {
|
|
1268
|
+
if (actualType !== "number") {
|
|
1269
|
+
throw new ValidationError(step.id, "TOOL_INPUT_VALIDATION_FAILED", `Workflow input validation failed: input '${key}' expected type '${expectedType}' but got '${actualType}'`, inputs);
|
|
1270
|
+
}
|
|
1271
|
+
} else if (expectedType === "array") {
|
|
1272
|
+
if (!Array.isArray(value)) {
|
|
1273
|
+
throw new ValidationError(step.id, "TOOL_INPUT_VALIDATION_FAILED", `Workflow input validation failed: input '${key}' expected type 'array' but got '${actualType}'`, inputs);
|
|
1274
|
+
}
|
|
1275
|
+
} else if (actualType !== expectedType) {
|
|
1276
|
+
throw new ValidationError(step.id, "TOOL_INPUT_VALIDATION_FAILED", `Workflow input validation failed: input '${key}' expected type '${expectedType}' but got '${actualType}'`, inputs);
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
function validateWorkflowOutput(outputSchema, output, endStepId) {
|
|
1283
|
+
const expectedType = outputSchema.type;
|
|
1284
|
+
if (typeof expectedType === "string") {
|
|
1285
|
+
if (expectedType === "object" && (typeof output !== "object" || output === null)) {
|
|
1286
|
+
throw new ValidationError(endStepId, "WORKFLOW_OUTPUT_VALIDATION_FAILED", `Workflow output validation failed: expected type 'object' but got '${output === null ? "null" : typeof output}'`, output);
|
|
1287
|
+
}
|
|
1288
|
+
if (expectedType === "array" && !Array.isArray(output)) {
|
|
1289
|
+
throw new ValidationError(endStepId, "WORKFLOW_OUTPUT_VALIDATION_FAILED", `Workflow output validation failed: expected type 'array' but got '${typeof output}'`, output);
|
|
1290
|
+
}
|
|
1291
|
+
if ((expectedType === "string" || expectedType === "boolean") && typeof output !== expectedType) {
|
|
1292
|
+
throw new ValidationError(endStepId, "WORKFLOW_OUTPUT_VALIDATION_FAILED", `Workflow output validation failed: expected type '${expectedType}' but got '${typeof output}'`, output);
|
|
1293
|
+
}
|
|
1294
|
+
if ((expectedType === "number" || expectedType === "integer") && typeof output !== "number") {
|
|
1295
|
+
throw new ValidationError(endStepId, "WORKFLOW_OUTPUT_VALIDATION_FAILED", `Workflow output validation failed: expected type '${expectedType}' but got '${typeof output}'`, output);
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
if (typeof output === "object" && output !== null && !Array.isArray(output)) {
|
|
1299
|
+
const required = outputSchema.required;
|
|
1300
|
+
if (Array.isArray(required)) {
|
|
1301
|
+
const missing = required.filter((key) => typeof key === "string" && !(key in output));
|
|
1302
|
+
if (missing.length > 0) {
|
|
1303
|
+
throw new ValidationError(endStepId, "WORKFLOW_OUTPUT_VALIDATION_FAILED", `Workflow output validation failed: missing required field(s): ${missing.join(", ")}`, output);
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
const properties = outputSchema.properties;
|
|
1307
|
+
if (properties && typeof properties === "object") {
|
|
1308
|
+
for (const [key, value] of Object.entries(output)) {
|
|
1309
|
+
const propSchema = properties[key];
|
|
1310
|
+
if (propSchema && typeof propSchema === "object" && "type" in propSchema) {
|
|
1311
|
+
const propExpectedType = propSchema.type;
|
|
1312
|
+
const actualType = typeof value;
|
|
1313
|
+
if (propExpectedType === "integer" || propExpectedType === "number") {
|
|
1314
|
+
if (actualType !== "number") {
|
|
1315
|
+
throw new ValidationError(endStepId, "WORKFLOW_OUTPUT_VALIDATION_FAILED", `Workflow output validation failed: field '${key}' expected type '${propExpectedType}' but got '${actualType}'`, output);
|
|
1316
|
+
}
|
|
1317
|
+
} else if (propExpectedType === "array") {
|
|
1318
|
+
if (!Array.isArray(value)) {
|
|
1319
|
+
throw new ValidationError(endStepId, "WORKFLOW_OUTPUT_VALIDATION_FAILED", `Workflow output validation failed: field '${key}' expected type 'array' but got '${actualType}'`, output);
|
|
1320
|
+
}
|
|
1321
|
+
} else if (actualType !== propExpectedType) {
|
|
1322
|
+
throw new ValidationError(endStepId, "WORKFLOW_OUTPUT_VALIDATION_FAILED", `Workflow output validation failed: field '${key}' expected type '${propExpectedType}' but got '${actualType}'`, output);
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
async function executeToolCall(step, scope, tools) {
|
|
1330
|
+
const toolDef = tools[step.params.toolName];
|
|
1331
|
+
if (!toolDef?.execute) {
|
|
1332
|
+
throw new ConfigurationError(step.id, "TOOL_NOT_FOUND", `Tool '${step.params.toolName}' not found or has no execute function`);
|
|
1333
|
+
}
|
|
1334
|
+
const resolvedInput = {};
|
|
1335
|
+
for (const [key, expr] of Object.entries(step.params.toolInput)) {
|
|
1336
|
+
resolvedInput[key] = evaluateExpression(expr, scope, step.id);
|
|
1337
|
+
}
|
|
1338
|
+
if (toolDef.inputSchema) {
|
|
1339
|
+
const validation = await safeValidateTypes({
|
|
1340
|
+
value: resolvedInput,
|
|
1341
|
+
schema: toolDef.inputSchema
|
|
1342
|
+
});
|
|
1343
|
+
if (!validation.success) {
|
|
1344
|
+
throw new ValidationError(step.id, "TOOL_INPUT_VALIDATION_FAILED", `Tool '${step.params.toolName}' input validation failed: ${validation.error.message}`, resolvedInput, validation.error);
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
try {
|
|
1348
|
+
return await toolDef.execute(resolvedInput, {
|
|
1349
|
+
toolCallId: step.id,
|
|
1350
|
+
messages: []
|
|
1351
|
+
});
|
|
1352
|
+
} catch (e) {
|
|
1353
|
+
throw new ExternalServiceError(step.id, "TOOL_EXECUTION_FAILED", e instanceof Error ? e.message : String(e), e);
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
async function executeLlmPrompt(step, scope, agent) {
|
|
1357
|
+
const interpolatedPrompt = interpolateTemplate(step.params.prompt, scope, step.id);
|
|
1358
|
+
const schemaStr = JSON.stringify(step.params.outputFormat, null, 2);
|
|
1359
|
+
const prompt = `${interpolatedPrompt}
|
|
1360
|
+
|
|
1361
|
+
You must respond with valid JSON matching this JSON Schema:
|
|
1362
|
+
${schemaStr}
|
|
1363
|
+
|
|
1364
|
+
Respond ONLY with the JSON object, no other text.`;
|
|
1365
|
+
try {
|
|
1366
|
+
const result = await agent.generate({ prompt });
|
|
1367
|
+
return JSON.parse(stripCodeFence(result.text));
|
|
1368
|
+
} catch (e) {
|
|
1369
|
+
if (e instanceof StepExecutionError)
|
|
1370
|
+
throw e;
|
|
1371
|
+
if (e instanceof SyntaxError) {
|
|
1372
|
+
throw new OutputQualityError(step.id, "LLM_OUTPUT_PARSE_ERROR", `LLM output is not valid JSON: ${e.message}`, undefined, e);
|
|
1373
|
+
}
|
|
1374
|
+
throw classifyLlmError(step.id, e);
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
async function executeExtractData(step, scope, agent) {
|
|
1378
|
+
const sourceData = evaluateExpression(step.params.sourceData, scope, step.id);
|
|
1379
|
+
const sourceStr = typeof sourceData === "string" ? sourceData : JSON.stringify(sourceData, null, 2);
|
|
1380
|
+
const schemaStr = JSON.stringify(step.params.outputFormat, null, 2);
|
|
1381
|
+
const prompt = `Extract the following structured data from the provided source data.
|
|
1382
|
+
|
|
1383
|
+
Source data:
|
|
1384
|
+
${sourceStr}
|
|
1385
|
+
|
|
1386
|
+
You must respond with valid JSON matching this JSON Schema:
|
|
1387
|
+
${schemaStr}
|
|
1388
|
+
|
|
1389
|
+
Respond ONLY with the JSON object, no other text.`;
|
|
1390
|
+
try {
|
|
1391
|
+
const result = await agent.generate({ prompt });
|
|
1392
|
+
return JSON.parse(stripCodeFence(result.text));
|
|
1393
|
+
} catch (e) {
|
|
1394
|
+
if (e instanceof StepExecutionError)
|
|
1395
|
+
throw e;
|
|
1396
|
+
if (e instanceof SyntaxError) {
|
|
1397
|
+
throw new OutputQualityError(step.id, "LLM_OUTPUT_PARSE_ERROR", `LLM output is not valid JSON: ${e.message}`, undefined, e);
|
|
1398
|
+
}
|
|
1399
|
+
throw classifyLlmError(step.id, e);
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
async function executeSwitchCase(step, scope, stepIndex, stepOutputs, loopVars, options) {
|
|
1403
|
+
const switchValue = evaluateExpression(step.params.switchOn, scope, step.id);
|
|
1404
|
+
let matchedBranchId;
|
|
1405
|
+
let defaultBranchId;
|
|
1406
|
+
for (const c of step.params.cases) {
|
|
1407
|
+
if (c.value.type === "default") {
|
|
1408
|
+
defaultBranchId = c.branchBodyStepId;
|
|
1409
|
+
} else {
|
|
1410
|
+
const caseValue = evaluateExpression(c.value, scope, step.id);
|
|
1411
|
+
if (caseValue === switchValue) {
|
|
1412
|
+
matchedBranchId = c.branchBodyStepId;
|
|
1413
|
+
break;
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
const selectedBranchId = matchedBranchId ?? defaultBranchId;
|
|
1418
|
+
if (!selectedBranchId) {
|
|
1419
|
+
return;
|
|
1420
|
+
}
|
|
1421
|
+
return await executeChain(selectedBranchId, stepIndex, stepOutputs, loopVars, options);
|
|
1422
|
+
}
|
|
1423
|
+
async function executeForEach(step, scope, stepIndex, stepOutputs, loopVars, options) {
|
|
1424
|
+
const target = evaluateExpression(step.params.target, scope, step.id);
|
|
1425
|
+
if (!Array.isArray(target)) {
|
|
1426
|
+
throw new ValidationError(step.id, "FOREACH_TARGET_NOT_ARRAY", `for-each target must be an array, got ${typeof target}`, target);
|
|
1427
|
+
}
|
|
1428
|
+
const results = [];
|
|
1429
|
+
for (const item of target) {
|
|
1430
|
+
const innerLoopVars = { ...loopVars, [step.params.itemName]: item };
|
|
1431
|
+
const lastOutput = await executeChain(step.params.loopBodyStepId, stepIndex, stepOutputs, innerLoopVars, options);
|
|
1432
|
+
results.push(lastOutput);
|
|
1433
|
+
}
|
|
1434
|
+
return results;
|
|
1435
|
+
}
|
|
1436
|
+
async function executeStep(step, scope, stepIndex, stepOutputs, loopVars, options) {
|
|
1437
|
+
switch (step.type) {
|
|
1438
|
+
case "tool-call":
|
|
1439
|
+
return executeToolCall(step, scope, options.tools);
|
|
1440
|
+
case "llm-prompt": {
|
|
1441
|
+
if (!options.agent)
|
|
1442
|
+
throw new ConfigurationError(step.id, "AGENT_NOT_PROVIDED", "No agent provided");
|
|
1443
|
+
return executeLlmPrompt(step, scope, options.agent);
|
|
1444
|
+
}
|
|
1445
|
+
case "extract-data": {
|
|
1446
|
+
if (!options.agent)
|
|
1447
|
+
throw new ConfigurationError(step.id, "AGENT_NOT_PROVIDED", "No agent provided");
|
|
1448
|
+
return executeExtractData(step, scope, options.agent);
|
|
1449
|
+
}
|
|
1450
|
+
case "switch-case":
|
|
1451
|
+
return executeSwitchCase(step, scope, stepIndex, stepOutputs, loopVars, options);
|
|
1452
|
+
case "for-each":
|
|
1453
|
+
return executeForEach(step, scope, stepIndex, stepOutputs, loopVars, options);
|
|
1454
|
+
case "start": {
|
|
1455
|
+
const inputs = options.inputs ?? {};
|
|
1456
|
+
const startStep = step;
|
|
1457
|
+
validateWorkflowInputs(startStep, inputs);
|
|
1458
|
+
return inputs;
|
|
1459
|
+
}
|
|
1460
|
+
case "end": {
|
|
1461
|
+
const endStep = step;
|
|
1462
|
+
if (endStep.params?.output) {
|
|
1463
|
+
return evaluateExpression(endStep.params.output, scope, step.id);
|
|
1464
|
+
}
|
|
1465
|
+
return;
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
var DEFAULT_MAX_RETRIES = 3;
|
|
1470
|
+
var DEFAULT_BASE_DELAY_MS = 1000;
|
|
1471
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
1472
|
+
async function retryStep(step, stepIndex, stepOutputs, loopVars, options, originalError) {
|
|
1473
|
+
const maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
1474
|
+
const baseDelay = options.retryDelayMs ?? DEFAULT_BASE_DELAY_MS;
|
|
1475
|
+
const scope = { ...stepOutputs, ...loopVars };
|
|
1476
|
+
for (let attempt = 1;attempt <= maxRetries; attempt++) {
|
|
1477
|
+
await sleep(baseDelay * 2 ** (attempt - 1));
|
|
1478
|
+
try {
|
|
1479
|
+
return await executeStep(step, scope, stepIndex, stepOutputs, loopVars, options);
|
|
1480
|
+
} catch {
|
|
1481
|
+
if (attempt === maxRetries)
|
|
1482
|
+
throw originalError;
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
throw originalError;
|
|
1486
|
+
}
|
|
1487
|
+
async function recoverFromError(error, step, stepIndex, stepOutputs, loopVars, options) {
|
|
1488
|
+
switch (error.code) {
|
|
1489
|
+
case "LLM_RATE_LIMITED":
|
|
1490
|
+
case "LLM_NETWORK_ERROR":
|
|
1491
|
+
case "LLM_NO_CONTENT":
|
|
1492
|
+
case "LLM_OUTPUT_PARSE_ERROR":
|
|
1493
|
+
return retryStep(step, stepIndex, stepOutputs, loopVars, options, error);
|
|
1494
|
+
case "LLM_API_ERROR":
|
|
1495
|
+
if (error instanceof ExternalServiceError && error.isRetryable) {
|
|
1496
|
+
return retryStep(step, stepIndex, stepOutputs, loopVars, options, error);
|
|
1497
|
+
}
|
|
1498
|
+
throw error;
|
|
1499
|
+
default:
|
|
1500
|
+
throw error;
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
async function executeChain(startStepId, stepIndex, stepOutputs, loopVars, options) {
|
|
1504
|
+
let currentStepId = startStepId;
|
|
1505
|
+
let lastOutput;
|
|
1506
|
+
while (currentStepId) {
|
|
1507
|
+
const step = stepIndex.get(currentStepId);
|
|
1508
|
+
if (!step) {
|
|
1509
|
+
throw new Error(`Step '${currentStepId}' not found`);
|
|
1510
|
+
}
|
|
1511
|
+
options.onStepStart?.(step.id, step);
|
|
1512
|
+
const scope = { ...stepOutputs, ...loopVars };
|
|
1513
|
+
let stepOutput;
|
|
1514
|
+
try {
|
|
1515
|
+
stepOutput = await executeStep(step, scope, stepIndex, stepOutputs, loopVars, options);
|
|
1516
|
+
} catch (e) {
|
|
1517
|
+
if (!(e instanceof StepExecutionError))
|
|
1518
|
+
throw e;
|
|
1519
|
+
stepOutput = await recoverFromError(e, step, stepIndex, stepOutputs, loopVars, options);
|
|
1520
|
+
}
|
|
1521
|
+
stepOutputs[step.id] = stepOutput;
|
|
1522
|
+
lastOutput = stepOutput;
|
|
1523
|
+
options.onStepComplete?.(step.id, stepOutput);
|
|
1524
|
+
currentStepId = step.nextStepId;
|
|
1525
|
+
}
|
|
1526
|
+
return lastOutput;
|
|
1527
|
+
}
|
|
1528
|
+
function validateWorkflowConfig(workflow, options) {
|
|
1529
|
+
const needsAgent = workflow.steps.some((s) => s.type === "llm-prompt" || s.type === "extract-data");
|
|
1530
|
+
if (needsAgent && !options.agent) {
|
|
1531
|
+
const llmStep = workflow.steps.find((s) => s.type === "llm-prompt" || s.type === "extract-data");
|
|
1532
|
+
throw new ConfigurationError(llmStep?.id ?? "unknown", "AGENT_NOT_PROVIDED", "Workflow contains LLM steps but no agent was provided");
|
|
1533
|
+
}
|
|
1534
|
+
for (const step of workflow.steps) {
|
|
1535
|
+
if (step.type !== "tool-call")
|
|
1536
|
+
continue;
|
|
1537
|
+
const toolDef = options.tools[step.params.toolName];
|
|
1538
|
+
if (!toolDef) {
|
|
1539
|
+
throw new ConfigurationError(step.id, "TOOL_NOT_FOUND", `Tool '${step.params.toolName}' not found`);
|
|
1540
|
+
}
|
|
1541
|
+
if (!toolDef.execute) {
|
|
1542
|
+
throw new ConfigurationError(step.id, "TOOL_MISSING_EXECUTE", `Tool '${step.params.toolName}' has no execute function`);
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
async function executeWorkflow(workflow, options) {
|
|
1547
|
+
const stepIndex = new Map;
|
|
1548
|
+
for (const step of workflow.steps) {
|
|
1549
|
+
stepIndex.set(step.id, step);
|
|
1550
|
+
}
|
|
1551
|
+
const stepOutputs = {};
|
|
1552
|
+
const resolvedAgent = options.agent ? isAgent(options.agent) ? options.agent : new ToolLoopAgent({
|
|
1553
|
+
model: options.agent,
|
|
1554
|
+
stopWhen: stepCountIs(1)
|
|
1555
|
+
}) : undefined;
|
|
1556
|
+
const resolvedOptions = { ...options, agent: resolvedAgent };
|
|
1557
|
+
try {
|
|
1558
|
+
validateWorkflowConfig(workflow, resolvedOptions);
|
|
1559
|
+
const chainOutput = await executeChain(workflow.initialStepId, stepIndex, stepOutputs, {}, resolvedOptions);
|
|
1560
|
+
if (workflow.outputSchema) {
|
|
1561
|
+
let endStepId = "unknown";
|
|
1562
|
+
for (const step of workflow.steps) {
|
|
1563
|
+
if (step.type === "end" && step.id in stepOutputs) {
|
|
1564
|
+
endStepId = step.id;
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
validateWorkflowOutput(workflow.outputSchema, chainOutput, endStepId);
|
|
1568
|
+
}
|
|
1569
|
+
return { success: true, stepOutputs, output: chainOutput };
|
|
1570
|
+
} catch (e) {
|
|
1571
|
+
const error = e instanceof StepExecutionError ? e : new ExternalServiceError("unknown", "TOOL_EXECUTION_FAILED", e instanceof Error ? e.message : String(e), e, undefined, false);
|
|
1572
|
+
return {
|
|
1573
|
+
success: false,
|
|
1574
|
+
stepOutputs,
|
|
1575
|
+
error
|
|
1576
|
+
};
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
// src/generator/index.ts
|
|
1580
|
+
import { generateText, stepCountIs as stepCountIs2, tool } from "ai";
|
|
1581
|
+
import { type as arktype } from "arktype";
|
|
1582
|
+
|
|
1583
|
+
// src/types.ts
|
|
1584
|
+
import { type } from "arktype";
|
|
1585
|
+
var expressionSchema = type({
|
|
1586
|
+
type: "'literal'",
|
|
1587
|
+
value: "unknown"
|
|
1588
|
+
}).or({
|
|
1589
|
+
type: "'jmespath'",
|
|
1590
|
+
expression: "string"
|
|
1591
|
+
}).describe("a value that must always be wrapped as an expression object — use { type: 'literal', value: ... } for any static value (strings, numbers, booleans, etc.), or { type: 'jmespath', expression: '...' } for dynamic data extracted from previous steps' outputs (via their step ids, e.g. `stepId.someKey`) or loop variables (e.g. `itemName.someKey` within a for-each loop body)");
|
|
1592
|
+
var toolCallParamsSchema = type({
|
|
1593
|
+
type: "'tool-call'",
|
|
1594
|
+
params: {
|
|
1595
|
+
toolName: "string",
|
|
1596
|
+
toolInput: [
|
|
1597
|
+
{
|
|
1598
|
+
"[string]": expressionSchema
|
|
1599
|
+
},
|
|
1600
|
+
"@",
|
|
1601
|
+
"a map of input parameter names to their values; ALL values must be wrapped as expression objects — even static strings like email addresses must use { type: 'literal', value: '...' }, never plain primitives"
|
|
1602
|
+
]
|
|
1603
|
+
}
|
|
1604
|
+
}).describe("a step that calls a tool with specified input parameters (which can be static values or expressions)");
|
|
1605
|
+
var switchCaseParamsSchema = type({
|
|
1606
|
+
type: "'switch-case'",
|
|
1607
|
+
params: {
|
|
1608
|
+
switchOn: expressionSchema,
|
|
1609
|
+
cases: [
|
|
1610
|
+
{
|
|
1611
|
+
value: expressionSchema.or({ type: "'default'" }),
|
|
1612
|
+
branchBodyStepId: [
|
|
1613
|
+
"string",
|
|
1614
|
+
"@",
|
|
1615
|
+
"the id of the first step in the branch body chain to execute if this case matches"
|
|
1616
|
+
]
|
|
1617
|
+
},
|
|
1618
|
+
"[]"
|
|
1619
|
+
]
|
|
1620
|
+
}
|
|
1621
|
+
}).describe("a step that branches to different step chains based on the value of an expression; each case's chain runs until a step with no nextStepId, at which point execution continues with this step's nextStepId; a case with type 'default' serves as the fallback if no other case matches");
|
|
1622
|
+
var forEachParamsSchema = type({
|
|
1623
|
+
type: "'for-each'",
|
|
1624
|
+
params: {
|
|
1625
|
+
target: expressionSchema,
|
|
1626
|
+
itemName: [
|
|
1627
|
+
"string",
|
|
1628
|
+
"@",
|
|
1629
|
+
"the name to refer to the current item in the list within expressions in the loop body"
|
|
1630
|
+
],
|
|
1631
|
+
loopBodyStepId: [
|
|
1632
|
+
"string",
|
|
1633
|
+
"@",
|
|
1634
|
+
"the id of the first step in the loop body chain to execute for each item in the list"
|
|
1635
|
+
]
|
|
1636
|
+
}
|
|
1637
|
+
}).describe("a step that iterates over a list and executes a chain of steps for each item; the loop body chain runs until a step with no nextStepId, at which point the next iteration begins; once all items are exhausted, execution continues with this step's nextStepId");
|
|
1638
|
+
var llmPromptSchema = type({
|
|
1639
|
+
type: "'llm-prompt'",
|
|
1640
|
+
params: {
|
|
1641
|
+
prompt: [
|
|
1642
|
+
"string",
|
|
1643
|
+
"@",
|
|
1644
|
+
"a template string where JMESPath expressions can be embedded using ${...} syntax (e.g. 'Hello ${user.name}, you have ${length(user.messages)} messages'). All data from previous steps is available via their step ids (e.g. ${stepId.someKey}), and loop variables are available within for-each loop bodies (e.g. ${itemName.someKey})"
|
|
1645
|
+
],
|
|
1646
|
+
outputFormat: [
|
|
1647
|
+
"object",
|
|
1648
|
+
"@",
|
|
1649
|
+
"JSON schema specifying the output format expected from the LLM"
|
|
1650
|
+
]
|
|
1651
|
+
}
|
|
1652
|
+
}).describe("a step that prompts an LLM with a text prompt to produce an output in a specified format");
|
|
1653
|
+
var extractDataParamsSchema = type({
|
|
1654
|
+
type: "'extract-data'",
|
|
1655
|
+
params: {
|
|
1656
|
+
sourceData: [expressionSchema, "@", "the data to extract information from"],
|
|
1657
|
+
outputFormat: [
|
|
1658
|
+
"object",
|
|
1659
|
+
"@",
|
|
1660
|
+
"JSON schema specifying the output format expected from the data extraction"
|
|
1661
|
+
]
|
|
1662
|
+
}
|
|
1663
|
+
}).describe("a step that uses an LLM to extract structured data from a larger blob of source data (e.g. llm responses or tool outputs with unknown output formats) based on a specified output format");
|
|
1664
|
+
var startParamsSchema = type({
|
|
1665
|
+
type: "'start'",
|
|
1666
|
+
params: {
|
|
1667
|
+
inputSchema: [
|
|
1668
|
+
"object",
|
|
1669
|
+
"@",
|
|
1670
|
+
"a JSON Schema object defining the inputs required to run the workflow; the workflow executor will validate provided inputs against this schema, and the validated inputs become available in JMESPath scope via this step's id"
|
|
1671
|
+
]
|
|
1672
|
+
}
|
|
1673
|
+
}).describe("a step that marks the entry point of a workflow and declares the input schema; its output is the validated input data, accessible by subsequent steps via its step id");
|
|
1674
|
+
var endSchema = type({
|
|
1675
|
+
type: "'end'",
|
|
1676
|
+
"params?": {
|
|
1677
|
+
output: expressionSchema
|
|
1678
|
+
}
|
|
1679
|
+
}).describe("a step that indicates the end of a branch; optionally specify an output expression whose evaluated value becomes the workflow's output");
|
|
1680
|
+
var workflowStepSchema = type({
|
|
1681
|
+
id: /^[a-zA-Z_][a-zA-Z0-9_]+$/,
|
|
1682
|
+
name: "string",
|
|
1683
|
+
description: "string",
|
|
1684
|
+
"nextStepId?": "string"
|
|
1685
|
+
}).and(toolCallParamsSchema.or(llmPromptSchema).or(extractDataParamsSchema).or(switchCaseParamsSchema).or(forEachParamsSchema).or(startParamsSchema).or(endSchema));
|
|
1686
|
+
var workflowDefinitionSchema = type({
|
|
1687
|
+
initialStepId: "string",
|
|
1688
|
+
"outputSchema?": [
|
|
1689
|
+
"object",
|
|
1690
|
+
"@",
|
|
1691
|
+
"an optional JSON Schema object declaring the shape of the workflow's output; when present, the value produced by the end step's output expression will be validated against this schema"
|
|
1692
|
+
],
|
|
1693
|
+
steps: [
|
|
1694
|
+
[workflowStepSchema, "[]"],
|
|
1695
|
+
"@",
|
|
1696
|
+
"a list of steps to execute in the workflow; these should be in no particular order as execution flow is determined by the nextStepId fields and branching logic within the steps"
|
|
1697
|
+
]
|
|
1698
|
+
});
|
|
1699
|
+
|
|
1700
|
+
// src/generator/prompt.ts
|
|
1701
|
+
import { asSchema as asSchema2 } from "ai";
|
|
1702
|
+
async function serializeToolsForPrompt(tools) {
|
|
1703
|
+
return JSON.stringify(await Promise.all(Object.entries(tools).map(async ([toolName, toolDef]) => ({
|
|
1704
|
+
name: toolName,
|
|
1705
|
+
description: toolDef.description,
|
|
1706
|
+
inputSchema: await asSchema2(toolDef.inputSchema).jsonSchema,
|
|
1707
|
+
outputSchema: toolDef.outputSchema ? await asSchema2(toolDef.outputSchema).jsonSchema : undefined
|
|
1708
|
+
}))));
|
|
1709
|
+
}
|
|
1710
|
+
function buildWorkflowGenerationPrompt(serializedTools) {
|
|
1711
|
+
return `You are a workflow architect. Your job is to design a workflow definition in the remora DSL that accomplishes a given task using the provided tools. You MUST call the createWorkflow tool with a valid workflow definition.
|
|
1712
|
+
|
|
1713
|
+
## Workflow Structure
|
|
1714
|
+
|
|
1715
|
+
A workflow has:
|
|
1716
|
+
- \`initialStepId\`: the id of the first step to execute
|
|
1717
|
+
- \`steps\`: an array of step objects (order does not matter — execution flow is determined by nextStepId links)
|
|
1718
|
+
- \`outputSchema\` (optional): a JSON Schema object declaring the shape of the workflow's output. When present, every \`end\` step should have an \`output\` expression that evaluates to a value matching this schema.
|
|
1719
|
+
|
|
1720
|
+
## Step Common Fields
|
|
1721
|
+
|
|
1722
|
+
Every step has:
|
|
1723
|
+
- \`id\`: unique identifier matching /^[a-zA-Z_][a-zA-Z0-9_]+$/ (letters, digits, underscores; at least 2 characters)
|
|
1724
|
+
- \`name\`: human-readable name
|
|
1725
|
+
- \`description\`: what this step does
|
|
1726
|
+
- \`type\`: one of the step types below
|
|
1727
|
+
- \`nextStepId\` (optional): id of the next step to execute after this one
|
|
1728
|
+
|
|
1729
|
+
## Step Types
|
|
1730
|
+
|
|
1731
|
+
### start
|
|
1732
|
+
Entry point that declares and validates workflow inputs. Its output (the validated inputs) is accessible by subsequent steps via its step id.
|
|
1733
|
+
\`\`\`json
|
|
1734
|
+
{
|
|
1735
|
+
"type": "start",
|
|
1736
|
+
"params": {
|
|
1737
|
+
"inputSchema": { "type": "object", "properties": { ... }, "required": [...] }
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
\`\`\`
|
|
1741
|
+
|
|
1742
|
+
### tool-call
|
|
1743
|
+
Calls a tool with input parameters. All values in toolInput MUST be expression objects.
|
|
1744
|
+
\`\`\`json
|
|
1745
|
+
{
|
|
1746
|
+
"type": "tool-call",
|
|
1747
|
+
"params": {
|
|
1748
|
+
"toolName": "name-of-tool",
|
|
1749
|
+
"toolInput": {
|
|
1750
|
+
"paramName": { "type": "literal", "value": "static value" },
|
|
1751
|
+
"otherParam": { "type": "jmespath", "expression": "previous_step.someField" }
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
\`\`\`
|
|
1756
|
+
|
|
1757
|
+
### llm-prompt
|
|
1758
|
+
Prompts an LLM with a template string to produce structured output. Use \${...} syntax to embed JMESPath expressions in the prompt.
|
|
1759
|
+
\`\`\`json
|
|
1760
|
+
{
|
|
1761
|
+
"type": "llm-prompt",
|
|
1762
|
+
"params": {
|
|
1763
|
+
"prompt": "Classify this ticket: \${get_tickets.tickets[0].subject}",
|
|
1764
|
+
"outputFormat": { "type": "object", "properties": { "category": { "type": "string" } }, "required": ["category"] }
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
\`\`\`
|
|
1768
|
+
|
|
1769
|
+
### extract-data
|
|
1770
|
+
Uses an LLM to extract structured data from a source. Use when you need to parse unstructured or semi-structured data into a known shape.
|
|
1771
|
+
\`\`\`json
|
|
1772
|
+
{
|
|
1773
|
+
"type": "extract-data",
|
|
1774
|
+
"params": {
|
|
1775
|
+
"sourceData": { "type": "jmespath", "expression": "previous_step.rawContent" },
|
|
1776
|
+
"outputFormat": { "type": "object", "properties": { ... }, "required": [...] }
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
\`\`\`
|
|
1780
|
+
|
|
1781
|
+
### switch-case
|
|
1782
|
+
Branches execution based on a value. Each case's branch chain runs until a step with no nextStepId, then execution continues with this step's nextStepId. Use \`{ "type": "default" }\` for a fallback case.
|
|
1783
|
+
\`\`\`json
|
|
1784
|
+
{
|
|
1785
|
+
"type": "switch-case",
|
|
1786
|
+
"params": {
|
|
1787
|
+
"switchOn": { "type": "jmespath", "expression": "classify.category" },
|
|
1788
|
+
"cases": [
|
|
1789
|
+
{
|
|
1790
|
+
"value": { "type": "literal", "value": "critical" },
|
|
1791
|
+
"branchBodyStepId": "handle_critical"
|
|
1792
|
+
},
|
|
1793
|
+
{
|
|
1794
|
+
"value": { "type": "default" },
|
|
1795
|
+
"branchBodyStepId": "handle_other"
|
|
1796
|
+
}
|
|
1797
|
+
]
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
\`\`\`
|
|
1801
|
+
|
|
1802
|
+
### for-each
|
|
1803
|
+
Iterates over a list and executes a chain of steps for each item. The loop body chain runs until a step with no nextStepId, then the next iteration begins. After all items, execution continues with this step's nextStepId. The \`itemName\` becomes a scoped variable accessible only within the loop body.
|
|
1804
|
+
\`\`\`json
|
|
1805
|
+
{
|
|
1806
|
+
"type": "for-each",
|
|
1807
|
+
"params": {
|
|
1808
|
+
"target": { "type": "jmespath", "expression": "get_items.items" },
|
|
1809
|
+
"itemName": "item",
|
|
1810
|
+
"loopBodyStepId": "process_item"
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
\`\`\`
|
|
1814
|
+
|
|
1815
|
+
### end
|
|
1816
|
+
Marks the end of a branch or the workflow. Must NOT have a nextStepId. Optionally, specify an output expression whose value becomes the workflow's return value.
|
|
1817
|
+
\`\`\`json
|
|
1818
|
+
{
|
|
1819
|
+
"type": "end"
|
|
1820
|
+
}
|
|
1821
|
+
\`\`\`
|
|
1822
|
+
|
|
1823
|
+
With output (when the workflow declares an outputSchema):
|
|
1824
|
+
\`\`\`json
|
|
1825
|
+
{
|
|
1826
|
+
"type": "end",
|
|
1827
|
+
"params": {
|
|
1828
|
+
"output": { "type": "jmespath", "expression": "summarize.result" }
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
\`\`\`
|
|
1832
|
+
|
|
1833
|
+
## Expression System
|
|
1834
|
+
|
|
1835
|
+
Every dynamic value must be an expression object:
|
|
1836
|
+
|
|
1837
|
+
1. **Literal** — for static values known at design time:
|
|
1838
|
+
\`{ "type": "literal", "value": <any value> }\`
|
|
1839
|
+
|
|
1840
|
+
2. **JMESPath** — for referencing data from previous steps or loop variables:
|
|
1841
|
+
\`{ "type": "jmespath", "expression": "<expression>" }\`
|
|
1842
|
+
The root of a JMESPath expression must be either a step id (e.g. \`get_orders.orders\`) or a loop variable name (e.g. \`item.id\` within a for-each body).
|
|
1843
|
+
|
|
1844
|
+
3. **Template strings** (llm-prompt only) — embed JMESPath in the prompt string using \${...}:
|
|
1845
|
+
\`"Summarize: \${fetch_data.content}"\`
|
|
1846
|
+
These are NOT expression objects — they appear directly in the prompt string.
|
|
1847
|
+
|
|
1848
|
+
## Structural Rules
|
|
1849
|
+
|
|
1850
|
+
1. Step IDs must be unique and match /^[a-zA-Z_][a-zA-Z0-9_]+$/.
|
|
1851
|
+
2. Steps link via nextStepId. Omitting nextStepId ends the chain.
|
|
1852
|
+
3. Branch body chains (switch-case) and loop body chains (for-each) must terminate — their last step must NOT have a nextStepId. Do NOT point them back to the parent or outside the body.
|
|
1853
|
+
4. Only reference step IDs of steps that will have executed before the current step (no forward references).
|
|
1854
|
+
5. Do not create cycles (for-each handles iteration — you do not need to loop manually).
|
|
1855
|
+
6. end steps must NOT have a nextStepId.
|
|
1856
|
+
|
|
1857
|
+
## Available Tools
|
|
1858
|
+
|
|
1859
|
+
Each tool below includes an \`outputSchema\` describing the shape of its return value. Use the output schema to construct correct JMESPath expressions. For example, if a tool returns \`{ "type": "object", "properties": { "orders": { "type": "array", ... } } }\`, reference the array as \`step_id.orders\`, not \`step_id\` alone.
|
|
1860
|
+
|
|
1861
|
+
${serializedTools}
|
|
1862
|
+
|
|
1863
|
+
## Common Mistakes
|
|
1864
|
+
|
|
1865
|
+
1. NEVER use bare primitives in toolInput. ALL values must be expression objects.
|
|
1866
|
+
WRONG: \`{ "email": "user@example.com" }\`
|
|
1867
|
+
RIGHT: \`{ "email": { "type": "literal", "value": "user@example.com" } }\`
|
|
1868
|
+
|
|
1869
|
+
2. Do NOT give end steps a nextStepId.
|
|
1870
|
+
|
|
1871
|
+
3. Branch/loop body chains must terminate (last step has no nextStepId). Do NOT point them outside their scope.
|
|
1872
|
+
|
|
1873
|
+
4. JMESPath expressions reference step outputs by step ID as the root identifier. Example: \`"get_orders.orders[0].id"\` means step "get_orders" → its output → .orders[0].id
|
|
1874
|
+
|
|
1875
|
+
5. For-each itemName is a scoped variable accessible ONLY within the loop body steps.
|
|
1876
|
+
|
|
1877
|
+
6. Step IDs must be at least 2 characters long.
|
|
1878
|
+
|
|
1879
|
+
7. for-each target must resolve to an ARRAY. Check the tool's outputSchema to determine the correct path.
|
|
1880
|
+
WRONG: \`"target": { "type": "jmespath", "expression": "get_orders" }\` (when get_orders returns an object with an \`orders\` array property)
|
|
1881
|
+
RIGHT: \`"target": { "type": "jmespath", "expression": "get_orders.orders" }\`
|
|
1882
|
+
|
|
1883
|
+
8. If the workflow needs to return structured data, declare an \`outputSchema\` on the workflow and give every \`end\` step an \`output\` expression that evaluates to a value matching it.`;
|
|
1884
|
+
}
|
|
1885
|
+
function formatDiagnostics(diagnostics) {
|
|
1886
|
+
const errors = diagnostics.filter((d) => d.severity === "error");
|
|
1887
|
+
if (errors.length === 0)
|
|
1888
|
+
return "No errors.";
|
|
1889
|
+
return errors.map((d) => {
|
|
1890
|
+
const loc = d.location ? ` (at step ${d.location.stepId}${d.location.field ? `, field ${d.location.field}` : ""})` : "";
|
|
1891
|
+
return `- [${d.code}] ${d.message}${loc}`;
|
|
1892
|
+
}).join(`
|
|
1893
|
+
`);
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
// src/generator/index.ts
|
|
1897
|
+
async function generateWorkflow(options) {
|
|
1898
|
+
const { model, tools, task, maxRetries = 3 } = options;
|
|
1899
|
+
const missingOutputSchema = Object.entries(tools).filter(([_, t]) => !t.outputSchema).map(([name]) => name);
|
|
1900
|
+
if (missingOutputSchema.length > 0) {
|
|
1901
|
+
throw new Error(`All tools must have an outputSchema. Missing: ${missingOutputSchema.join(", ")}`);
|
|
1902
|
+
}
|
|
1903
|
+
const serializedTools = await serializeToolsForPrompt(tools);
|
|
1904
|
+
const systemPrompt = buildWorkflowGenerationPrompt(serializedTools);
|
|
1905
|
+
let successWorkflow = null;
|
|
1906
|
+
let lastDiagnostics = [];
|
|
1907
|
+
let attempts = 0;
|
|
1908
|
+
const createWorkflowTool = tool({
|
|
1909
|
+
description: "Create a workflow definition",
|
|
1910
|
+
inputSchema: workflowDefinitionSchema,
|
|
1911
|
+
execute: async (workflowDef) => {
|
|
1912
|
+
attempts++;
|
|
1913
|
+
const result = await compileWorkflow(workflowDef, { tools });
|
|
1914
|
+
lastDiagnostics = result.diagnostics;
|
|
1915
|
+
const errors = result.diagnostics.filter((d) => d.severity === "error");
|
|
1916
|
+
if (errors.length > 0) {
|
|
1917
|
+
return {
|
|
1918
|
+
success: false,
|
|
1919
|
+
errors: formatDiagnostics(result.diagnostics)
|
|
1920
|
+
};
|
|
1921
|
+
}
|
|
1922
|
+
successWorkflow = result.workflow ?? workflowDef;
|
|
1923
|
+
return { success: true };
|
|
1924
|
+
}
|
|
1925
|
+
});
|
|
1926
|
+
await generateText({
|
|
1927
|
+
model,
|
|
1928
|
+
system: systemPrompt,
|
|
1929
|
+
prompt: `Create a workflow to accomplish the following task:
|
|
1930
|
+
|
|
1931
|
+
${task}`,
|
|
1932
|
+
tools: { createWorkflow: createWorkflowTool },
|
|
1933
|
+
toolChoice: { type: "tool", toolName: "createWorkflow" },
|
|
1934
|
+
stopWhen: [stepCountIs2(maxRetries + 1), () => successWorkflow !== null]
|
|
1935
|
+
});
|
|
1936
|
+
return {
|
|
1937
|
+
workflow: successWorkflow,
|
|
1938
|
+
diagnostics: lastDiagnostics,
|
|
1939
|
+
attempts
|
|
1940
|
+
};
|
|
1941
|
+
}
|
|
1942
|
+
function createWorkflowGeneratorTool(options) {
|
|
1943
|
+
const { model, tools: baseTools, maxRetries } = options;
|
|
1944
|
+
return tool({
|
|
1945
|
+
description: "Generate a validated workflow definition from a natural language task description",
|
|
1946
|
+
inputSchema: arktype({ task: "string" }),
|
|
1947
|
+
execute: async ({ task }) => {
|
|
1948
|
+
return generateWorkflow({
|
|
1949
|
+
model,
|
|
1950
|
+
tools: baseTools ?? {},
|
|
1951
|
+
task,
|
|
1952
|
+
maxRetries
|
|
1953
|
+
});
|
|
1954
|
+
}
|
|
1955
|
+
});
|
|
1956
|
+
}
|
|
1957
|
+
export {
|
|
1958
|
+
workflowDefinitionSchema,
|
|
1959
|
+
serializeToolsForPrompt,
|
|
1960
|
+
generateWorkflow,
|
|
1961
|
+
executeWorkflow,
|
|
1962
|
+
createWorkflowGeneratorTool,
|
|
1963
|
+
compileWorkflow,
|
|
1964
|
+
buildWorkflowGenerationPrompt,
|
|
1965
|
+
ValidationError,
|
|
1966
|
+
StepExecutionError,
|
|
1967
|
+
OutputQualityError,
|
|
1968
|
+
ExternalServiceError,
|
|
1969
|
+
ExpressionError,
|
|
1970
|
+
ConfigurationError
|
|
1971
|
+
};
|
|
1972
|
+
|
|
1973
|
+
//# debugId=DD448656596BC50E64756E2164756E21
|