@remoraflow/core 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 +110 -0
- package/dist/compiler/index.d.ts +25 -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 +4 -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-limits.d.ts +9 -0
- package/dist/compiler/passes/validate-limits.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 +100 -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/compiler/utils/schema.d.ts +33 -0
- package/dist/compiler/utils/schema.d.ts.map +1 -0
- package/dist/example-tasks.d.ts +834 -0
- package/dist/example-tasks.d.ts.map +1 -0
- package/dist/executor/context.d.ts +42 -0
- package/dist/executor/context.d.ts.map +1 -0
- package/dist/executor/errors.d.ts +57 -0
- package/dist/executor/errors.d.ts.map +1 -0
- package/dist/executor/executor-types.d.ts +117 -0
- package/dist/executor/executor-types.d.ts.map +1 -0
- package/dist/executor/helpers.d.ts +21 -0
- package/dist/executor/helpers.d.ts.map +1 -0
- package/dist/executor/index.d.ts +16 -0
- package/dist/executor/index.d.ts.map +1 -0
- package/dist/executor/schema-inference.d.ts +3 -0
- package/dist/executor/schema-inference.d.ts.map +1 -0
- package/dist/executor/state.d.ts +292 -0
- package/dist/executor/state.d.ts.map +1 -0
- package/dist/executor/steps/agent-loop.d.ts +15 -0
- package/dist/executor/steps/agent-loop.d.ts.map +1 -0
- package/dist/executor/steps/end.d.ts +5 -0
- package/dist/executor/steps/end.d.ts.map +1 -0
- package/dist/executor/steps/extract-data.d.ts +15 -0
- package/dist/executor/steps/extract-data.d.ts.map +1 -0
- package/dist/executor/steps/for-each.d.ts +11 -0
- package/dist/executor/steps/for-each.d.ts.map +1 -0
- package/dist/executor/steps/llm-prompt.d.ts +13 -0
- package/dist/executor/steps/llm-prompt.d.ts.map +1 -0
- package/dist/executor/steps/sleep.d.ts +7 -0
- package/dist/executor/steps/sleep.d.ts.map +1 -0
- package/dist/executor/steps/start.d.ts +2 -0
- package/dist/executor/steps/start.d.ts.map +1 -0
- package/dist/executor/steps/switch-case.d.ts +11 -0
- package/dist/executor/steps/switch-case.d.ts.map +1 -0
- package/dist/executor/steps/tool-call.d.ts +9 -0
- package/dist/executor/steps/tool-call.d.ts.map +1 -0
- package/dist/executor/steps/wait-for-condition.d.ts +8 -0
- package/dist/executor/steps/wait-for-condition.d.ts.map +1 -0
- package/dist/generator/index.d.ts +59 -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 +15 -0
- package/dist/lib.d.ts.map +1 -0
- package/dist/lib.js +3566 -0
- package/dist/lib.js.map +42 -0
- package/dist/types.d.ts +481 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +54 -0
package/dist/lib.js
ADDED
|
@@ -0,0 +1,3566 @@
|
|
|
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
|
+
nextStepId: oldInitialStepId
|
|
30
|
+
};
|
|
31
|
+
workflow.steps.unshift(startStep);
|
|
32
|
+
workflow.initialStepId = startStepId;
|
|
33
|
+
diagnostics.push({
|
|
34
|
+
severity: "warning",
|
|
35
|
+
location: { stepId: null, field: "initialStepId" },
|
|
36
|
+
message: "Workflow has no start step; one was automatically added with an empty input schema",
|
|
37
|
+
code: "MISSING_START_STEP"
|
|
38
|
+
});
|
|
39
|
+
return { workflow, diagnostics };
|
|
40
|
+
}
|
|
41
|
+
function addMissingEndSteps(workflow, _graph) {
|
|
42
|
+
const newEndSteps = [];
|
|
43
|
+
for (const step of workflow.steps) {
|
|
44
|
+
if (step.type === "end")
|
|
45
|
+
continue;
|
|
46
|
+
if (step.nextStepId)
|
|
47
|
+
continue;
|
|
48
|
+
const endStepId = `${step.id}_end`;
|
|
49
|
+
step.nextStepId = endStepId;
|
|
50
|
+
newEndSteps.push({
|
|
51
|
+
id: endStepId,
|
|
52
|
+
name: "End",
|
|
53
|
+
description: `End of chain after ${step.id}`,
|
|
54
|
+
type: "end"
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
workflow.steps.push(...newEndSteps);
|
|
58
|
+
return { workflow, diagnostics: [] };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// src/compiler/utils/graph.ts
|
|
62
|
+
function buildStepIndex(steps) {
|
|
63
|
+
const index = new Map;
|
|
64
|
+
const duplicates = [];
|
|
65
|
+
for (const step of steps) {
|
|
66
|
+
if (index.has(step.id)) {
|
|
67
|
+
duplicates.push(step.id);
|
|
68
|
+
} else {
|
|
69
|
+
index.set(step.id, step);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return { index, duplicates };
|
|
73
|
+
}
|
|
74
|
+
function computeSuccessors(stepIndex) {
|
|
75
|
+
const successors = new Map;
|
|
76
|
+
for (const [id, step] of stepIndex) {
|
|
77
|
+
const succs = new Set;
|
|
78
|
+
if (step.nextStepId) {
|
|
79
|
+
succs.add(step.nextStepId);
|
|
80
|
+
}
|
|
81
|
+
if (step.type === "switch-case") {
|
|
82
|
+
for (const c of step.params.cases) {
|
|
83
|
+
succs.add(c.branchBodyStepId);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (step.type === "for-each") {
|
|
87
|
+
succs.add(step.params.loopBodyStepId);
|
|
88
|
+
}
|
|
89
|
+
if (step.type === "wait-for-condition") {
|
|
90
|
+
succs.add(step.params.conditionStepId);
|
|
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
|
+
if (step.type === "wait-for-condition") {
|
|
258
|
+
walkChain(step.params.conditionStepId, loopVars, step.id);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
walkChain(initialStepId, new Set, null);
|
|
263
|
+
return { loopVariablesInScope, bodyOwnership };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// src/compiler/passes/build-graph.ts
|
|
267
|
+
function buildGraph(workflow) {
|
|
268
|
+
const diagnostics = [];
|
|
269
|
+
const VALID_STEP_ID = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
270
|
+
for (const step of workflow.steps) {
|
|
271
|
+
if (!VALID_STEP_ID.test(step.id)) {
|
|
272
|
+
diagnostics.push({
|
|
273
|
+
severity: "error",
|
|
274
|
+
location: { stepId: step.id, field: "id" },
|
|
275
|
+
message: `Step ID '${step.id}' is invalid — must match [a-zA-Z_][a-zA-Z0-9_]* (use underscores, not hyphens)`,
|
|
276
|
+
code: "INVALID_STEP_ID"
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
const stepIdSet = new Set(workflow.steps.map((s) => s.id));
|
|
281
|
+
for (const step of workflow.steps) {
|
|
282
|
+
if (step.type === "for-each") {
|
|
283
|
+
const { itemName } = step.params;
|
|
284
|
+
if (!VALID_STEP_ID.test(itemName)) {
|
|
285
|
+
diagnostics.push({
|
|
286
|
+
severity: "error",
|
|
287
|
+
location: { stepId: step.id, field: "params.itemName" },
|
|
288
|
+
message: `Item name '${itemName}' is invalid — must match [a-zA-Z_][a-zA-Z0-9_]*`,
|
|
289
|
+
code: "INVALID_ITEM_NAME"
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
if (stepIdSet.has(itemName)) {
|
|
293
|
+
diagnostics.push({
|
|
294
|
+
severity: "warning",
|
|
295
|
+
location: { stepId: step.id, field: "params.itemName" },
|
|
296
|
+
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`,
|
|
297
|
+
code: "ITEM_NAME_SHADOWS_STEP_ID"
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
const { index: stepIndex, duplicates } = buildStepIndex(workflow.steps);
|
|
303
|
+
for (const dupId of duplicates) {
|
|
304
|
+
diagnostics.push({
|
|
305
|
+
severity: "error",
|
|
306
|
+
location: { stepId: dupId, field: "id" },
|
|
307
|
+
message: `Duplicate step ID '${dupId}'`,
|
|
308
|
+
code: "DUPLICATE_STEP_ID"
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
if (!stepIndex.has(workflow.initialStepId)) {
|
|
312
|
+
diagnostics.push({
|
|
313
|
+
severity: "error",
|
|
314
|
+
location: { stepId: null, field: "initialStepId" },
|
|
315
|
+
message: `Initial step '${workflow.initialStepId}' does not exist`,
|
|
316
|
+
code: "MISSING_INITIAL_STEP"
|
|
317
|
+
});
|
|
318
|
+
return { graph: null, diagnostics };
|
|
319
|
+
}
|
|
320
|
+
if (duplicates.length > 0) {
|
|
321
|
+
return { graph: null, diagnostics };
|
|
322
|
+
}
|
|
323
|
+
const successors = computeSuccessors(stepIndex);
|
|
324
|
+
const cycles = detectCycles(stepIndex, successors);
|
|
325
|
+
for (const cycle of cycles) {
|
|
326
|
+
const firstStep = cycle[0];
|
|
327
|
+
if (!firstStep)
|
|
328
|
+
continue;
|
|
329
|
+
diagnostics.push({
|
|
330
|
+
severity: "error",
|
|
331
|
+
location: { stepId: firstStep, field: "nextStepId" },
|
|
332
|
+
message: `Cycle detected: ${cycle.join(" → ")} → ${firstStep}`,
|
|
333
|
+
code: "CYCLE_DETECTED"
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
const reachableSteps = computeReachability(workflow.initialStepId, successors);
|
|
337
|
+
for (const [id] of stepIndex) {
|
|
338
|
+
if (!reachableSteps.has(id)) {
|
|
339
|
+
diagnostics.push({
|
|
340
|
+
severity: "warning",
|
|
341
|
+
location: { stepId: id, field: "id" },
|
|
342
|
+
message: `Step '${id}' is not reachable from initial step '${workflow.initialStepId}'`,
|
|
343
|
+
code: "UNREACHABLE_STEP"
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (cycles.length > 0) {
|
|
348
|
+
return { graph: null, diagnostics };
|
|
349
|
+
}
|
|
350
|
+
const reachableIds = [...reachableSteps];
|
|
351
|
+
const topologicalOrder = topologicalSort(reachableIds, successors);
|
|
352
|
+
if (!topologicalOrder) {
|
|
353
|
+
return { graph: null, diagnostics };
|
|
354
|
+
}
|
|
355
|
+
const predecessors = computePredecessors(topologicalOrder, successors);
|
|
356
|
+
const { loopVariablesInScope, bodyOwnership } = computeLoopScopesAndOwnership(workflow.initialStepId, stepIndex);
|
|
357
|
+
return {
|
|
358
|
+
graph: {
|
|
359
|
+
stepIndex,
|
|
360
|
+
successors,
|
|
361
|
+
predecessors,
|
|
362
|
+
topologicalOrder,
|
|
363
|
+
reachableSteps,
|
|
364
|
+
loopVariablesInScope,
|
|
365
|
+
bodyOwnership
|
|
366
|
+
},
|
|
367
|
+
diagnostics
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// src/compiler/passes/generate-constrained-tool-schemas.ts
|
|
372
|
+
function collectCallSites(workflow) {
|
|
373
|
+
const callSitesByTool = new Map;
|
|
374
|
+
for (const step of workflow.steps) {
|
|
375
|
+
if (step.type !== "tool-call")
|
|
376
|
+
continue;
|
|
377
|
+
const { toolName, toolInput } = step.params;
|
|
378
|
+
const keys = new Map;
|
|
379
|
+
for (const [key, expr] of Object.entries(toolInput)) {
|
|
380
|
+
const expression = expr;
|
|
381
|
+
if (expression.type === "literal") {
|
|
382
|
+
keys.set(key, { type: "literal", value: expression.value });
|
|
383
|
+
} else {
|
|
384
|
+
keys.set(key, { type: "jmespath" });
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
let sites = callSitesByTool.get(toolName);
|
|
388
|
+
if (!sites) {
|
|
389
|
+
sites = [];
|
|
390
|
+
callSitesByTool.set(toolName, sites);
|
|
391
|
+
}
|
|
392
|
+
sites.push({ stepId: step.id, keys });
|
|
393
|
+
}
|
|
394
|
+
return callSitesByTool;
|
|
395
|
+
}
|
|
396
|
+
function constrainProperty(callSites, key, originalPropertySchema) {
|
|
397
|
+
let providedCount = 0;
|
|
398
|
+
let allLiteral = true;
|
|
399
|
+
const literalValues = [];
|
|
400
|
+
const seen = new Set;
|
|
401
|
+
for (const site of callSites) {
|
|
402
|
+
const entry = site.keys.get(key);
|
|
403
|
+
if (!entry)
|
|
404
|
+
continue;
|
|
405
|
+
providedCount++;
|
|
406
|
+
if (entry.type === "jmespath") {
|
|
407
|
+
allLiteral = false;
|
|
408
|
+
} else {
|
|
409
|
+
const serialized = JSON.stringify(entry.value);
|
|
410
|
+
if (!seen.has(serialized)) {
|
|
411
|
+
seen.add(serialized);
|
|
412
|
+
literalValues.push(entry.value);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
const providedInAll = providedCount === callSites.length;
|
|
417
|
+
if (!allLiteral) {
|
|
418
|
+
return { schema: originalPropertySchema, allLiteral: false, providedInAll };
|
|
419
|
+
}
|
|
420
|
+
const original = originalPropertySchema && typeof originalPropertySchema === "object" ? originalPropertySchema : {};
|
|
421
|
+
if (literalValues.length === 1) {
|
|
422
|
+
return {
|
|
423
|
+
schema: { ...original, const: literalValues[0] },
|
|
424
|
+
allLiteral: true,
|
|
425
|
+
providedInAll
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
return {
|
|
429
|
+
schema: { ...original, enum: literalValues },
|
|
430
|
+
allLiteral: true,
|
|
431
|
+
providedInAll
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
function generateConstrainedToolSchemas(workflow, tools) {
|
|
435
|
+
const callSitesByTool = collectCallSites(workflow);
|
|
436
|
+
const result = {};
|
|
437
|
+
for (const [toolName, callSites] of callSitesByTool) {
|
|
438
|
+
const toolDef = tools[toolName];
|
|
439
|
+
if (!toolDef)
|
|
440
|
+
continue;
|
|
441
|
+
const originalProperties = toolDef.inputSchema.properties ?? {};
|
|
442
|
+
const allUsedKeys = new Set;
|
|
443
|
+
for (const site of callSites) {
|
|
444
|
+
for (const key of site.keys.keys()) {
|
|
445
|
+
allUsedKeys.add(key);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
const constrainedProperties = {};
|
|
449
|
+
const requiredKeys = [];
|
|
450
|
+
let fullyStatic = true;
|
|
451
|
+
for (const key of allUsedKeys) {
|
|
452
|
+
const originalProp = originalProperties[key];
|
|
453
|
+
if (originalProp === undefined)
|
|
454
|
+
continue;
|
|
455
|
+
const { schema, allLiteral, providedInAll } = constrainProperty(callSites, key, originalProp);
|
|
456
|
+
constrainedProperties[key] = schema;
|
|
457
|
+
if (providedInAll) {
|
|
458
|
+
requiredKeys.push(key);
|
|
459
|
+
}
|
|
460
|
+
if (!allLiteral) {
|
|
461
|
+
fullyStatic = false;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
const constrained = {
|
|
465
|
+
inputSchema: {
|
|
466
|
+
required: requiredKeys.sort(),
|
|
467
|
+
properties: constrainedProperties
|
|
468
|
+
},
|
|
469
|
+
fullyStatic,
|
|
470
|
+
callSites: callSites.map((s) => s.stepId).sort()
|
|
471
|
+
};
|
|
472
|
+
if (toolDef.outputSchema) {
|
|
473
|
+
constrained.outputSchema = toolDef.outputSchema;
|
|
474
|
+
}
|
|
475
|
+
result[toolName] = constrained;
|
|
476
|
+
}
|
|
477
|
+
for (const step of workflow.steps) {
|
|
478
|
+
if (step.type !== "agent-loop")
|
|
479
|
+
continue;
|
|
480
|
+
for (const toolName of step.params.tools) {
|
|
481
|
+
if (result[toolName]) {
|
|
482
|
+
result[toolName].fullyStatic = false;
|
|
483
|
+
if (!result[toolName].callSites.includes(step.id)) {
|
|
484
|
+
result[toolName].callSites.push(step.id);
|
|
485
|
+
result[toolName].callSites.sort();
|
|
486
|
+
}
|
|
487
|
+
} else if (tools[toolName]) {
|
|
488
|
+
const toolDef = tools[toolName];
|
|
489
|
+
const constrained = {
|
|
490
|
+
inputSchema: {
|
|
491
|
+
required: [],
|
|
492
|
+
properties: {}
|
|
493
|
+
},
|
|
494
|
+
fullyStatic: false,
|
|
495
|
+
callSites: [step.id]
|
|
496
|
+
};
|
|
497
|
+
if (toolDef.outputSchema) {
|
|
498
|
+
constrained.outputSchema = toolDef.outputSchema;
|
|
499
|
+
}
|
|
500
|
+
result[toolName] = constrained;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
return result;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// src/compiler/utils/schema.ts
|
|
508
|
+
function parseSimplePath(expression) {
|
|
509
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)*$/.test(expression)) {
|
|
510
|
+
return null;
|
|
511
|
+
}
|
|
512
|
+
return expression.split(".");
|
|
513
|
+
}
|
|
514
|
+
function resolvePath(schema, path) {
|
|
515
|
+
let current = schema;
|
|
516
|
+
for (const segment of path) {
|
|
517
|
+
const properties = current.properties;
|
|
518
|
+
if (!properties?.[segment])
|
|
519
|
+
return null;
|
|
520
|
+
current = properties[segment];
|
|
521
|
+
}
|
|
522
|
+
return current;
|
|
523
|
+
}
|
|
524
|
+
function getSchemaType(schema) {
|
|
525
|
+
if (typeof schema.type === "string")
|
|
526
|
+
return schema.type;
|
|
527
|
+
return null;
|
|
528
|
+
}
|
|
529
|
+
function findArrayProperties(schema) {
|
|
530
|
+
const properties = schema.properties;
|
|
531
|
+
if (!properties)
|
|
532
|
+
return [];
|
|
533
|
+
return Object.entries(properties).filter(([_, propSchema]) => propSchema.type === "array").map(([name]) => name);
|
|
534
|
+
}
|
|
535
|
+
function getStepOutputSchema(step, tools, workflow, graph) {
|
|
536
|
+
switch (step.type) {
|
|
537
|
+
case "tool-call": {
|
|
538
|
+
if (!tools)
|
|
539
|
+
return null;
|
|
540
|
+
const toolDef = tools[step.params.toolName];
|
|
541
|
+
return toolDef?.outputSchema ?? null;
|
|
542
|
+
}
|
|
543
|
+
case "llm-prompt":
|
|
544
|
+
case "extract-data":
|
|
545
|
+
case "agent-loop":
|
|
546
|
+
return step.params.outputFormat ?? null;
|
|
547
|
+
case "for-each": {
|
|
548
|
+
const bodyItemSchema = resolveChainOutputSchema(step.params.loopBodyStepId, tools, workflow, graph);
|
|
549
|
+
if (!bodyItemSchema)
|
|
550
|
+
return null;
|
|
551
|
+
return { type: "array", items: bodyItemSchema };
|
|
552
|
+
}
|
|
553
|
+
case "switch-case":
|
|
554
|
+
return null;
|
|
555
|
+
case "start":
|
|
556
|
+
return null;
|
|
557
|
+
case "end":
|
|
558
|
+
return null;
|
|
559
|
+
default:
|
|
560
|
+
return null;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
function resolveChainOutputSchema(startId, tools, workflow, graph) {
|
|
564
|
+
let currentId = startId;
|
|
565
|
+
const visited = new Set;
|
|
566
|
+
while (currentId) {
|
|
567
|
+
if (visited.has(currentId))
|
|
568
|
+
break;
|
|
569
|
+
visited.add(currentId);
|
|
570
|
+
const step = graph.stepIndex.get(currentId);
|
|
571
|
+
if (!step)
|
|
572
|
+
break;
|
|
573
|
+
if (!step.nextStepId) {
|
|
574
|
+
return getStepOutputSchema(step, tools, workflow, graph);
|
|
575
|
+
}
|
|
576
|
+
currentId = step.nextStepId;
|
|
577
|
+
}
|
|
578
|
+
return null;
|
|
579
|
+
}
|
|
580
|
+
function resolveExpressionSchema(expression, _endStepId, tools, workflow, graph) {
|
|
581
|
+
const segments = parseSimplePath(expression);
|
|
582
|
+
if (!segments)
|
|
583
|
+
return null;
|
|
584
|
+
const [rootId, ...fieldPath] = segments;
|
|
585
|
+
if (!rootId)
|
|
586
|
+
return null;
|
|
587
|
+
let rootSchema = null;
|
|
588
|
+
if (rootId === "input" && workflow.inputSchema) {
|
|
589
|
+
rootSchema = workflow.inputSchema;
|
|
590
|
+
} else {
|
|
591
|
+
const referencedStep = graph.stepIndex.get(rootId);
|
|
592
|
+
if (!referencedStep)
|
|
593
|
+
return null;
|
|
594
|
+
rootSchema = getStepOutputSchema(referencedStep, tools, workflow, graph);
|
|
595
|
+
}
|
|
596
|
+
if (!rootSchema)
|
|
597
|
+
return null;
|
|
598
|
+
if (fieldPath.length === 0)
|
|
599
|
+
return rootSchema;
|
|
600
|
+
return resolvePath(rootSchema, fieldPath);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// src/compiler/passes/validate-control-flow.ts
|
|
604
|
+
function validateControlFlow(workflow, graph, tools) {
|
|
605
|
+
const diagnostics = [];
|
|
606
|
+
for (const step of workflow.steps) {
|
|
607
|
+
if (step.type === "end" && step.nextStepId) {
|
|
608
|
+
diagnostics.push({
|
|
609
|
+
severity: "error",
|
|
610
|
+
location: { stepId: step.id, field: "nextStepId" },
|
|
611
|
+
message: `End step '${step.id}' should not have a nextStepId`,
|
|
612
|
+
code: "END_STEP_HAS_NEXT"
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
if (step.type === "switch-case") {
|
|
616
|
+
const defaultCount = step.params.cases.filter((c) => c.value.type === "default").length;
|
|
617
|
+
if (defaultCount > 1) {
|
|
618
|
+
diagnostics.push({
|
|
619
|
+
severity: "error",
|
|
620
|
+
location: { stepId: step.id, field: "params.cases" },
|
|
621
|
+
message: `Switch-case step '${step.id}' has ${defaultCount} default cases (expected at most 1)`,
|
|
622
|
+
code: "MULTIPLE_DEFAULT_CASES"
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
for (const [i, c] of step.params.cases.entries()) {
|
|
626
|
+
const escapes = checkBodyEscapes(c.branchBodyStepId, step.id, graph);
|
|
627
|
+
if (escapes) {
|
|
628
|
+
diagnostics.push({
|
|
629
|
+
severity: "warning",
|
|
630
|
+
location: {
|
|
631
|
+
stepId: step.id,
|
|
632
|
+
field: `params.cases[${i}].branchBodyStepId`
|
|
633
|
+
},
|
|
634
|
+
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}'`,
|
|
635
|
+
code: "BRANCH_BODY_ESCAPES"
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
if (step.type === "for-each") {
|
|
641
|
+
const escapes = checkBodyEscapes(step.params.loopBodyStepId, step.id, graph);
|
|
642
|
+
if (escapes) {
|
|
643
|
+
diagnostics.push({
|
|
644
|
+
severity: "warning",
|
|
645
|
+
location: {
|
|
646
|
+
stepId: step.id,
|
|
647
|
+
field: "params.loopBodyStepId"
|
|
648
|
+
},
|
|
649
|
+
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}'`,
|
|
650
|
+
code: "LOOP_BODY_ESCAPES"
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
if (step.type === "wait-for-condition") {
|
|
655
|
+
const escapes = checkBodyEscapes(step.params.conditionStepId, step.id, graph);
|
|
656
|
+
if (escapes) {
|
|
657
|
+
diagnostics.push({
|
|
658
|
+
severity: "warning",
|
|
659
|
+
location: {
|
|
660
|
+
stepId: step.id,
|
|
661
|
+
field: "params.conditionStepId"
|
|
662
|
+
},
|
|
663
|
+
message: `Condition body starting at '${step.params.conditionStepId}' in step '${step.id}' has a step ('${escapes.escapingStep}') whose nextStepId points outside the condition body to '${escapes.target}'`,
|
|
664
|
+
code: "CONDITION_BODY_ESCAPES"
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
if (workflow.outputSchema) {
|
|
670
|
+
const outputPoints = findWorkflowOutputPoints(workflow.initialStepId, graph);
|
|
671
|
+
for (const point of outputPoints) {
|
|
672
|
+
if (point.type === "missing_end") {
|
|
673
|
+
diagnostics.push({
|
|
674
|
+
severity: "error",
|
|
675
|
+
location: { stepId: point.stepId, field: "nextStepId" },
|
|
676
|
+
message: `Step '${point.stepId}' is a terminal step but is not an end step; all execution paths must terminate at an end step when outputSchema is declared`,
|
|
677
|
+
code: "PATH_MISSING_END_STEP"
|
|
678
|
+
});
|
|
679
|
+
} else if (point.type === "end_without_output") {
|
|
680
|
+
diagnostics.push({
|
|
681
|
+
severity: "error",
|
|
682
|
+
location: { stepId: point.stepId, field: "params" },
|
|
683
|
+
message: `End step '${point.stepId}' has no output expression, but the workflow declares an outputSchema`,
|
|
684
|
+
code: "END_STEP_MISSING_OUTPUT"
|
|
685
|
+
});
|
|
686
|
+
} else if (point.type === "end_with_output") {
|
|
687
|
+
const step = graph.stepIndex.get(point.stepId);
|
|
688
|
+
if (step?.type === "end" && step.params?.output) {
|
|
689
|
+
const expr = step.params.output;
|
|
690
|
+
const outputSchema = workflow.outputSchema;
|
|
691
|
+
if (expr.type === "literal") {
|
|
692
|
+
diagnostics.push(...validateOutputShapeMismatch(expr.value, outputSchema, point.stepId));
|
|
693
|
+
} else if (expr.type === "jmespath") {
|
|
694
|
+
const resolvedSchema = resolveExpressionSchema(expr.expression, point.stepId, tools ?? null, workflow, graph);
|
|
695
|
+
if (resolvedSchema) {
|
|
696
|
+
diagnostics.push(...validateSchemaCompatibility(resolvedSchema, outputSchema, point.stepId, expr.expression));
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
} else {
|
|
703
|
+
for (const step of workflow.steps) {
|
|
704
|
+
if (step.type === "end" && step.params?.output) {
|
|
705
|
+
diagnostics.push({
|
|
706
|
+
severity: "warning",
|
|
707
|
+
location: { stepId: step.id, field: "params.output" },
|
|
708
|
+
message: `End step '${step.id}' has an output expression, but the workflow does not declare an outputSchema`,
|
|
709
|
+
code: "END_STEP_UNEXPECTED_OUTPUT"
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
return diagnostics;
|
|
715
|
+
}
|
|
716
|
+
function checkBodyEscapes(startId, parentStepId, graph) {
|
|
717
|
+
let currentId = startId;
|
|
718
|
+
const visited = new Set;
|
|
719
|
+
while (currentId) {
|
|
720
|
+
if (visited.has(currentId))
|
|
721
|
+
break;
|
|
722
|
+
visited.add(currentId);
|
|
723
|
+
const step = graph.stepIndex.get(currentId);
|
|
724
|
+
if (!step)
|
|
725
|
+
break;
|
|
726
|
+
if (step.nextStepId) {
|
|
727
|
+
const nextOwner = graph.bodyOwnership.get(step.nextStepId);
|
|
728
|
+
const currentOwner = graph.bodyOwnership.get(currentId);
|
|
729
|
+
if ((currentOwner === parentStepId || currentId === startId) && nextOwner !== parentStepId) {
|
|
730
|
+
return { escapingStep: currentId, target: step.nextStepId };
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
currentId = step.nextStepId;
|
|
734
|
+
}
|
|
735
|
+
return null;
|
|
736
|
+
}
|
|
737
|
+
function findWorkflowOutputPoints(startId, graph) {
|
|
738
|
+
return collectOutputPoints(startId, graph, new Set);
|
|
739
|
+
}
|
|
740
|
+
function collectOutputPoints(startId, graph, visited) {
|
|
741
|
+
const points = [];
|
|
742
|
+
let currentId = startId;
|
|
743
|
+
while (currentId) {
|
|
744
|
+
if (visited.has(currentId))
|
|
745
|
+
break;
|
|
746
|
+
visited.add(currentId);
|
|
747
|
+
const step = graph.stepIndex.get(currentId);
|
|
748
|
+
if (!step)
|
|
749
|
+
break;
|
|
750
|
+
if (!step.nextStepId) {
|
|
751
|
+
if (step.type === "end") {
|
|
752
|
+
points.push({
|
|
753
|
+
type: step.params?.output ? "end_with_output" : "end_without_output",
|
|
754
|
+
stepId: step.id
|
|
755
|
+
});
|
|
756
|
+
} else if (step.type === "switch-case") {
|
|
757
|
+
for (const c of step.params.cases) {
|
|
758
|
+
points.push(...collectOutputPoints(c.branchBodyStepId, graph, visited));
|
|
759
|
+
}
|
|
760
|
+
} else if (step.type === "for-each") {
|
|
761
|
+
points.push(...collectOutputPoints(step.params.loopBodyStepId, graph, visited));
|
|
762
|
+
} else {
|
|
763
|
+
points.push({ type: "missing_end", stepId: step.id });
|
|
764
|
+
}
|
|
765
|
+
break;
|
|
766
|
+
}
|
|
767
|
+
currentId = step.nextStepId;
|
|
768
|
+
}
|
|
769
|
+
return points;
|
|
770
|
+
}
|
|
771
|
+
function validateOutputShapeMismatch(value, schema, stepId) {
|
|
772
|
+
const diagnostics = [];
|
|
773
|
+
const expectedType = schema.type;
|
|
774
|
+
if (typeof expectedType !== "string")
|
|
775
|
+
return diagnostics;
|
|
776
|
+
const actualType = getValueType(value);
|
|
777
|
+
if (expectedType !== actualType) {
|
|
778
|
+
diagnostics.push({
|
|
779
|
+
severity: "error",
|
|
780
|
+
location: { stepId, field: "params.output" },
|
|
781
|
+
message: `End step '${stepId}' output literal has type '${actualType}' but outputSchema expects '${expectedType}'`,
|
|
782
|
+
code: "LITERAL_OUTPUT_SHAPE_MISMATCH"
|
|
783
|
+
});
|
|
784
|
+
return diagnostics;
|
|
785
|
+
}
|
|
786
|
+
if (expectedType === "object" && typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
787
|
+
const obj = value;
|
|
788
|
+
const required = schema.required;
|
|
789
|
+
if (Array.isArray(required)) {
|
|
790
|
+
const missing = required.filter((key) => typeof key === "string" && !(key in obj));
|
|
791
|
+
if (missing.length > 0) {
|
|
792
|
+
diagnostics.push({
|
|
793
|
+
severity: "error",
|
|
794
|
+
location: { stepId, field: "params.output" },
|
|
795
|
+
message: `End step '${stepId}' output literal is missing required field(s): ${missing.join(", ")}`,
|
|
796
|
+
code: "LITERAL_OUTPUT_SHAPE_MISMATCH"
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
const properties = schema.properties;
|
|
801
|
+
if (properties && typeof properties === "object") {
|
|
802
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
803
|
+
const propSchema = properties[key];
|
|
804
|
+
if (propSchema && typeof propSchema === "object" && "type" in propSchema) {
|
|
805
|
+
const propExpected = propSchema.type;
|
|
806
|
+
const propActual = getValueType(val);
|
|
807
|
+
if (propExpected !== propActual) {
|
|
808
|
+
diagnostics.push({
|
|
809
|
+
severity: "error",
|
|
810
|
+
location: { stepId, field: "params.output" },
|
|
811
|
+
message: `End step '${stepId}' output literal field '${key}' has type '${propActual}' but schema expects '${propExpected}'`,
|
|
812
|
+
code: "LITERAL_OUTPUT_SHAPE_MISMATCH"
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
return diagnostics;
|
|
820
|
+
}
|
|
821
|
+
function getValueType(value) {
|
|
822
|
+
if (value === null)
|
|
823
|
+
return "null";
|
|
824
|
+
if (Array.isArray(value))
|
|
825
|
+
return "array";
|
|
826
|
+
return typeof value;
|
|
827
|
+
}
|
|
828
|
+
function validateSchemaCompatibility(resolvedSchema, outputSchema, stepId, expression) {
|
|
829
|
+
const diagnostics = [];
|
|
830
|
+
const expectedType = outputSchema.type;
|
|
831
|
+
const resolvedType = resolvedSchema.type;
|
|
832
|
+
if (typeof expectedType !== "string" || typeof resolvedType !== "string") {
|
|
833
|
+
return diagnostics;
|
|
834
|
+
}
|
|
835
|
+
if (expectedType !== resolvedType) {
|
|
836
|
+
diagnostics.push({
|
|
837
|
+
severity: "error",
|
|
838
|
+
location: { stepId, field: "params.output" },
|
|
839
|
+
message: `End step '${stepId}' output expression '${expression}' resolves to type '${resolvedType}' but outputSchema expects '${expectedType}'`,
|
|
840
|
+
code: "LITERAL_OUTPUT_SHAPE_MISMATCH"
|
|
841
|
+
});
|
|
842
|
+
return diagnostics;
|
|
843
|
+
}
|
|
844
|
+
if (expectedType === "object") {
|
|
845
|
+
const expectedRequired = outputSchema.required;
|
|
846
|
+
const resolvedProps = resolvedSchema.properties ?? {};
|
|
847
|
+
if (Array.isArray(expectedRequired)) {
|
|
848
|
+
const missing = expectedRequired.filter((key) => typeof key === "string" && !(key in resolvedProps));
|
|
849
|
+
if (missing.length > 0) {
|
|
850
|
+
diagnostics.push({
|
|
851
|
+
severity: "error",
|
|
852
|
+
location: { stepId, field: "params.output" },
|
|
853
|
+
message: `End step '${stepId}' output expression '${expression}' schema is missing required field(s): ${missing.join(", ")}`,
|
|
854
|
+
code: "LITERAL_OUTPUT_SHAPE_MISMATCH"
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
const expectedProps = outputSchema.properties ?? {};
|
|
859
|
+
for (const [key, expectedPropSchema] of Object.entries(expectedProps)) {
|
|
860
|
+
const resolvedPropSchema = resolvedProps[key];
|
|
861
|
+
if (!resolvedPropSchema)
|
|
862
|
+
continue;
|
|
863
|
+
const expectedPropType = expectedPropSchema.type;
|
|
864
|
+
const resolvedPropType = resolvedPropSchema.type;
|
|
865
|
+
if (typeof expectedPropType === "string" && typeof resolvedPropType === "string" && expectedPropType !== resolvedPropType) {
|
|
866
|
+
diagnostics.push({
|
|
867
|
+
severity: "error",
|
|
868
|
+
location: { stepId, field: "params.output" },
|
|
869
|
+
message: `End step '${stepId}' output expression '${expression}' field '${key}' has type '${resolvedPropType}' but outputSchema expects '${expectedPropType}'`,
|
|
870
|
+
code: "LITERAL_OUTPUT_SHAPE_MISMATCH"
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
return diagnostics;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// src/compiler/passes/validate-foreach-target.ts
|
|
879
|
+
function validateForeachTarget(workflow, graph, tools) {
|
|
880
|
+
const diagnostics = [];
|
|
881
|
+
for (const step of workflow.steps) {
|
|
882
|
+
if (step.type !== "for-each")
|
|
883
|
+
continue;
|
|
884
|
+
if (step.params.target.type !== "jmespath")
|
|
885
|
+
continue;
|
|
886
|
+
const expression = step.params.target.expression;
|
|
887
|
+
const segments = parseSimplePath(expression);
|
|
888
|
+
if (!segments)
|
|
889
|
+
continue;
|
|
890
|
+
const [rootId, ...fieldPath] = segments;
|
|
891
|
+
if (!rootId)
|
|
892
|
+
continue;
|
|
893
|
+
let outputSchema = null;
|
|
894
|
+
if (rootId === "input" && workflow.inputSchema) {
|
|
895
|
+
outputSchema = workflow.inputSchema;
|
|
896
|
+
} else {
|
|
897
|
+
const referencedStep = graph.stepIndex.get(rootId);
|
|
898
|
+
if (!referencedStep)
|
|
899
|
+
continue;
|
|
900
|
+
outputSchema = getStepOutputSchema(referencedStep, tools, workflow, graph);
|
|
901
|
+
}
|
|
902
|
+
if (!outputSchema)
|
|
903
|
+
continue;
|
|
904
|
+
const resolvedSchema = resolvePath(outputSchema, fieldPath);
|
|
905
|
+
if (!resolvedSchema)
|
|
906
|
+
continue;
|
|
907
|
+
const resolvedType = getSchemaType(resolvedSchema);
|
|
908
|
+
if (resolvedType === "array")
|
|
909
|
+
continue;
|
|
910
|
+
if (resolvedType === "object") {
|
|
911
|
+
const suggestions = findArrayProperties(resolvedSchema);
|
|
912
|
+
const hint = suggestions.length > 0 ? ` Did you mean ${suggestions.map((s) => `'${rootId}.${s}'`).join(" or ")}?` : "";
|
|
913
|
+
diagnostics.push({
|
|
914
|
+
severity: "error",
|
|
915
|
+
location: { stepId: step.id, field: "params.target" },
|
|
916
|
+
message: `for-each target '${expression}' resolves to an object, not an array.${hint}`,
|
|
917
|
+
code: "FOREACH_TARGET_NOT_ARRAY"
|
|
918
|
+
});
|
|
919
|
+
} else if (resolvedType !== null) {
|
|
920
|
+
diagnostics.push({
|
|
921
|
+
severity: "error",
|
|
922
|
+
location: { stepId: step.id, field: "params.target" },
|
|
923
|
+
message: `for-each target '${expression}' resolves to type '${resolvedType}', not an array.`,
|
|
924
|
+
code: "FOREACH_TARGET_NOT_ARRAY"
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
return diagnostics;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// src/compiler/utils/jmespath-helpers.ts
|
|
932
|
+
import { compile } from "@jmespath-community/jmespath";
|
|
933
|
+
function validateJmespathSyntax(expression) {
|
|
934
|
+
try {
|
|
935
|
+
compile(expression);
|
|
936
|
+
return { valid: true };
|
|
937
|
+
} catch (e) {
|
|
938
|
+
return { valid: false, error: e.message };
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
function extractRootIdentifiers(expression) {
|
|
942
|
+
let ast;
|
|
943
|
+
try {
|
|
944
|
+
ast = compile(expression);
|
|
945
|
+
} catch {
|
|
946
|
+
return [];
|
|
947
|
+
}
|
|
948
|
+
const roots = new Set;
|
|
949
|
+
collectRoots(ast, roots);
|
|
950
|
+
return [...roots];
|
|
951
|
+
}
|
|
952
|
+
function collectRoots(node, roots) {
|
|
953
|
+
if (!node || typeof node !== "object")
|
|
954
|
+
return;
|
|
955
|
+
switch (node.type) {
|
|
956
|
+
case "Field":
|
|
957
|
+
if (node.name)
|
|
958
|
+
roots.add(node.name);
|
|
959
|
+
break;
|
|
960
|
+
case "Subexpression":
|
|
961
|
+
if (node.left)
|
|
962
|
+
collectLeftmostRoot(node.left, roots);
|
|
963
|
+
break;
|
|
964
|
+
case "FilterProjection":
|
|
965
|
+
if (node.left)
|
|
966
|
+
collectLeftmostRoot(node.left, roots);
|
|
967
|
+
break;
|
|
968
|
+
case "Projection":
|
|
969
|
+
case "Flatten":
|
|
970
|
+
if (node.left)
|
|
971
|
+
collectLeftmostRoot(node.left, roots);
|
|
972
|
+
break;
|
|
973
|
+
case "Function":
|
|
974
|
+
if (node.children) {
|
|
975
|
+
for (const child of node.children) {
|
|
976
|
+
collectRoots(child, roots);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
break;
|
|
980
|
+
case "MultiSelectList":
|
|
981
|
+
case "MultiSelectHash":
|
|
982
|
+
if (node.children) {
|
|
983
|
+
for (const child of node.children) {
|
|
984
|
+
collectRoots(child, roots);
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
break;
|
|
988
|
+
case "Comparator":
|
|
989
|
+
case "And":
|
|
990
|
+
case "Or":
|
|
991
|
+
case "Arithmetic":
|
|
992
|
+
if (node.left)
|
|
993
|
+
collectRoots(node.left, roots);
|
|
994
|
+
if (node.right)
|
|
995
|
+
collectRoots(node.right, roots);
|
|
996
|
+
break;
|
|
997
|
+
case "Not":
|
|
998
|
+
case "Negate":
|
|
999
|
+
if (node.children?.[0])
|
|
1000
|
+
collectRoots(node.children[0], roots);
|
|
1001
|
+
break;
|
|
1002
|
+
case "Pipe":
|
|
1003
|
+
if (node.left)
|
|
1004
|
+
collectRoots(node.left, roots);
|
|
1005
|
+
break;
|
|
1006
|
+
case "IndexExpression":
|
|
1007
|
+
if (node.left)
|
|
1008
|
+
collectLeftmostRoot(node.left, roots);
|
|
1009
|
+
break;
|
|
1010
|
+
case "ValueProjection":
|
|
1011
|
+
case "Slice":
|
|
1012
|
+
if (node.left)
|
|
1013
|
+
collectLeftmostRoot(node.left, roots);
|
|
1014
|
+
break;
|
|
1015
|
+
case "Literal":
|
|
1016
|
+
case "Current":
|
|
1017
|
+
case "Identity":
|
|
1018
|
+
case "Expref":
|
|
1019
|
+
break;
|
|
1020
|
+
case "KeyValuePair": {
|
|
1021
|
+
const kvValue = node.value;
|
|
1022
|
+
if (kvValue) {
|
|
1023
|
+
collectRoots(kvValue, roots);
|
|
1024
|
+
}
|
|
1025
|
+
break;
|
|
1026
|
+
}
|
|
1027
|
+
default:
|
|
1028
|
+
if (node.left)
|
|
1029
|
+
collectRoots(node.left, roots);
|
|
1030
|
+
if (node.right)
|
|
1031
|
+
collectRoots(node.right, roots);
|
|
1032
|
+
if (node.children) {
|
|
1033
|
+
for (const child of node.children) {
|
|
1034
|
+
collectRoots(child, roots);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
break;
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
function collectLeftmostRoot(node, roots) {
|
|
1041
|
+
if (!node)
|
|
1042
|
+
return;
|
|
1043
|
+
switch (node.type) {
|
|
1044
|
+
case "Field":
|
|
1045
|
+
if (node.name)
|
|
1046
|
+
roots.add(node.name);
|
|
1047
|
+
break;
|
|
1048
|
+
case "Subexpression":
|
|
1049
|
+
if (node.left)
|
|
1050
|
+
collectLeftmostRoot(node.left, roots);
|
|
1051
|
+
break;
|
|
1052
|
+
case "FilterProjection":
|
|
1053
|
+
case "Projection":
|
|
1054
|
+
case "Flatten":
|
|
1055
|
+
case "IndexExpression":
|
|
1056
|
+
case "ValueProjection":
|
|
1057
|
+
case "Slice":
|
|
1058
|
+
if (node.left)
|
|
1059
|
+
collectLeftmostRoot(node.left, roots);
|
|
1060
|
+
break;
|
|
1061
|
+
default:
|
|
1062
|
+
collectRoots(node, roots);
|
|
1063
|
+
break;
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
function extractTemplateExpressions(template) {
|
|
1067
|
+
const expressions = [];
|
|
1068
|
+
const unclosed = [];
|
|
1069
|
+
let i = 0;
|
|
1070
|
+
while (i < template.length) {
|
|
1071
|
+
if (template[i] === "$" && template[i + 1] === "{") {
|
|
1072
|
+
const start = i;
|
|
1073
|
+
i += 2;
|
|
1074
|
+
let depth = 1;
|
|
1075
|
+
const exprStart = i;
|
|
1076
|
+
while (i < template.length && depth > 0) {
|
|
1077
|
+
if (template[i] === "{")
|
|
1078
|
+
depth++;
|
|
1079
|
+
else if (template[i] === "}")
|
|
1080
|
+
depth--;
|
|
1081
|
+
if (depth > 0)
|
|
1082
|
+
i++;
|
|
1083
|
+
}
|
|
1084
|
+
if (depth === 0) {
|
|
1085
|
+
const expression = template.slice(exprStart, i);
|
|
1086
|
+
expressions.push({ expression, start, end: i + 1 });
|
|
1087
|
+
i++;
|
|
1088
|
+
} else {
|
|
1089
|
+
unclosed.push(start);
|
|
1090
|
+
}
|
|
1091
|
+
} else {
|
|
1092
|
+
i++;
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
return { expressions, unclosed };
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
// src/compiler/passes/validate-jmespath.ts
|
|
1099
|
+
function validateJmespath(workflow, graph) {
|
|
1100
|
+
const diagnostics = [];
|
|
1101
|
+
const { expressions, templateDiagnostics } = collectExpressions(workflow);
|
|
1102
|
+
diagnostics.push(...templateDiagnostics);
|
|
1103
|
+
for (const expr of expressions) {
|
|
1104
|
+
const syntaxResult = validateJmespathSyntax(expr.expression);
|
|
1105
|
+
if (!syntaxResult.valid) {
|
|
1106
|
+
diagnostics.push({
|
|
1107
|
+
severity: "error",
|
|
1108
|
+
location: { stepId: expr.stepId, field: expr.field },
|
|
1109
|
+
message: `Invalid JMESPath syntax in '${expr.expression}': ${syntaxResult.error}`,
|
|
1110
|
+
code: "JMESPATH_SYNTAX_ERROR"
|
|
1111
|
+
});
|
|
1112
|
+
continue;
|
|
1113
|
+
}
|
|
1114
|
+
validateExpressionScope(expr, graph, !!workflow.inputSchema, diagnostics);
|
|
1115
|
+
}
|
|
1116
|
+
return diagnostics;
|
|
1117
|
+
}
|
|
1118
|
+
function collectFromExpressionValue(val, stepId, field, expressions, templateDiagnostics) {
|
|
1119
|
+
if (val.type === "jmespath" && val.expression) {
|
|
1120
|
+
expressions.push({
|
|
1121
|
+
expression: val.expression,
|
|
1122
|
+
stepId,
|
|
1123
|
+
field: `${field}.expression`
|
|
1124
|
+
});
|
|
1125
|
+
} else if (val.type === "template" && val.template) {
|
|
1126
|
+
const { expressions: templateExprs, unclosed } = extractTemplateExpressions(val.template);
|
|
1127
|
+
for (const te of templateExprs) {
|
|
1128
|
+
expressions.push({
|
|
1129
|
+
expression: te.expression,
|
|
1130
|
+
stepId,
|
|
1131
|
+
field: `${field}.template[${te.start}:${te.end}]`
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1134
|
+
for (const pos of unclosed) {
|
|
1135
|
+
templateDiagnostics.push({
|
|
1136
|
+
severity: "error",
|
|
1137
|
+
location: { stepId, field: `${field}.template[${pos}]` },
|
|
1138
|
+
message: `Unclosed template expression at position ${pos} in template`,
|
|
1139
|
+
code: "UNCLOSED_TEMPLATE_EXPRESSION"
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
function collectExpressions(workflow) {
|
|
1145
|
+
const expressions = [];
|
|
1146
|
+
const templateDiagnostics = [];
|
|
1147
|
+
for (const step of workflow.steps) {
|
|
1148
|
+
switch (step.type) {
|
|
1149
|
+
case "tool-call":
|
|
1150
|
+
for (const [key, val] of Object.entries(step.params.toolInput)) {
|
|
1151
|
+
collectFromExpressionValue(val, step.id, `params.toolInput.${key}`, expressions, templateDiagnostics);
|
|
1152
|
+
}
|
|
1153
|
+
break;
|
|
1154
|
+
case "switch-case":
|
|
1155
|
+
collectFromExpressionValue(step.params.switchOn, step.id, "params.switchOn", expressions, templateDiagnostics);
|
|
1156
|
+
for (const [i, c] of step.params.cases.entries()) {
|
|
1157
|
+
collectFromExpressionValue(c.value, step.id, `params.cases[${i}].value`, expressions, templateDiagnostics);
|
|
1158
|
+
}
|
|
1159
|
+
break;
|
|
1160
|
+
case "for-each":
|
|
1161
|
+
collectFromExpressionValue(step.params.target, step.id, "params.target", expressions, templateDiagnostics);
|
|
1162
|
+
break;
|
|
1163
|
+
case "extract-data":
|
|
1164
|
+
collectFromExpressionValue(step.params.sourceData, step.id, "params.sourceData", expressions, templateDiagnostics);
|
|
1165
|
+
break;
|
|
1166
|
+
case "start":
|
|
1167
|
+
break;
|
|
1168
|
+
case "end":
|
|
1169
|
+
if (step.params?.output) {
|
|
1170
|
+
collectFromExpressionValue(step.params.output, step.id, "params.output", expressions, templateDiagnostics);
|
|
1171
|
+
}
|
|
1172
|
+
break;
|
|
1173
|
+
case "llm-prompt": {
|
|
1174
|
+
const { expressions: templateExprs, unclosed } = extractTemplateExpressions(step.params.prompt);
|
|
1175
|
+
for (const te of templateExprs) {
|
|
1176
|
+
expressions.push({
|
|
1177
|
+
expression: te.expression,
|
|
1178
|
+
stepId: step.id,
|
|
1179
|
+
field: `params.prompt[${te.start}:${te.end}]`
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
for (const pos of unclosed) {
|
|
1183
|
+
templateDiagnostics.push({
|
|
1184
|
+
severity: "error",
|
|
1185
|
+
location: {
|
|
1186
|
+
stepId: step.id,
|
|
1187
|
+
field: `params.prompt[${pos}]`
|
|
1188
|
+
},
|
|
1189
|
+
message: `Unclosed template expression at position ${pos} in prompt`,
|
|
1190
|
+
code: "UNCLOSED_TEMPLATE_EXPRESSION"
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
break;
|
|
1194
|
+
}
|
|
1195
|
+
case "agent-loop": {
|
|
1196
|
+
const { expressions: templateExprs, unclosed } = extractTemplateExpressions(step.params.instructions);
|
|
1197
|
+
for (const te of templateExprs) {
|
|
1198
|
+
expressions.push({
|
|
1199
|
+
expression: te.expression,
|
|
1200
|
+
stepId: step.id,
|
|
1201
|
+
field: `params.instructions[${te.start}:${te.end}]`
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1204
|
+
for (const pos of unclosed) {
|
|
1205
|
+
templateDiagnostics.push({
|
|
1206
|
+
severity: "error",
|
|
1207
|
+
location: {
|
|
1208
|
+
stepId: step.id,
|
|
1209
|
+
field: `params.instructions[${pos}]`
|
|
1210
|
+
},
|
|
1211
|
+
message: `Unclosed template expression at position ${pos} in instructions`,
|
|
1212
|
+
code: "UNCLOSED_TEMPLATE_EXPRESSION"
|
|
1213
|
+
});
|
|
1214
|
+
}
|
|
1215
|
+
if (step.params.maxSteps) {
|
|
1216
|
+
collectFromExpressionValue(step.params.maxSteps, step.id, "params.maxSteps", expressions, templateDiagnostics);
|
|
1217
|
+
}
|
|
1218
|
+
break;
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
return { expressions, templateDiagnostics };
|
|
1223
|
+
}
|
|
1224
|
+
function validateExpressionScope(expr, graph, hasInputSchema, diagnostics) {
|
|
1225
|
+
const astRoots = extractRootIdentifiers(expr.expression);
|
|
1226
|
+
const loopVars = graph.loopVariablesInScope.get(expr.stepId);
|
|
1227
|
+
const predecessors = graph.predecessors.get(expr.stepId);
|
|
1228
|
+
for (const root of astRoots) {
|
|
1229
|
+
if (root === "input" && hasInputSchema)
|
|
1230
|
+
continue;
|
|
1231
|
+
if (loopVars?.has(root))
|
|
1232
|
+
continue;
|
|
1233
|
+
if (graph.stepIndex.has(root)) {
|
|
1234
|
+
if (predecessors && !predecessors.has(root)) {
|
|
1235
|
+
diagnostics.push({
|
|
1236
|
+
severity: "warning",
|
|
1237
|
+
location: { stepId: expr.stepId, field: expr.field },
|
|
1238
|
+
message: `Expression '${expr.expression}' references step '${root}' which may not have executed before step '${expr.stepId}'`,
|
|
1239
|
+
code: "JMESPATH_FORWARD_REFERENCE"
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
continue;
|
|
1243
|
+
}
|
|
1244
|
+
diagnostics.push({
|
|
1245
|
+
severity: "error",
|
|
1246
|
+
location: { stepId: expr.stepId, field: expr.field },
|
|
1247
|
+
message: `Expression '${expr.expression}' references '${root}' which is not a known step ID or loop variable in scope`,
|
|
1248
|
+
code: "JMESPATH_INVALID_ROOT_REFERENCE"
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
// src/compiler/passes/validate-limits.ts
|
|
1254
|
+
var DEFAULT_LIMITS = {
|
|
1255
|
+
maxAttempts: Number.POSITIVE_INFINITY,
|
|
1256
|
+
maxSleepMs: 300000,
|
|
1257
|
+
maxBackoffMultiplier: 2,
|
|
1258
|
+
minBackoffMultiplier: 1,
|
|
1259
|
+
maxTimeoutMs: 600000
|
|
1260
|
+
};
|
|
1261
|
+
function validateLimits(workflow, limits) {
|
|
1262
|
+
const resolved = { ...DEFAULT_LIMITS, ...limits };
|
|
1263
|
+
const diagnostics = [];
|
|
1264
|
+
for (const step of workflow.steps) {
|
|
1265
|
+
if (step.type === "sleep") {
|
|
1266
|
+
const expr = step.params.durationMs;
|
|
1267
|
+
if (expr.type === "literal" && typeof expr.value === "number") {
|
|
1268
|
+
if (expr.value > resolved.maxSleepMs) {
|
|
1269
|
+
diagnostics.push({
|
|
1270
|
+
severity: "error",
|
|
1271
|
+
location: { stepId: step.id, field: "params.durationMs" },
|
|
1272
|
+
message: `Sleep duration ${expr.value}ms exceeds limit of ${resolved.maxSleepMs}ms`,
|
|
1273
|
+
code: "SLEEP_DURATION_EXCEEDS_LIMIT"
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
if (step.type === "wait-for-condition") {
|
|
1279
|
+
const { maxAttempts, intervalMs, backoffMultiplier, timeoutMs } = step.params;
|
|
1280
|
+
if (maxAttempts?.type === "literal" && typeof maxAttempts.value === "number") {
|
|
1281
|
+
if (maxAttempts.value > resolved.maxAttempts) {
|
|
1282
|
+
diagnostics.push({
|
|
1283
|
+
severity: "error",
|
|
1284
|
+
location: { stepId: step.id, field: "params.maxAttempts" },
|
|
1285
|
+
message: `maxAttempts ${maxAttempts.value} exceeds limit of ${resolved.maxAttempts}`,
|
|
1286
|
+
code: "WAIT_ATTEMPTS_EXCEEDS_LIMIT"
|
|
1287
|
+
});
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
if (intervalMs?.type === "literal" && typeof intervalMs.value === "number") {
|
|
1291
|
+
if (intervalMs.value > resolved.maxSleepMs) {
|
|
1292
|
+
diagnostics.push({
|
|
1293
|
+
severity: "error",
|
|
1294
|
+
location: { stepId: step.id, field: "params.intervalMs" },
|
|
1295
|
+
message: `intervalMs ${intervalMs.value}ms exceeds limit of ${resolved.maxSleepMs}ms`,
|
|
1296
|
+
code: "WAIT_INTERVAL_EXCEEDS_LIMIT"
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
if (backoffMultiplier?.type === "literal" && typeof backoffMultiplier.value === "number") {
|
|
1301
|
+
if (backoffMultiplier.value < resolved.minBackoffMultiplier || backoffMultiplier.value > resolved.maxBackoffMultiplier) {
|
|
1302
|
+
diagnostics.push({
|
|
1303
|
+
severity: "error",
|
|
1304
|
+
location: { stepId: step.id, field: "params.backoffMultiplier" },
|
|
1305
|
+
message: `backoffMultiplier ${backoffMultiplier.value} is outside allowed range [${resolved.minBackoffMultiplier}, ${resolved.maxBackoffMultiplier}]`,
|
|
1306
|
+
code: "BACKOFF_MULTIPLIER_OUT_OF_RANGE"
|
|
1307
|
+
});
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
if (timeoutMs?.type === "literal" && typeof timeoutMs.value === "number") {
|
|
1311
|
+
if (timeoutMs.value > resolved.maxTimeoutMs) {
|
|
1312
|
+
diagnostics.push({
|
|
1313
|
+
severity: "error",
|
|
1314
|
+
location: { stepId: step.id, field: "params.timeoutMs" },
|
|
1315
|
+
message: `timeoutMs ${timeoutMs.value}ms exceeds limit of ${resolved.maxTimeoutMs}ms`,
|
|
1316
|
+
code: "WAIT_TIMEOUT_EXCEEDS_LIMIT"
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
return diagnostics;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
// src/compiler/passes/validate-references.ts
|
|
1326
|
+
function validateReferences(workflow) {
|
|
1327
|
+
const diagnostics = [];
|
|
1328
|
+
const stepIds = new Set(workflow.steps.map((s) => s.id));
|
|
1329
|
+
if (!stepIds.has(workflow.initialStepId)) {
|
|
1330
|
+
diagnostics.push({
|
|
1331
|
+
severity: "error",
|
|
1332
|
+
location: { stepId: null, field: "initialStepId" },
|
|
1333
|
+
message: `Initial step '${workflow.initialStepId}' does not exist`,
|
|
1334
|
+
code: "MISSING_INITIAL_STEP"
|
|
1335
|
+
});
|
|
1336
|
+
}
|
|
1337
|
+
for (const step of workflow.steps) {
|
|
1338
|
+
if (step.nextStepId && !stepIds.has(step.nextStepId)) {
|
|
1339
|
+
diagnostics.push({
|
|
1340
|
+
severity: "error",
|
|
1341
|
+
location: { stepId: step.id, field: "nextStepId" },
|
|
1342
|
+
message: `Step '${step.id}' references non-existent next step '${step.nextStepId}'`,
|
|
1343
|
+
code: "MISSING_NEXT_STEP"
|
|
1344
|
+
});
|
|
1345
|
+
}
|
|
1346
|
+
if (step.type === "switch-case") {
|
|
1347
|
+
for (const [i, c] of step.params.cases.entries()) {
|
|
1348
|
+
if (!stepIds.has(c.branchBodyStepId)) {
|
|
1349
|
+
diagnostics.push({
|
|
1350
|
+
severity: "error",
|
|
1351
|
+
location: {
|
|
1352
|
+
stepId: step.id,
|
|
1353
|
+
field: `params.cases[${i}].branchBodyStepId`
|
|
1354
|
+
},
|
|
1355
|
+
message: `Step '${step.id}' case ${i} references non-existent branch body step '${c.branchBodyStepId}'`,
|
|
1356
|
+
code: "MISSING_BRANCH_BODY_STEP"
|
|
1357
|
+
});
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
if (step.type === "for-each") {
|
|
1362
|
+
if (!stepIds.has(step.params.loopBodyStepId)) {
|
|
1363
|
+
diagnostics.push({
|
|
1364
|
+
severity: "error",
|
|
1365
|
+
location: {
|
|
1366
|
+
stepId: step.id,
|
|
1367
|
+
field: "params.loopBodyStepId"
|
|
1368
|
+
},
|
|
1369
|
+
message: `Step '${step.id}' references non-existent loop body step '${step.params.loopBodyStepId}'`,
|
|
1370
|
+
code: "MISSING_LOOP_BODY_STEP"
|
|
1371
|
+
});
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
if (step.type === "wait-for-condition") {
|
|
1375
|
+
if (!stepIds.has(step.params.conditionStepId)) {
|
|
1376
|
+
diagnostics.push({
|
|
1377
|
+
severity: "error",
|
|
1378
|
+
location: {
|
|
1379
|
+
stepId: step.id,
|
|
1380
|
+
field: "params.conditionStepId"
|
|
1381
|
+
},
|
|
1382
|
+
message: `Step '${step.id}' references non-existent condition body step '${step.params.conditionStepId}'`,
|
|
1383
|
+
code: "MISSING_CONDITION_BODY_STEP"
|
|
1384
|
+
});
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
return diagnostics;
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
// src/compiler/passes/validate-tools.ts
|
|
1392
|
+
function validateTools(workflow, tools) {
|
|
1393
|
+
const diagnostics = [];
|
|
1394
|
+
for (const step of workflow.steps) {
|
|
1395
|
+
if (step.type === "tool-call") {
|
|
1396
|
+
const { toolName, toolInput } = step.params;
|
|
1397
|
+
const toolDef = tools[toolName];
|
|
1398
|
+
if (!toolDef) {
|
|
1399
|
+
diagnostics.push({
|
|
1400
|
+
severity: "error",
|
|
1401
|
+
location: { stepId: step.id, field: "params.toolName" },
|
|
1402
|
+
message: `Step '${step.id}' references unknown tool '${toolName}'`,
|
|
1403
|
+
code: "UNKNOWN_TOOL"
|
|
1404
|
+
});
|
|
1405
|
+
continue;
|
|
1406
|
+
}
|
|
1407
|
+
const schemaProperties = toolDef.inputSchema.properties ?? {};
|
|
1408
|
+
const requiredKeys = new Set(toolDef.inputSchema.required ?? []);
|
|
1409
|
+
const definedKeys = new Set(Object.keys(schemaProperties));
|
|
1410
|
+
const providedKeys = new Set(Object.keys(toolInput));
|
|
1411
|
+
for (const key of providedKeys) {
|
|
1412
|
+
if (!definedKeys.has(key)) {
|
|
1413
|
+
diagnostics.push({
|
|
1414
|
+
severity: "warning",
|
|
1415
|
+
location: {
|
|
1416
|
+
stepId: step.id,
|
|
1417
|
+
field: `params.toolInput.${key}`
|
|
1418
|
+
},
|
|
1419
|
+
message: `Step '${step.id}' provides input key '${key}' which is not defined in tool '${toolName}' schema`,
|
|
1420
|
+
code: "EXTRA_TOOL_INPUT_KEY"
|
|
1421
|
+
});
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
for (const key of requiredKeys) {
|
|
1425
|
+
if (!providedKeys.has(key)) {
|
|
1426
|
+
diagnostics.push({
|
|
1427
|
+
severity: "error",
|
|
1428
|
+
location: { stepId: step.id, field: "params.toolInput" },
|
|
1429
|
+
message: `Step '${step.id}' is missing required input key '${key}' for tool '${toolName}'`,
|
|
1430
|
+
code: "MISSING_TOOL_INPUT_KEY"
|
|
1431
|
+
});
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
if (step.type === "agent-loop") {
|
|
1436
|
+
for (const [i, toolName] of step.params.tools.entries()) {
|
|
1437
|
+
if (!tools[toolName]) {
|
|
1438
|
+
diagnostics.push({
|
|
1439
|
+
severity: "error",
|
|
1440
|
+
location: {
|
|
1441
|
+
stepId: step.id,
|
|
1442
|
+
field: `params.tools[${i}]`
|
|
1443
|
+
},
|
|
1444
|
+
message: `Step '${step.id}' references unknown tool '${toolName}'`,
|
|
1445
|
+
code: "UNKNOWN_TOOL"
|
|
1446
|
+
});
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
return diagnostics;
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
// src/compiler/index.ts
|
|
1455
|
+
async function compileWorkflow(workflow, options) {
|
|
1456
|
+
const diagnostics = [];
|
|
1457
|
+
const graphResult = buildGraph(workflow);
|
|
1458
|
+
diagnostics.push(...graphResult.diagnostics);
|
|
1459
|
+
const refDiagnostics = validateReferences(workflow);
|
|
1460
|
+
for (const d of refDiagnostics) {
|
|
1461
|
+
if (d.code === "MISSING_INITIAL_STEP" && diagnostics.some((e) => e.code === "MISSING_INITIAL_STEP")) {
|
|
1462
|
+
continue;
|
|
1463
|
+
}
|
|
1464
|
+
diagnostics.push(d);
|
|
1465
|
+
}
|
|
1466
|
+
diagnostics.push(...validateLimits(workflow, options?.limits));
|
|
1467
|
+
let constrainedToolSchemas = null;
|
|
1468
|
+
let toolSchemas = null;
|
|
1469
|
+
if (options?.tools) {
|
|
1470
|
+
toolSchemas = await extractToolSchemas(options.tools);
|
|
1471
|
+
diagnostics.push(...validateTools(workflow, toolSchemas));
|
|
1472
|
+
constrainedToolSchemas = generateConstrainedToolSchemas(workflow, toolSchemas);
|
|
1473
|
+
}
|
|
1474
|
+
if (graphResult.graph) {
|
|
1475
|
+
diagnostics.push(...validateControlFlow(workflow, graphResult.graph, toolSchemas));
|
|
1476
|
+
diagnostics.push(...validateJmespath(workflow, graphResult.graph));
|
|
1477
|
+
if (toolSchemas) {
|
|
1478
|
+
diagnostics.push(...validateForeachTarget(workflow, graphResult.graph, toolSchemas));
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
const hasErrors = diagnostics.some((d) => d.severity === "error");
|
|
1482
|
+
let optimizedWorkflow = null;
|
|
1483
|
+
if (graphResult.graph && !hasErrors) {
|
|
1484
|
+
const bpResult = applyBestPractices(workflow, graphResult.graph);
|
|
1485
|
+
optimizedWorkflow = bpResult.workflow;
|
|
1486
|
+
diagnostics.push(...bpResult.diagnostics);
|
|
1487
|
+
}
|
|
1488
|
+
return {
|
|
1489
|
+
diagnostics,
|
|
1490
|
+
graph: graphResult.graph,
|
|
1491
|
+
workflow: optimizedWorkflow,
|
|
1492
|
+
constrainedToolSchemas
|
|
1493
|
+
};
|
|
1494
|
+
}
|
|
1495
|
+
async function extractToolSchemas(tools) {
|
|
1496
|
+
const schemas = {};
|
|
1497
|
+
for (const [name, toolDef] of Object.entries(tools)) {
|
|
1498
|
+
const jsonSchema = await asSchema(toolDef.inputSchema).jsonSchema;
|
|
1499
|
+
schemas[name] = {
|
|
1500
|
+
inputSchema: jsonSchema
|
|
1501
|
+
};
|
|
1502
|
+
if (toolDef.outputSchema) {
|
|
1503
|
+
schemas[name].outputSchema = await asSchema(toolDef.outputSchema).jsonSchema;
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
return schemas;
|
|
1507
|
+
}
|
|
1508
|
+
// src/executor/errors.ts
|
|
1509
|
+
class StepExecutionError extends Error {
|
|
1510
|
+
stepId;
|
|
1511
|
+
code;
|
|
1512
|
+
category;
|
|
1513
|
+
cause;
|
|
1514
|
+
name = "StepExecutionError";
|
|
1515
|
+
constructor(stepId, code, category, message, cause) {
|
|
1516
|
+
super(message);
|
|
1517
|
+
this.stepId = stepId;
|
|
1518
|
+
this.code = code;
|
|
1519
|
+
this.category = category;
|
|
1520
|
+
this.cause = cause;
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
class ConfigurationError extends StepExecutionError {
|
|
1525
|
+
constructor(stepId, code, message) {
|
|
1526
|
+
super(stepId, code, "configuration", message);
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
class ValidationError extends StepExecutionError {
|
|
1531
|
+
input;
|
|
1532
|
+
constructor(stepId, code, message, input, cause) {
|
|
1533
|
+
super(stepId, code, "validation", message, cause);
|
|
1534
|
+
this.input = input;
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
class ExternalServiceError extends StepExecutionError {
|
|
1539
|
+
statusCode;
|
|
1540
|
+
isRetryable;
|
|
1541
|
+
constructor(stepId, code, message, cause, statusCode, isRetryable = true) {
|
|
1542
|
+
super(stepId, code, "external-service", message, cause);
|
|
1543
|
+
this.statusCode = statusCode;
|
|
1544
|
+
this.isRetryable = isRetryable;
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
class ExpressionError extends StepExecutionError {
|
|
1549
|
+
expression;
|
|
1550
|
+
constructor(stepId, code, message, expression, cause) {
|
|
1551
|
+
super(stepId, code, "expression", message, cause);
|
|
1552
|
+
this.expression = expression;
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
class OutputQualityError extends StepExecutionError {
|
|
1557
|
+
rawOutput;
|
|
1558
|
+
constructor(stepId, code, message, rawOutput, cause) {
|
|
1559
|
+
super(stepId, code, "output-quality", message, cause);
|
|
1560
|
+
this.rawOutput = rawOutput;
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
class ExtractionError extends StepExecutionError {
|
|
1565
|
+
reason;
|
|
1566
|
+
constructor(stepId, message, reason) {
|
|
1567
|
+
super(stepId, "EXTRACTION_GAVE_UP", "output-quality", message);
|
|
1568
|
+
this.reason = reason;
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
// src/executor/context.ts
|
|
1573
|
+
function createDefaultDurableContext() {
|
|
1574
|
+
return {
|
|
1575
|
+
step: (_name, fn) => fn(),
|
|
1576
|
+
sleep: (_name, ms) => new Promise((r) => setTimeout(r, ms)),
|
|
1577
|
+
waitForCondition: async (_name, checkFn, opts) => {
|
|
1578
|
+
let delay = opts.intervalMs;
|
|
1579
|
+
const deadline = opts.timeoutMs ? Date.now() + opts.timeoutMs : undefined;
|
|
1580
|
+
for (let attempt = 0;attempt < opts.maxAttempts; attempt++) {
|
|
1581
|
+
const result = await checkFn();
|
|
1582
|
+
if (result)
|
|
1583
|
+
return result;
|
|
1584
|
+
if (deadline && Date.now() + delay > deadline) {
|
|
1585
|
+
throw new ExternalServiceError(_name, "WAIT_CONDITION_TIMEOUT", `wait-for-condition '${_name}' timed out after ${opts.timeoutMs}ms`, undefined, undefined, false);
|
|
1586
|
+
}
|
|
1587
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
1588
|
+
delay *= opts.backoffMultiplier;
|
|
1589
|
+
}
|
|
1590
|
+
throw new ExternalServiceError(_name, "WAIT_CONDITION_MAX_ATTEMPTS", `wait-for-condition '${_name}' exceeded ${opts.maxAttempts} attempts`, undefined, undefined, false);
|
|
1591
|
+
}
|
|
1592
|
+
};
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
// src/executor/state.ts
|
|
1596
|
+
import { type } from "arktype";
|
|
1597
|
+
var stepStatusSchema = type("'pending' | 'running' | 'completed' | 'failed' | 'skipped'");
|
|
1598
|
+
var runStatusSchema = type("'pending' | 'running' | 'completed' | 'failed'");
|
|
1599
|
+
var errorSnapshotSchema = type({
|
|
1600
|
+
code: "string",
|
|
1601
|
+
category: "string",
|
|
1602
|
+
message: "string",
|
|
1603
|
+
"stepId?": "string",
|
|
1604
|
+
"statusCode?": "number",
|
|
1605
|
+
"isRetryable?": "boolean"
|
|
1606
|
+
});
|
|
1607
|
+
var retryRecordSchema = type({
|
|
1608
|
+
attempt: "number",
|
|
1609
|
+
startedAt: "string",
|
|
1610
|
+
failedAt: "string",
|
|
1611
|
+
errorCode: "string",
|
|
1612
|
+
errorMessage: "string"
|
|
1613
|
+
});
|
|
1614
|
+
var executionPathSegmentSchema = type({
|
|
1615
|
+
type: "'for-each'",
|
|
1616
|
+
stepId: "string",
|
|
1617
|
+
iterationIndex: "number",
|
|
1618
|
+
itemValue: "unknown"
|
|
1619
|
+
}).or({
|
|
1620
|
+
type: "'switch-case'",
|
|
1621
|
+
stepId: "string",
|
|
1622
|
+
matchedCaseIndex: "number",
|
|
1623
|
+
matchedValue: "unknown"
|
|
1624
|
+
}).or({
|
|
1625
|
+
type: "'wait-for-condition'",
|
|
1626
|
+
stepId: "string",
|
|
1627
|
+
pollAttempt: "number"
|
|
1628
|
+
});
|
|
1629
|
+
var traceEntrySchema = type({
|
|
1630
|
+
type: "'log'",
|
|
1631
|
+
message: "string",
|
|
1632
|
+
"data?": "unknown"
|
|
1633
|
+
}).or({
|
|
1634
|
+
type: "'agent-step'",
|
|
1635
|
+
step: "unknown"
|
|
1636
|
+
});
|
|
1637
|
+
var stepExecutionRecordSchema = type({
|
|
1638
|
+
stepId: "string",
|
|
1639
|
+
status: stepStatusSchema,
|
|
1640
|
+
"startedAt?": "string",
|
|
1641
|
+
"completedAt?": "string",
|
|
1642
|
+
"durationMs?": "number",
|
|
1643
|
+
"output?": "unknown",
|
|
1644
|
+
"error?": errorSnapshotSchema,
|
|
1645
|
+
"resolvedInputs?": "unknown",
|
|
1646
|
+
"trace?": [traceEntrySchema, "[]"],
|
|
1647
|
+
retries: [retryRecordSchema, "[]"],
|
|
1648
|
+
path: [executionPathSegmentSchema, "[]"]
|
|
1649
|
+
});
|
|
1650
|
+
var executionStateSchema = type({
|
|
1651
|
+
runId: "string",
|
|
1652
|
+
status: runStatusSchema,
|
|
1653
|
+
startedAt: "string",
|
|
1654
|
+
"completedAt?": "string",
|
|
1655
|
+
"durationMs?": "number",
|
|
1656
|
+
stepRecords: [stepExecutionRecordSchema, "[]"],
|
|
1657
|
+
"output?": "unknown",
|
|
1658
|
+
"error?": errorSnapshotSchema
|
|
1659
|
+
});
|
|
1660
|
+
var executionDeltaSchema = type({
|
|
1661
|
+
type: "'run-started'",
|
|
1662
|
+
runId: "string",
|
|
1663
|
+
startedAt: "string"
|
|
1664
|
+
}).or({
|
|
1665
|
+
type: "'step-started'",
|
|
1666
|
+
stepId: "string",
|
|
1667
|
+
path: [executionPathSegmentSchema, "[]"],
|
|
1668
|
+
startedAt: "string"
|
|
1669
|
+
}).or({
|
|
1670
|
+
type: "'step-completed'",
|
|
1671
|
+
stepId: "string",
|
|
1672
|
+
path: [executionPathSegmentSchema, "[]"],
|
|
1673
|
+
completedAt: "string",
|
|
1674
|
+
durationMs: "number",
|
|
1675
|
+
output: "unknown",
|
|
1676
|
+
"resolvedInputs?": "unknown",
|
|
1677
|
+
"trace?": [traceEntrySchema, "[]"]
|
|
1678
|
+
}).or({
|
|
1679
|
+
type: "'step-failed'",
|
|
1680
|
+
stepId: "string",
|
|
1681
|
+
path: [executionPathSegmentSchema, "[]"],
|
|
1682
|
+
failedAt: "string",
|
|
1683
|
+
durationMs: "number",
|
|
1684
|
+
error: errorSnapshotSchema,
|
|
1685
|
+
"resolvedInputs?": "unknown"
|
|
1686
|
+
}).or({
|
|
1687
|
+
type: "'step-retry'",
|
|
1688
|
+
stepId: "string",
|
|
1689
|
+
path: [executionPathSegmentSchema, "[]"],
|
|
1690
|
+
retry: retryRecordSchema
|
|
1691
|
+
}).or({
|
|
1692
|
+
type: "'run-completed'",
|
|
1693
|
+
runId: "string",
|
|
1694
|
+
completedAt: "string",
|
|
1695
|
+
durationMs: "number",
|
|
1696
|
+
"output?": "unknown"
|
|
1697
|
+
}).or({
|
|
1698
|
+
type: "'run-failed'",
|
|
1699
|
+
runId: "string",
|
|
1700
|
+
failedAt: "string",
|
|
1701
|
+
durationMs: "number",
|
|
1702
|
+
error: errorSnapshotSchema
|
|
1703
|
+
});
|
|
1704
|
+
function snapshotError(err) {
|
|
1705
|
+
const snapshot = {
|
|
1706
|
+
code: err.code,
|
|
1707
|
+
category: err.category,
|
|
1708
|
+
message: err.message
|
|
1709
|
+
};
|
|
1710
|
+
if (err.stepId)
|
|
1711
|
+
snapshot.stepId = err.stepId;
|
|
1712
|
+
if ("statusCode" in err && typeof err.statusCode === "number") {
|
|
1713
|
+
snapshot.statusCode = err.statusCode;
|
|
1714
|
+
}
|
|
1715
|
+
if ("isRetryable" in err && typeof err.isRetryable === "boolean") {
|
|
1716
|
+
snapshot.isRetryable = err.isRetryable;
|
|
1717
|
+
}
|
|
1718
|
+
return snapshot;
|
|
1719
|
+
}
|
|
1720
|
+
function segmentsEqual(sa, sb) {
|
|
1721
|
+
if (sa.type !== sb.type || sa.stepId !== sb.stepId)
|
|
1722
|
+
return false;
|
|
1723
|
+
if (sa.type === "for-each" && sb.type === "for-each") {
|
|
1724
|
+
return sa.iterationIndex === sb.iterationIndex;
|
|
1725
|
+
}
|
|
1726
|
+
if (sa.type === "switch-case" && sb.type === "switch-case") {
|
|
1727
|
+
return sa.matchedCaseIndex === sb.matchedCaseIndex;
|
|
1728
|
+
}
|
|
1729
|
+
if (sa.type === "wait-for-condition" && sb.type === "wait-for-condition") {
|
|
1730
|
+
return sa.pollAttempt === sb.pollAttempt;
|
|
1731
|
+
}
|
|
1732
|
+
return true;
|
|
1733
|
+
}
|
|
1734
|
+
function pathsEqual(a, b) {
|
|
1735
|
+
if (a.length !== b.length)
|
|
1736
|
+
return false;
|
|
1737
|
+
for (let i = 0;i < a.length; i++) {
|
|
1738
|
+
const sa = a[i];
|
|
1739
|
+
const sb = b[i];
|
|
1740
|
+
if (!segmentsEqual(sa, sb))
|
|
1741
|
+
return false;
|
|
1742
|
+
}
|
|
1743
|
+
return true;
|
|
1744
|
+
}
|
|
1745
|
+
function findRecordIndex(records, stepId, path) {
|
|
1746
|
+
for (let i = records.length - 1;i >= 0; i--) {
|
|
1747
|
+
const record = records[i];
|
|
1748
|
+
if (record.stepId === stepId && pathsEqual(record.path, path)) {
|
|
1749
|
+
return i;
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
return -1;
|
|
1753
|
+
}
|
|
1754
|
+
function applyDelta(state, delta) {
|
|
1755
|
+
switch (delta.type) {
|
|
1756
|
+
case "run-started":
|
|
1757
|
+
return { ...state, status: "running", startedAt: delta.startedAt };
|
|
1758
|
+
case "step-started": {
|
|
1759
|
+
const record = {
|
|
1760
|
+
stepId: delta.stepId,
|
|
1761
|
+
status: "running",
|
|
1762
|
+
startedAt: delta.startedAt,
|
|
1763
|
+
retries: [],
|
|
1764
|
+
path: delta.path
|
|
1765
|
+
};
|
|
1766
|
+
return {
|
|
1767
|
+
...state,
|
|
1768
|
+
stepRecords: [...state.stepRecords, record]
|
|
1769
|
+
};
|
|
1770
|
+
}
|
|
1771
|
+
case "step-completed": {
|
|
1772
|
+
const records = [...state.stepRecords];
|
|
1773
|
+
const idx = findRecordIndex(records, delta.stepId, delta.path);
|
|
1774
|
+
if (idx >= 0) {
|
|
1775
|
+
const existing = records[idx];
|
|
1776
|
+
const updated = {
|
|
1777
|
+
...existing,
|
|
1778
|
+
status: "completed",
|
|
1779
|
+
completedAt: delta.completedAt,
|
|
1780
|
+
durationMs: delta.durationMs,
|
|
1781
|
+
output: delta.output
|
|
1782
|
+
};
|
|
1783
|
+
if (delta.resolvedInputs !== undefined) {
|
|
1784
|
+
updated.resolvedInputs = delta.resolvedInputs;
|
|
1785
|
+
}
|
|
1786
|
+
if (delta.trace !== undefined) {
|
|
1787
|
+
updated.trace = delta.trace;
|
|
1788
|
+
}
|
|
1789
|
+
records[idx] = updated;
|
|
1790
|
+
}
|
|
1791
|
+
return { ...state, stepRecords: records };
|
|
1792
|
+
}
|
|
1793
|
+
case "step-failed": {
|
|
1794
|
+
const records = [...state.stepRecords];
|
|
1795
|
+
const idx = findRecordIndex(records, delta.stepId, delta.path);
|
|
1796
|
+
if (idx >= 0) {
|
|
1797
|
+
const existing = records[idx];
|
|
1798
|
+
const updated = {
|
|
1799
|
+
...existing,
|
|
1800
|
+
status: "failed",
|
|
1801
|
+
completedAt: delta.failedAt,
|
|
1802
|
+
durationMs: delta.durationMs,
|
|
1803
|
+
error: delta.error
|
|
1804
|
+
};
|
|
1805
|
+
if (delta.resolvedInputs !== undefined) {
|
|
1806
|
+
updated.resolvedInputs = delta.resolvedInputs;
|
|
1807
|
+
}
|
|
1808
|
+
records[idx] = updated;
|
|
1809
|
+
}
|
|
1810
|
+
return { ...state, stepRecords: records };
|
|
1811
|
+
}
|
|
1812
|
+
case "step-retry": {
|
|
1813
|
+
const records = [...state.stepRecords];
|
|
1814
|
+
const idx = findRecordIndex(records, delta.stepId, delta.path);
|
|
1815
|
+
if (idx >= 0) {
|
|
1816
|
+
const existing = records[idx];
|
|
1817
|
+
records[idx] = {
|
|
1818
|
+
...existing,
|
|
1819
|
+
retries: [...existing.retries, delta.retry]
|
|
1820
|
+
};
|
|
1821
|
+
}
|
|
1822
|
+
return { ...state, stepRecords: records };
|
|
1823
|
+
}
|
|
1824
|
+
case "run-completed":
|
|
1825
|
+
return {
|
|
1826
|
+
...state,
|
|
1827
|
+
status: "completed",
|
|
1828
|
+
completedAt: delta.completedAt,
|
|
1829
|
+
durationMs: delta.durationMs,
|
|
1830
|
+
output: delta.output
|
|
1831
|
+
};
|
|
1832
|
+
case "run-failed":
|
|
1833
|
+
return {
|
|
1834
|
+
...state,
|
|
1835
|
+
status: "failed",
|
|
1836
|
+
completedAt: delta.failedAt,
|
|
1837
|
+
durationMs: delta.durationMs,
|
|
1838
|
+
error: delta.error
|
|
1839
|
+
};
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
// src/executor/executor-types.ts
|
|
1844
|
+
var DEFAULT_EXECUTOR_LIMITS = {
|
|
1845
|
+
maxTotalMs: 600000,
|
|
1846
|
+
maxActiveMs: 300000,
|
|
1847
|
+
maxSleepMs: 300000,
|
|
1848
|
+
maxAttempts: Number.POSITIVE_INFINITY,
|
|
1849
|
+
maxBackoffMultiplier: 2,
|
|
1850
|
+
minBackoffMultiplier: 1,
|
|
1851
|
+
maxTimeoutMs: 600000,
|
|
1852
|
+
probeThresholdBytes: 50000,
|
|
1853
|
+
probeResultMaxBytes: 1e4,
|
|
1854
|
+
probeMaxSteps: 10
|
|
1855
|
+
};
|
|
1856
|
+
|
|
1857
|
+
class ExecutionTimer {
|
|
1858
|
+
startTime = Date.now();
|
|
1859
|
+
activeMs = 0;
|
|
1860
|
+
activeStart = null;
|
|
1861
|
+
limits;
|
|
1862
|
+
constructor(limits) {
|
|
1863
|
+
this.limits = { ...DEFAULT_EXECUTOR_LIMITS, ...limits };
|
|
1864
|
+
}
|
|
1865
|
+
get resolvedLimits() {
|
|
1866
|
+
return this.limits;
|
|
1867
|
+
}
|
|
1868
|
+
beginActive() {
|
|
1869
|
+
this.activeStart = Date.now();
|
|
1870
|
+
}
|
|
1871
|
+
endActive(stepId) {
|
|
1872
|
+
if (this.activeStart !== null) {
|
|
1873
|
+
this.activeMs += Date.now() - this.activeStart;
|
|
1874
|
+
this.activeStart = null;
|
|
1875
|
+
}
|
|
1876
|
+
if (this.activeMs > this.limits.maxActiveMs) {
|
|
1877
|
+
throw new ExternalServiceError(stepId, "EXECUTION_ACTIVE_TIMEOUT", `Active execution time ${this.activeMs}ms exceeded limit of ${this.limits.maxActiveMs}ms`, undefined, undefined, false);
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
checkTotal(stepId) {
|
|
1881
|
+
const elapsed = Date.now() - this.startTime;
|
|
1882
|
+
if (elapsed > this.limits.maxTotalMs) {
|
|
1883
|
+
throw new ExternalServiceError(stepId, "EXECUTION_TOTAL_TIMEOUT", `Total execution time ${elapsed}ms exceeded limit of ${this.limits.maxTotalMs}ms`, undefined, undefined, false);
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
class ExecutionStateManager {
|
|
1889
|
+
state;
|
|
1890
|
+
onChange;
|
|
1891
|
+
constructor(onChange) {
|
|
1892
|
+
this.state = {
|
|
1893
|
+
runId: crypto.randomUUID(),
|
|
1894
|
+
status: "pending",
|
|
1895
|
+
startedAt: new Date().toISOString(),
|
|
1896
|
+
stepRecords: []
|
|
1897
|
+
};
|
|
1898
|
+
this.onChange = onChange;
|
|
1899
|
+
}
|
|
1900
|
+
get currentState() {
|
|
1901
|
+
return this.state;
|
|
1902
|
+
}
|
|
1903
|
+
emit(delta) {
|
|
1904
|
+
this.state = applyDelta(this.state, delta);
|
|
1905
|
+
this.onChange?.(this.state, delta);
|
|
1906
|
+
}
|
|
1907
|
+
runStarted() {
|
|
1908
|
+
this.emit({
|
|
1909
|
+
type: "run-started",
|
|
1910
|
+
runId: this.state.runId,
|
|
1911
|
+
startedAt: this.state.startedAt
|
|
1912
|
+
});
|
|
1913
|
+
}
|
|
1914
|
+
stepStarted(stepId, path) {
|
|
1915
|
+
this.emit({
|
|
1916
|
+
type: "step-started",
|
|
1917
|
+
stepId,
|
|
1918
|
+
path,
|
|
1919
|
+
startedAt: new Date().toISOString()
|
|
1920
|
+
});
|
|
1921
|
+
}
|
|
1922
|
+
stepCompleted(stepId, path, output, durationMs, resolvedInputs, trace) {
|
|
1923
|
+
this.emit({
|
|
1924
|
+
type: "step-completed",
|
|
1925
|
+
stepId,
|
|
1926
|
+
path,
|
|
1927
|
+
completedAt: new Date().toISOString(),
|
|
1928
|
+
durationMs,
|
|
1929
|
+
output,
|
|
1930
|
+
resolvedInputs,
|
|
1931
|
+
trace
|
|
1932
|
+
});
|
|
1933
|
+
}
|
|
1934
|
+
stepFailed(stepId, path, error, durationMs, resolvedInputs) {
|
|
1935
|
+
this.emit({
|
|
1936
|
+
type: "step-failed",
|
|
1937
|
+
stepId,
|
|
1938
|
+
path,
|
|
1939
|
+
failedAt: new Date().toISOString(),
|
|
1940
|
+
durationMs,
|
|
1941
|
+
error: snapshotError(error),
|
|
1942
|
+
resolvedInputs
|
|
1943
|
+
});
|
|
1944
|
+
}
|
|
1945
|
+
retryAttempted(stepId, path, retry) {
|
|
1946
|
+
this.emit({
|
|
1947
|
+
type: "step-retry",
|
|
1948
|
+
stepId,
|
|
1949
|
+
path,
|
|
1950
|
+
retry
|
|
1951
|
+
});
|
|
1952
|
+
}
|
|
1953
|
+
runCompleted(output) {
|
|
1954
|
+
const startMs = new Date(this.state.startedAt).getTime();
|
|
1955
|
+
this.emit({
|
|
1956
|
+
type: "run-completed",
|
|
1957
|
+
runId: this.state.runId,
|
|
1958
|
+
completedAt: new Date().toISOString(),
|
|
1959
|
+
durationMs: Date.now() - startMs,
|
|
1960
|
+
output
|
|
1961
|
+
});
|
|
1962
|
+
}
|
|
1963
|
+
runFailed(error) {
|
|
1964
|
+
const startMs = new Date(this.state.startedAt).getTime();
|
|
1965
|
+
this.emit({
|
|
1966
|
+
type: "run-failed",
|
|
1967
|
+
runId: this.state.runId,
|
|
1968
|
+
failedAt: new Date().toISOString(),
|
|
1969
|
+
durationMs: Date.now() - startMs,
|
|
1970
|
+
error: snapshotError(error)
|
|
1971
|
+
});
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
// src/executor/steps/agent-loop.ts
|
|
1976
|
+
import {
|
|
1977
|
+
generateText,
|
|
1978
|
+
jsonSchema,
|
|
1979
|
+
Output,
|
|
1980
|
+
stepCountIs,
|
|
1981
|
+
ToolLoopAgent
|
|
1982
|
+
} from "ai";
|
|
1983
|
+
|
|
1984
|
+
// src/executor/helpers.ts
|
|
1985
|
+
import { search } from "@jmespath-community/jmespath";
|
|
1986
|
+
import {
|
|
1987
|
+
APICallError,
|
|
1988
|
+
JSONParseError,
|
|
1989
|
+
NoContentGeneratedError,
|
|
1990
|
+
RetryError,
|
|
1991
|
+
TypeValidationError,
|
|
1992
|
+
tool
|
|
1993
|
+
} from "ai";
|
|
1994
|
+
import { type as arktype } from "arktype";
|
|
1995
|
+
function evaluateExpression(expr, scope, stepId) {
|
|
1996
|
+
if (expr.type === "literal") {
|
|
1997
|
+
return expr.value;
|
|
1998
|
+
}
|
|
1999
|
+
if (expr.type === "template") {
|
|
2000
|
+
return interpolateTemplate(expr.template, scope, stepId);
|
|
2001
|
+
}
|
|
2002
|
+
try {
|
|
2003
|
+
return search(scope, expr.expression);
|
|
2004
|
+
} catch (e) {
|
|
2005
|
+
throw new ExpressionError(stepId, "JMESPATH_EVALUATION_ERROR", `JMESPath expression '${expr.expression}' failed: ${e instanceof Error ? e.message : String(e)}`, expr.expression, e);
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
function stringifyValue(value) {
|
|
2009
|
+
if (value === null || value === undefined)
|
|
2010
|
+
return "";
|
|
2011
|
+
if (typeof value === "object")
|
|
2012
|
+
return JSON.stringify(value);
|
|
2013
|
+
return String(value);
|
|
2014
|
+
}
|
|
2015
|
+
function interpolateTemplate(template, scope, stepId) {
|
|
2016
|
+
const { expressions } = extractTemplateExpressions(template);
|
|
2017
|
+
if (expressions.length === 0)
|
|
2018
|
+
return template;
|
|
2019
|
+
let result = "";
|
|
2020
|
+
let lastEnd = 0;
|
|
2021
|
+
for (const expr of expressions) {
|
|
2022
|
+
result += template.slice(lastEnd, expr.start);
|
|
2023
|
+
try {
|
|
2024
|
+
const value = search(scope, expr.expression);
|
|
2025
|
+
result += stringifyValue(value);
|
|
2026
|
+
} catch (e) {
|
|
2027
|
+
throw new ExpressionError(stepId, "TEMPLATE_INTERPOLATION_ERROR", `Template expression '${expr.expression}' failed: ${e instanceof Error ? e.message : String(e)}`, expr.expression, e);
|
|
2028
|
+
}
|
|
2029
|
+
lastEnd = expr.end;
|
|
2030
|
+
}
|
|
2031
|
+
result += template.slice(lastEnd);
|
|
2032
|
+
return result;
|
|
2033
|
+
}
|
|
2034
|
+
function classifyLlmError(stepId, e) {
|
|
2035
|
+
if (APICallError.isInstance(e)) {
|
|
2036
|
+
const code = e.statusCode === 429 ? "LLM_RATE_LIMITED" : "LLM_API_ERROR";
|
|
2037
|
+
return new ExternalServiceError(stepId, code, e.message, e, e.statusCode, e.isRetryable ?? true);
|
|
2038
|
+
}
|
|
2039
|
+
if (RetryError.isInstance(e)) {
|
|
2040
|
+
return new ExternalServiceError(stepId, "LLM_API_ERROR", e.message, e, undefined, false);
|
|
2041
|
+
}
|
|
2042
|
+
if (NoContentGeneratedError.isInstance(e)) {
|
|
2043
|
+
return new ExternalServiceError(stepId, "LLM_NO_CONTENT", e.message, e, undefined, true);
|
|
2044
|
+
}
|
|
2045
|
+
if (TypeValidationError.isInstance(e)) {
|
|
2046
|
+
return new OutputQualityError(stepId, "LLM_OUTPUT_PARSE_ERROR", `LLM output could not be parsed: ${e.message}`, e.value, e);
|
|
2047
|
+
}
|
|
2048
|
+
if (JSONParseError.isInstance(e)) {
|
|
2049
|
+
return new OutputQualityError(stepId, "LLM_OUTPUT_PARSE_ERROR", `LLM output could not be parsed: ${e.message}`, e.text, e);
|
|
2050
|
+
}
|
|
2051
|
+
return new ExternalServiceError(stepId, "LLM_NETWORK_ERROR", e instanceof Error ? e.message : String(e), e, undefined, true);
|
|
2052
|
+
}
|
|
2053
|
+
function createProbeDataTool(sourceData, limits) {
|
|
2054
|
+
return tool({
|
|
2055
|
+
description: "Query the available data using a JMESPath expression. Returns the matching subset of the data.",
|
|
2056
|
+
inputSchema: arktype({
|
|
2057
|
+
expression: [
|
|
2058
|
+
"string",
|
|
2059
|
+
"@",
|
|
2060
|
+
"A JMESPath expression to evaluate against the data. Examples: 'users[0]', 'users[*].name', 'metadata.total', 'users[?age > `30`].name'"
|
|
2061
|
+
]
|
|
2062
|
+
}),
|
|
2063
|
+
execute: async ({ expression }) => {
|
|
2064
|
+
try {
|
|
2065
|
+
const result = search(sourceData, expression);
|
|
2066
|
+
const resultStr = typeof result === "string" ? result : JSON.stringify(result, null, 2);
|
|
2067
|
+
if (new TextEncoder().encode(resultStr).byteLength > limits.probeResultMaxBytes) {
|
|
2068
|
+
const truncated = resultStr.slice(0, limits.probeResultMaxBytes);
|
|
2069
|
+
return `${truncated}
|
|
2070
|
+
|
|
2071
|
+
[TRUNCATED - result exceeded ${limits.probeResultMaxBytes} bytes. Use a more specific JMESPath expression to narrow the result.]`;
|
|
2072
|
+
}
|
|
2073
|
+
return resultStr;
|
|
2074
|
+
} catch (e) {
|
|
2075
|
+
return `JMESPath error: ${e instanceof Error ? e.message : String(e)}. Check your expression syntax.`;
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
});
|
|
2079
|
+
}
|
|
2080
|
+
function createGiveUpTool() {
|
|
2081
|
+
let reason;
|
|
2082
|
+
const giveUpTool = tool({
|
|
2083
|
+
description: "Call this if you determine you cannot complete the task or find/extract the requested data.",
|
|
2084
|
+
inputSchema: arktype({
|
|
2085
|
+
reason: [
|
|
2086
|
+
"string",
|
|
2087
|
+
"@",
|
|
2088
|
+
"Explanation of why the task cannot be completed"
|
|
2089
|
+
]
|
|
2090
|
+
}),
|
|
2091
|
+
execute: async ({ reason: r }) => {
|
|
2092
|
+
reason = r;
|
|
2093
|
+
return { acknowledged: true };
|
|
2094
|
+
}
|
|
2095
|
+
});
|
|
2096
|
+
return {
|
|
2097
|
+
tool: giveUpTool,
|
|
2098
|
+
getReason: () => reason
|
|
2099
|
+
};
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
// src/executor/steps/agent-loop.ts
|
|
2103
|
+
async function executeAgentLoop(step, scope, options, limits) {
|
|
2104
|
+
if (!options.model) {
|
|
2105
|
+
throw new ConfigurationError(step.id, "AGENT_NOT_PROVIDED", "agent-loop steps require a LanguageModel to be provided");
|
|
2106
|
+
}
|
|
2107
|
+
const interpolatedInstructions = interpolateTemplate(step.params.instructions, scope, step.id);
|
|
2108
|
+
const outputSchema = jsonSchema(step.params.outputFormat);
|
|
2109
|
+
if (options.agent) {
|
|
2110
|
+
return executeWithAgent(step, interpolatedInstructions, options.agent, options.model, outputSchema, step.params.outputFormat);
|
|
2111
|
+
}
|
|
2112
|
+
return executeWithModel(step, interpolatedInstructions, scope, options, options.model, limits, outputSchema);
|
|
2113
|
+
}
|
|
2114
|
+
async function executeWithAgent(step, interpolatedInstructions, agent, model, outputSchema, rawOutputFormat) {
|
|
2115
|
+
const schemaStr = JSON.stringify(rawOutputFormat, null, 2);
|
|
2116
|
+
const prompt = `${interpolatedInstructions}
|
|
2117
|
+
|
|
2118
|
+
When you have completed the task, respond with your final answer. Your response should contain the following structured information matching this JSON Schema:
|
|
2119
|
+
\`\`\`json
|
|
2120
|
+
${schemaStr}
|
|
2121
|
+
\`\`\`
|
|
2122
|
+
|
|
2123
|
+
Include all the required fields in your response.`;
|
|
2124
|
+
try {
|
|
2125
|
+
const result = await agent.generate({ prompt });
|
|
2126
|
+
const giveUp = createGiveUpTool();
|
|
2127
|
+
const coerced = await generateText({
|
|
2128
|
+
model,
|
|
2129
|
+
output: Output.object({ schema: outputSchema }),
|
|
2130
|
+
tools: { "give-up": giveUp.tool },
|
|
2131
|
+
prompt: `Extract the structured data from the following text. Return only the data matching the schema. If the text does not contain enough information to populate the required fields, call the give-up tool with an explanation.
|
|
2132
|
+
|
|
2133
|
+
Text:
|
|
2134
|
+
${result.text}`
|
|
2135
|
+
});
|
|
2136
|
+
if (giveUp.getReason() !== undefined) {
|
|
2137
|
+
throw new ExtractionError(step.id, `Could not coerce agent output into expected schema: ${giveUp.getReason()}`, giveUp.getReason());
|
|
2138
|
+
}
|
|
2139
|
+
const trace = [
|
|
2140
|
+
...result.steps.map((s) => ({ type: "agent-step", step: s })),
|
|
2141
|
+
...coerced.steps.map((s) => ({ type: "agent-step", step: s }))
|
|
2142
|
+
];
|
|
2143
|
+
return { output: coerced.output, trace };
|
|
2144
|
+
} catch (e) {
|
|
2145
|
+
if (e instanceof StepExecutionError)
|
|
2146
|
+
throw e;
|
|
2147
|
+
throw classifyLlmError(step.id, e);
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
async function executeWithModel(step, interpolatedInstructions, scope, options, model, limits, outputSchema) {
|
|
2151
|
+
const subsetTools = {};
|
|
2152
|
+
for (const toolName of step.params.tools) {
|
|
2153
|
+
const toolDef = options.tools[toolName];
|
|
2154
|
+
if (!toolDef) {
|
|
2155
|
+
throw new ConfigurationError(step.id, "TOOL_NOT_FOUND", `agent-loop step references tool '${toolName}' which is not in the provided tool set`);
|
|
2156
|
+
}
|
|
2157
|
+
if (!toolDef.execute) {
|
|
2158
|
+
throw new ConfigurationError(step.id, "TOOL_MISSING_EXECUTE", `agent-loop step references tool '${toolName}' which has no execute function`);
|
|
2159
|
+
}
|
|
2160
|
+
subsetTools[toolName] = toolDef;
|
|
2161
|
+
}
|
|
2162
|
+
const probeDataTool = createProbeDataTool(scope, limits);
|
|
2163
|
+
const giveUp = createGiveUpTool();
|
|
2164
|
+
subsetTools["probe-data"] = probeDataTool;
|
|
2165
|
+
subsetTools["give-up"] = giveUp.tool;
|
|
2166
|
+
const maxSteps = step.params.maxSteps ? evaluateExpression(step.params.maxSteps, scope, step.id) : 10;
|
|
2167
|
+
if (typeof maxSteps !== "number" || maxSteps < 1) {
|
|
2168
|
+
throw new ValidationError(step.id, "TOOL_INPUT_VALIDATION_FAILED", `agent-loop maxSteps must be a positive number, got ${typeof maxSteps === "number" ? maxSteps : typeof maxSteps}`, maxSteps);
|
|
2169
|
+
}
|
|
2170
|
+
const agent = new ToolLoopAgent({
|
|
2171
|
+
model,
|
|
2172
|
+
tools: subsetTools,
|
|
2173
|
+
output: Output.object({ schema: outputSchema }),
|
|
2174
|
+
stopWhen: [
|
|
2175
|
+
() => giveUp?.getReason() !== undefined,
|
|
2176
|
+
stepCountIs(Math.floor(maxSteps))
|
|
2177
|
+
]
|
|
2178
|
+
});
|
|
2179
|
+
try {
|
|
2180
|
+
const result = await agent.generate({ prompt: interpolatedInstructions });
|
|
2181
|
+
if (giveUp.getReason() !== undefined) {
|
|
2182
|
+
throw new ExtractionError(step.id, `Agent gave up: ${giveUp.getReason()}`, giveUp.getReason());
|
|
2183
|
+
}
|
|
2184
|
+
const trace = result.steps.map((s) => ({
|
|
2185
|
+
type: "agent-step",
|
|
2186
|
+
step: s
|
|
2187
|
+
}));
|
|
2188
|
+
return { output: result.output, trace };
|
|
2189
|
+
} catch (e) {
|
|
2190
|
+
if (e instanceof StepExecutionError)
|
|
2191
|
+
throw e;
|
|
2192
|
+
throw classifyLlmError(step.id, e);
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
function resolveAgentLoopInputs(step, scope) {
|
|
2196
|
+
try {
|
|
2197
|
+
return {
|
|
2198
|
+
instructions: interpolateTemplate(step.params.instructions, scope, step.id)
|
|
2199
|
+
};
|
|
2200
|
+
} catch {
|
|
2201
|
+
return { instructions: step.params.instructions };
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
|
|
2205
|
+
// src/executor/steps/end.ts
|
|
2206
|
+
function executeEnd(step, scope) {
|
|
2207
|
+
if (step.params?.output) {
|
|
2208
|
+
return evaluateExpression(step.params.output, scope, step.id);
|
|
2209
|
+
}
|
|
2210
|
+
return;
|
|
2211
|
+
}
|
|
2212
|
+
|
|
2213
|
+
// src/executor/steps/extract-data.ts
|
|
2214
|
+
import { safeValidateTypes } from "@ai-sdk/provider-utils";
|
|
2215
|
+
import { search as search2 } from "@jmespath-community/jmespath";
|
|
2216
|
+
import {
|
|
2217
|
+
jsonSchema as jsonSchema2,
|
|
2218
|
+
Output as Output2,
|
|
2219
|
+
stepCountIs as stepCountIs2,
|
|
2220
|
+
ToolLoopAgent as ToolLoopAgent2,
|
|
2221
|
+
tool as tool2
|
|
2222
|
+
} from "ai";
|
|
2223
|
+
|
|
2224
|
+
// src/executor/schema-inference.ts
|
|
2225
|
+
function deepEqual(a, b) {
|
|
2226
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
2227
|
+
}
|
|
2228
|
+
function normalizeSchemaShape(schema) {
|
|
2229
|
+
if (schema === null || schema === undefined)
|
|
2230
|
+
return schema;
|
|
2231
|
+
if (typeof schema === "string")
|
|
2232
|
+
return schema;
|
|
2233
|
+
if (Array.isArray(schema)) {
|
|
2234
|
+
if (schema.length === 2 && typeof schema[1] === "number") {
|
|
2235
|
+
return [normalizeSchemaShape(schema[0]), 0];
|
|
2236
|
+
}
|
|
2237
|
+
return schema.map(normalizeSchemaShape);
|
|
2238
|
+
}
|
|
2239
|
+
if (typeof schema === "object") {
|
|
2240
|
+
const normalized = {};
|
|
2241
|
+
for (const [key, val] of Object.entries(schema)) {
|
|
2242
|
+
normalized[key] = normalizeSchemaShape(val);
|
|
2243
|
+
}
|
|
2244
|
+
return normalized;
|
|
2245
|
+
}
|
|
2246
|
+
return schema;
|
|
2247
|
+
}
|
|
2248
|
+
function mostCommon(arr) {
|
|
2249
|
+
if (arr.length === 0)
|
|
2250
|
+
return null;
|
|
2251
|
+
const counts = new Map;
|
|
2252
|
+
arr.forEach((item) => {
|
|
2253
|
+
let found = false;
|
|
2254
|
+
for (const [_idx, entry] of counts.entries()) {
|
|
2255
|
+
if (deepEqual(item, entry.element)) {
|
|
2256
|
+
entry.count++;
|
|
2257
|
+
found = true;
|
|
2258
|
+
break;
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
if (!found) {
|
|
2262
|
+
counts.set(counts.size, { element: item, count: 1 });
|
|
2263
|
+
}
|
|
2264
|
+
});
|
|
2265
|
+
let max = { element: arr[0], count: 0 };
|
|
2266
|
+
for (const entry of counts.values()) {
|
|
2267
|
+
if (entry.count > max.count)
|
|
2268
|
+
max = entry;
|
|
2269
|
+
}
|
|
2270
|
+
return max.element;
|
|
2271
|
+
}
|
|
2272
|
+
function inferSchema(value, types = [], maxKeys = 20) {
|
|
2273
|
+
let schema;
|
|
2274
|
+
if (value === null) {
|
|
2275
|
+
schema = "null";
|
|
2276
|
+
} else if (value === undefined) {
|
|
2277
|
+
schema = "undefined";
|
|
2278
|
+
} else if (typeof value === "object") {
|
|
2279
|
+
if (Array.isArray(value)) {
|
|
2280
|
+
const elementSchemas = value.map((element) => inferSchema(element, types, maxKeys));
|
|
2281
|
+
const mostCommonElementSchema = mostCommon(elementSchemas);
|
|
2282
|
+
const percentUniformSchemas = elementSchemas.filter((schema2) => deepEqual(schema2, mostCommonElementSchema)).length / elementSchemas.length;
|
|
2283
|
+
const allElementsHaveSameSchema = percentUniformSchemas >= 0.5;
|
|
2284
|
+
if (allElementsHaveSameSchema) {
|
|
2285
|
+
schema = [mostCommonElementSchema, value.length];
|
|
2286
|
+
} else {
|
|
2287
|
+
schema = elementSchemas;
|
|
2288
|
+
}
|
|
2289
|
+
} else {
|
|
2290
|
+
const keys = Object.keys(value);
|
|
2291
|
+
const totalKeys = keys.length;
|
|
2292
|
+
if (totalKeys >= 3) {
|
|
2293
|
+
const sampleKeys = keys.slice(0, maxKeys);
|
|
2294
|
+
const valueSchemas = sampleKeys.map((key) => inferSchema(value[key], [], maxKeys));
|
|
2295
|
+
const normalizedSchemas = valueSchemas.map(normalizeSchemaShape);
|
|
2296
|
+
const mostCommonNormalized = mostCommon(normalizedSchemas);
|
|
2297
|
+
if (mostCommonNormalized !== null && typeof mostCommonNormalized === "object" && mostCommonNormalized !== null) {
|
|
2298
|
+
const uniformCount = normalizedSchemas.filter((s) => deepEqual(s, mostCommonNormalized)).length;
|
|
2299
|
+
const percentUniform = uniformCount / sampleKeys.length;
|
|
2300
|
+
if (uniformCount >= 3 && percentUniform >= 0.75) {
|
|
2301
|
+
const representativeSchema = valueSchemas.find((s) => deepEqual(normalizeSchemaShape(s), mostCommonNormalized));
|
|
2302
|
+
schema = {
|
|
2303
|
+
__dict: [representativeSchema, totalKeys]
|
|
2304
|
+
};
|
|
2305
|
+
types.push(schema);
|
|
2306
|
+
return schema;
|
|
2307
|
+
}
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
const _schema = {};
|
|
2311
|
+
const keysToInclude = keys.slice(0, maxKeys);
|
|
2312
|
+
for (const key of keysToInclude) {
|
|
2313
|
+
_schema[key] = inferSchema(value[key], types, maxKeys);
|
|
2314
|
+
}
|
|
2315
|
+
if (totalKeys > maxKeys) {
|
|
2316
|
+
_schema[`...${totalKeys - maxKeys} more keys`] = "truncated";
|
|
2317
|
+
}
|
|
2318
|
+
schema = _schema;
|
|
2319
|
+
}
|
|
2320
|
+
} else if (typeof value === "string") {
|
|
2321
|
+
schema = "string";
|
|
2322
|
+
} else if (typeof value === "number") {
|
|
2323
|
+
schema = "number";
|
|
2324
|
+
} else if (typeof value === "boolean") {
|
|
2325
|
+
schema = "boolean";
|
|
2326
|
+
} else {
|
|
2327
|
+
schema = "unknown";
|
|
2328
|
+
}
|
|
2329
|
+
types.push(schema);
|
|
2330
|
+
return schema;
|
|
2331
|
+
}
|
|
2332
|
+
function schemaToString(schema, indent) {
|
|
2333
|
+
const indentStr = typeof indent === "number" ? " ".repeat(indent) : indent;
|
|
2334
|
+
const pretty = indentStr !== undefined;
|
|
2335
|
+
function stringify(value, depth = 0) {
|
|
2336
|
+
if (value === null)
|
|
2337
|
+
return "null";
|
|
2338
|
+
if (value === undefined)
|
|
2339
|
+
return "undefined";
|
|
2340
|
+
if (typeof value === "string")
|
|
2341
|
+
return value;
|
|
2342
|
+
if (Array.isArray(value)) {
|
|
2343
|
+
if (value.length === 2 && typeof value[1] === "number") {
|
|
2344
|
+
return `${stringify(value[0], depth)}[${value[1]}]`;
|
|
2345
|
+
}
|
|
2346
|
+
if (!pretty || !indentStr) {
|
|
2347
|
+
return `[${value.map((v) => stringify(v, depth)).join(", ")}]`;
|
|
2348
|
+
}
|
|
2349
|
+
const currentIndent = indentStr.repeat(depth);
|
|
2350
|
+
const nextIndent = indentStr.repeat(depth + 1);
|
|
2351
|
+
const items = value.map((v) => `${nextIndent}${stringify(v, depth + 1)}`).join(`,
|
|
2352
|
+
`);
|
|
2353
|
+
return `[
|
|
2354
|
+
${items}
|
|
2355
|
+
${currentIndent}]`;
|
|
2356
|
+
}
|
|
2357
|
+
if (typeof value === "object") {
|
|
2358
|
+
const entries = Object.entries(value);
|
|
2359
|
+
const first = entries[0];
|
|
2360
|
+
if (entries.length === 1 && first !== undefined && first[0] === "__dict" && Array.isArray(first[1]) && first[1].length === 2 && typeof first[1][1] === "number") {
|
|
2361
|
+
const [valueSchema, count] = first[1];
|
|
2362
|
+
if (!pretty || !indentStr) {
|
|
2363
|
+
return `{ [key]: ${stringify(valueSchema, depth)} }[${count}]`;
|
|
2364
|
+
}
|
|
2365
|
+
const currentIndent2 = indentStr.repeat(depth);
|
|
2366
|
+
const nextIndent2 = indentStr.repeat(depth + 1);
|
|
2367
|
+
return `{
|
|
2368
|
+
${nextIndent2}[key]: ${stringify(valueSchema, depth + 1)}
|
|
2369
|
+
${currentIndent2}}[${count}]`;
|
|
2370
|
+
}
|
|
2371
|
+
if (!pretty || !indentStr) {
|
|
2372
|
+
const formatted2 = entries.map(([key, val]) => `${key}: ${stringify(val, depth)}`).join(", ");
|
|
2373
|
+
return `{ ${formatted2} }`;
|
|
2374
|
+
}
|
|
2375
|
+
const currentIndent = indentStr.repeat(depth);
|
|
2376
|
+
const nextIndent = indentStr.repeat(depth + 1);
|
|
2377
|
+
const formatted = entries.map(([key, val]) => `${nextIndent}${key}: ${stringify(val, depth + 1)}`).join(`,
|
|
2378
|
+
`);
|
|
2379
|
+
return `{
|
|
2380
|
+
${formatted}
|
|
2381
|
+
${currentIndent}}`;
|
|
2382
|
+
}
|
|
2383
|
+
return JSON.stringify(value);
|
|
2384
|
+
}
|
|
2385
|
+
return stringify(schema);
|
|
2386
|
+
}
|
|
2387
|
+
function summarizeObjectStructure(value, indent) {
|
|
2388
|
+
const schema = inferSchema(value);
|
|
2389
|
+
return schemaToString(schema, indent);
|
|
2390
|
+
}
|
|
2391
|
+
|
|
2392
|
+
// src/executor/steps/extract-data.ts
|
|
2393
|
+
async function executeExtractData(step, scope, options, limits) {
|
|
2394
|
+
if (!options.model) {
|
|
2395
|
+
throw new ConfigurationError(step.id, "AGENT_NOT_PROVIDED", "extract-data steps require a LanguageModel to be provided");
|
|
2396
|
+
}
|
|
2397
|
+
const { model } = options;
|
|
2398
|
+
const sourceData = evaluateExpression(step.params.sourceData, scope, step.id);
|
|
2399
|
+
const sourceStr = typeof sourceData === "string" ? sourceData : JSON.stringify(sourceData, null, 2);
|
|
2400
|
+
const byteLength = new TextEncoder().encode(sourceStr).byteLength;
|
|
2401
|
+
let useProbeMode = byteLength > limits.probeThresholdBytes;
|
|
2402
|
+
if (useProbeMode && typeof sourceData === "string") {
|
|
2403
|
+
try {
|
|
2404
|
+
JSON.parse(sourceData);
|
|
2405
|
+
} catch {
|
|
2406
|
+
useProbeMode = false;
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2409
|
+
if (useProbeMode) {
|
|
2410
|
+
const structuredData = typeof sourceData === "string" ? JSON.parse(sourceData) : sourceData;
|
|
2411
|
+
return executeExtractDataProbe(step, structuredData, model, limits);
|
|
2412
|
+
}
|
|
2413
|
+
const agent = new ToolLoopAgent2({
|
|
2414
|
+
model,
|
|
2415
|
+
output: Output2.object({
|
|
2416
|
+
schema: jsonSchema2(step.params.outputFormat)
|
|
2417
|
+
}),
|
|
2418
|
+
stopWhen: stepCountIs2(1)
|
|
2419
|
+
});
|
|
2420
|
+
const prompt = `Extract the following structured data from the provided source data.
|
|
2421
|
+
|
|
2422
|
+
Source data:
|
|
2423
|
+
${sourceStr}`;
|
|
2424
|
+
try {
|
|
2425
|
+
const result = await agent.generate({ prompt });
|
|
2426
|
+
const trace = result.steps.map((s) => ({
|
|
2427
|
+
type: "agent-step",
|
|
2428
|
+
step: s
|
|
2429
|
+
}));
|
|
2430
|
+
return { output: result.output, trace };
|
|
2431
|
+
} catch (e) {
|
|
2432
|
+
if (e instanceof StepExecutionError)
|
|
2433
|
+
throw e;
|
|
2434
|
+
throw classifyLlmError(step.id, e);
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
async function executeExtractDataProbe(step, sourceData, model, limits) {
|
|
2438
|
+
const structureSummary = summarizeObjectStructure(sourceData, 2);
|
|
2439
|
+
let submittedResult;
|
|
2440
|
+
const outputSchema = jsonSchema2(step.params.outputFormat);
|
|
2441
|
+
const probeDataTool = createProbeDataTool(sourceData, limits);
|
|
2442
|
+
const giveUp = createGiveUpTool();
|
|
2443
|
+
const submitResultTool = tool2({
|
|
2444
|
+
description: "Submit the extracted data. Provide either `data` (the object directly) or `expression` (a JMESPath expression that evaluates to it). The result is validated against the target schema.",
|
|
2445
|
+
inputSchema: jsonSchema2({
|
|
2446
|
+
type: "object",
|
|
2447
|
+
properties: {
|
|
2448
|
+
data: { description: "The extracted data object directly" },
|
|
2449
|
+
expression: {
|
|
2450
|
+
type: "string",
|
|
2451
|
+
description: "A JMESPath expression that evaluates to the extracted data"
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2454
|
+
}),
|
|
2455
|
+
execute: async (input) => {
|
|
2456
|
+
let result2;
|
|
2457
|
+
if (input.expression !== undefined) {
|
|
2458
|
+
try {
|
|
2459
|
+
result2 = search2(sourceData, input.expression);
|
|
2460
|
+
} catch (e) {
|
|
2461
|
+
throw new Error(`JMESPath error: ${e instanceof Error ? e.message : String(e)}. Fix the expression and try again.`);
|
|
2462
|
+
}
|
|
2463
|
+
} else if (input.data !== undefined) {
|
|
2464
|
+
result2 = input.data;
|
|
2465
|
+
} else {
|
|
2466
|
+
throw new Error("Provide either `data` or `expression` to submit a result.");
|
|
2467
|
+
}
|
|
2468
|
+
const validation = await safeValidateTypes({
|
|
2469
|
+
value: result2,
|
|
2470
|
+
schema: outputSchema
|
|
2471
|
+
});
|
|
2472
|
+
if (!validation.success) {
|
|
2473
|
+
throw new Error(`Result does not match the target output schema: ${validation.error.message}. Fix the data or expression and try again.`);
|
|
2474
|
+
}
|
|
2475
|
+
submittedResult = validation.value;
|
|
2476
|
+
return { success: true };
|
|
2477
|
+
}
|
|
2478
|
+
});
|
|
2479
|
+
const agent = new ToolLoopAgent2({
|
|
2480
|
+
model,
|
|
2481
|
+
tools: {
|
|
2482
|
+
"probe-data": probeDataTool,
|
|
2483
|
+
"submit-result": submitResultTool,
|
|
2484
|
+
"give-up": giveUp.tool
|
|
2485
|
+
},
|
|
2486
|
+
stopWhen: [
|
|
2487
|
+
() => submittedResult !== undefined || giveUp.getReason() !== undefined,
|
|
2488
|
+
stepCountIs2(limits.probeMaxSteps)
|
|
2489
|
+
]
|
|
2490
|
+
});
|
|
2491
|
+
const schemaStr = JSON.stringify(step.params.outputFormat, null, 2);
|
|
2492
|
+
const prompt = `You need to extract structured data from a large dataset. The data is too large to include directly, so you have three tools:
|
|
2493
|
+
|
|
2494
|
+
- probe-data: Query the data with a JMESPath expression to explore its contents.
|
|
2495
|
+
- submit-result: Submit the final extraction. Pass either \`data\` (the object directly) or \`expression\` (a JMESPath expression that evaluates to it). The result is validated against the target schema — if invalid, you'll get an error and can retry.
|
|
2496
|
+
- give-up: Call this if you determine the requested data cannot be found or extracted.
|
|
2497
|
+
|
|
2498
|
+
## Data Structure Summary
|
|
2499
|
+
\`\`\`
|
|
2500
|
+
${structureSummary}
|
|
2501
|
+
\`\`\`
|
|
2502
|
+
|
|
2503
|
+
## Target Output Schema
|
|
2504
|
+
\`\`\`json
|
|
2505
|
+
${schemaStr}
|
|
2506
|
+
\`\`\`
|
|
2507
|
+
|
|
2508
|
+
## Instructions
|
|
2509
|
+
1. Use probe-data with JMESPath expressions to explore and extract values you need.
|
|
2510
|
+
2. When you have all the data, call submit-result with either the data directly or a JMESPath expression that produces it.
|
|
2511
|
+
3. If the data you need is not present or cannot be extracted, call give-up with a reason.`;
|
|
2512
|
+
let result;
|
|
2513
|
+
try {
|
|
2514
|
+
result = await agent.generate({ prompt });
|
|
2515
|
+
} catch (e) {
|
|
2516
|
+
if (e instanceof StepExecutionError)
|
|
2517
|
+
throw e;
|
|
2518
|
+
throw classifyLlmError(step.id, e);
|
|
2519
|
+
}
|
|
2520
|
+
const trace = result.steps.map((s) => ({
|
|
2521
|
+
type: "agent-step",
|
|
2522
|
+
step: s
|
|
2523
|
+
}));
|
|
2524
|
+
if (submittedResult !== undefined) {
|
|
2525
|
+
return { output: submittedResult, trace };
|
|
2526
|
+
}
|
|
2527
|
+
if (giveUp.getReason() !== undefined) {
|
|
2528
|
+
throw new ExtractionError(step.id, `LLM was unable to extract the requested data: ${giveUp.getReason()}`, giveUp.getReason());
|
|
2529
|
+
}
|
|
2530
|
+
throw new OutputQualityError(step.id, "LLM_OUTPUT_PARSE_ERROR", "extract-data probe mode exhausted all steps without submitting a result", undefined);
|
|
2531
|
+
}
|
|
2532
|
+
function resolveExtractDataInputs(step, scope) {
|
|
2533
|
+
try {
|
|
2534
|
+
return {
|
|
2535
|
+
sourceData: evaluateExpression(step.params.sourceData, scope, step.id)
|
|
2536
|
+
};
|
|
2537
|
+
} catch {
|
|
2538
|
+
return;
|
|
2539
|
+
}
|
|
2540
|
+
}
|
|
2541
|
+
|
|
2542
|
+
// src/executor/steps/for-each.ts
|
|
2543
|
+
async function executeForEach(step, scope, stepIndex, stepOutputs, loopVars, options, context, stateManager, execPath, executeChain) {
|
|
2544
|
+
const target = evaluateExpression(step.params.target, scope, step.id);
|
|
2545
|
+
if (!Array.isArray(target)) {
|
|
2546
|
+
throw new ValidationError(step.id, "FOREACH_TARGET_NOT_ARRAY", `for-each target must be an array, got ${typeof target}`, target);
|
|
2547
|
+
}
|
|
2548
|
+
const results = [];
|
|
2549
|
+
for (let i = 0;i < target.length; i++) {
|
|
2550
|
+
const item = target[i];
|
|
2551
|
+
const iterationPath = [
|
|
2552
|
+
...execPath,
|
|
2553
|
+
{
|
|
2554
|
+
type: "for-each",
|
|
2555
|
+
stepId: step.id,
|
|
2556
|
+
iterationIndex: i,
|
|
2557
|
+
itemValue: item
|
|
2558
|
+
}
|
|
2559
|
+
];
|
|
2560
|
+
const innerLoopVars = { ...loopVars, [step.params.itemName]: item };
|
|
2561
|
+
const lastOutput = await executeChain(step.params.loopBodyStepId, stepIndex, stepOutputs, innerLoopVars, options, context, undefined, stateManager, iterationPath);
|
|
2562
|
+
results.push(lastOutput);
|
|
2563
|
+
}
|
|
2564
|
+
return results;
|
|
2565
|
+
}
|
|
2566
|
+
function resolveForEachInputs(step, scope) {
|
|
2567
|
+
try {
|
|
2568
|
+
return {
|
|
2569
|
+
target: evaluateExpression(step.params.target, scope, step.id)
|
|
2570
|
+
};
|
|
2571
|
+
} catch {
|
|
2572
|
+
return;
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
|
|
2576
|
+
// src/executor/steps/llm-prompt.ts
|
|
2577
|
+
import { jsonSchema as jsonSchema3, Output as Output3, stepCountIs as stepCountIs3, ToolLoopAgent as ToolLoopAgent3 } from "ai";
|
|
2578
|
+
async function executeLlmPrompt(step, scope, options) {
|
|
2579
|
+
if (!options.model) {
|
|
2580
|
+
throw new ConfigurationError(step.id, "AGENT_NOT_PROVIDED", "llm-prompt steps require a LanguageModel to be provided");
|
|
2581
|
+
}
|
|
2582
|
+
const prompt = interpolateTemplate(step.params.prompt, scope, step.id);
|
|
2583
|
+
const agent = new ToolLoopAgent3({
|
|
2584
|
+
model: options.model,
|
|
2585
|
+
output: Output3.object({
|
|
2586
|
+
schema: jsonSchema3(step.params.outputFormat)
|
|
2587
|
+
}),
|
|
2588
|
+
stopWhen: stepCountIs3(1)
|
|
2589
|
+
});
|
|
2590
|
+
try {
|
|
2591
|
+
const result = await agent.generate({ prompt });
|
|
2592
|
+
const trace = result.steps.map((s) => ({
|
|
2593
|
+
type: "agent-step",
|
|
2594
|
+
step: s
|
|
2595
|
+
}));
|
|
2596
|
+
return { output: result.output, trace };
|
|
2597
|
+
} catch (e) {
|
|
2598
|
+
if (e instanceof StepExecutionError)
|
|
2599
|
+
throw e;
|
|
2600
|
+
throw classifyLlmError(step.id, e);
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
function resolveLlmPromptInputs(step, scope) {
|
|
2604
|
+
try {
|
|
2605
|
+
return {
|
|
2606
|
+
prompt: interpolateTemplate(step.params.prompt, scope, step.id)
|
|
2607
|
+
};
|
|
2608
|
+
} catch {
|
|
2609
|
+
return { prompt: step.params.prompt };
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2612
|
+
|
|
2613
|
+
// src/executor/steps/sleep.ts
|
|
2614
|
+
async function executeSleep(step, scope, context, timer) {
|
|
2615
|
+
const durationMs = evaluateExpression(step.params.durationMs, scope, step.id);
|
|
2616
|
+
if (typeof durationMs !== "number" || durationMs < 0) {
|
|
2617
|
+
throw new ValidationError(step.id, "SLEEP_INVALID_DURATION", `sleep durationMs must be a non-negative number, got ${typeof durationMs === "number" ? durationMs : typeof durationMs}`, durationMs);
|
|
2618
|
+
}
|
|
2619
|
+
const clamped = Math.min(durationMs, timer.resolvedLimits.maxSleepMs);
|
|
2620
|
+
await context.sleep(step.id, clamped);
|
|
2621
|
+
}
|
|
2622
|
+
|
|
2623
|
+
// src/executor/steps/start.ts
|
|
2624
|
+
function executeStart() {
|
|
2625
|
+
return;
|
|
2626
|
+
}
|
|
2627
|
+
|
|
2628
|
+
// src/executor/steps/switch-case.ts
|
|
2629
|
+
async function executeSwitchCase(step, scope, stepIndex, stepOutputs, loopVars, options, context, stateManager, execPath, executeChain) {
|
|
2630
|
+
const switchValue = evaluateExpression(step.params.switchOn, scope, step.id);
|
|
2631
|
+
let matchedBranchId;
|
|
2632
|
+
let defaultBranchId;
|
|
2633
|
+
let matchedCaseIndex = -1;
|
|
2634
|
+
for (let i = 0;i < step.params.cases.length; i++) {
|
|
2635
|
+
const c = step.params.cases[i];
|
|
2636
|
+
if (c.value.type === "default") {
|
|
2637
|
+
defaultBranchId = c.branchBodyStepId;
|
|
2638
|
+
if (matchedCaseIndex === -1)
|
|
2639
|
+
matchedCaseIndex = i;
|
|
2640
|
+
} else {
|
|
2641
|
+
const caseValue = evaluateExpression(c.value, scope, step.id);
|
|
2642
|
+
if (caseValue === switchValue) {
|
|
2643
|
+
matchedBranchId = c.branchBodyStepId;
|
|
2644
|
+
matchedCaseIndex = i;
|
|
2645
|
+
break;
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
const selectedBranchId = matchedBranchId ?? defaultBranchId;
|
|
2650
|
+
if (!selectedBranchId) {
|
|
2651
|
+
return;
|
|
2652
|
+
}
|
|
2653
|
+
const branchPath = [
|
|
2654
|
+
...execPath,
|
|
2655
|
+
{
|
|
2656
|
+
type: "switch-case",
|
|
2657
|
+
stepId: step.id,
|
|
2658
|
+
matchedCaseIndex,
|
|
2659
|
+
matchedValue: switchValue
|
|
2660
|
+
}
|
|
2661
|
+
];
|
|
2662
|
+
return await executeChain(selectedBranchId, stepIndex, stepOutputs, loopVars, options, context, undefined, stateManager, branchPath);
|
|
2663
|
+
}
|
|
2664
|
+
function resolveSwitchCaseInputs(step, scope) {
|
|
2665
|
+
try {
|
|
2666
|
+
return {
|
|
2667
|
+
switchOn: evaluateExpression(step.params.switchOn, scope, step.id)
|
|
2668
|
+
};
|
|
2669
|
+
} catch {
|
|
2670
|
+
return;
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2673
|
+
|
|
2674
|
+
// src/executor/steps/tool-call.ts
|
|
2675
|
+
import { safeValidateTypes as safeValidateTypes2 } from "@ai-sdk/provider-utils";
|
|
2676
|
+
async function executeToolCall(step, scope, tools) {
|
|
2677
|
+
const toolDef = tools[step.params.toolName];
|
|
2678
|
+
if (!toolDef?.execute) {
|
|
2679
|
+
throw new ConfigurationError(step.id, "TOOL_NOT_FOUND", `Tool '${step.params.toolName}' not found or has no execute function`);
|
|
2680
|
+
}
|
|
2681
|
+
const resolvedInput = {};
|
|
2682
|
+
for (const [key, expr] of Object.entries(step.params.toolInput)) {
|
|
2683
|
+
resolvedInput[key] = evaluateExpression(expr, scope, step.id);
|
|
2684
|
+
}
|
|
2685
|
+
if (toolDef.inputSchema) {
|
|
2686
|
+
const validation = await safeValidateTypes2({
|
|
2687
|
+
value: resolvedInput,
|
|
2688
|
+
schema: toolDef.inputSchema
|
|
2689
|
+
});
|
|
2690
|
+
if (!validation.success) {
|
|
2691
|
+
throw new ValidationError(step.id, "TOOL_INPUT_VALIDATION_FAILED", `Tool '${step.params.toolName}' input validation failed: ${validation.error.message}`, resolvedInput, validation.error);
|
|
2692
|
+
}
|
|
2693
|
+
}
|
|
2694
|
+
try {
|
|
2695
|
+
return await toolDef.execute(resolvedInput, {
|
|
2696
|
+
toolCallId: step.id,
|
|
2697
|
+
messages: []
|
|
2698
|
+
});
|
|
2699
|
+
} catch (e) {
|
|
2700
|
+
throw new ExternalServiceError(step.id, "TOOL_EXECUTION_FAILED", e instanceof Error ? e.message : String(e), e);
|
|
2701
|
+
}
|
|
2702
|
+
}
|
|
2703
|
+
function resolveToolCallInputs(step, scope) {
|
|
2704
|
+
const resolved = {};
|
|
2705
|
+
for (const [key, expr] of Object.entries(step.params.toolInput)) {
|
|
2706
|
+
try {
|
|
2707
|
+
resolved[key] = evaluateExpression(expr, scope, step.id);
|
|
2708
|
+
} catch {
|
|
2709
|
+
resolved[key] = `<error resolving ${key}>`;
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
2712
|
+
return resolved;
|
|
2713
|
+
}
|
|
2714
|
+
|
|
2715
|
+
// src/executor/steps/wait-for-condition.ts
|
|
2716
|
+
async function executeWaitForCondition(step, scope, stepIndex, stepOutputs, loopVars, options, context, timer, stateManager, execPath, executeChain) {
|
|
2717
|
+
const limits = timer.resolvedLimits;
|
|
2718
|
+
const maxAttempts = Math.min(step.params.maxAttempts ? evaluateExpression(step.params.maxAttempts, scope, step.id) : 10, limits.maxAttempts);
|
|
2719
|
+
const intervalMs = Math.min(step.params.intervalMs ? evaluateExpression(step.params.intervalMs, scope, step.id) : 1000, limits.maxSleepMs);
|
|
2720
|
+
const rawBackoff = step.params.backoffMultiplier ? evaluateExpression(step.params.backoffMultiplier, scope, step.id) : 1;
|
|
2721
|
+
const backoffMultiplier = Math.max(limits.minBackoffMultiplier, Math.min(rawBackoff, limits.maxBackoffMultiplier));
|
|
2722
|
+
const timeoutMs = step.params.timeoutMs ? Math.min(evaluateExpression(step.params.timeoutMs, scope, step.id), limits.maxTimeoutMs) : undefined;
|
|
2723
|
+
let pollAttempt = 0;
|
|
2724
|
+
return context.waitForCondition(step.id, async () => {
|
|
2725
|
+
const pollPath = [
|
|
2726
|
+
...execPath,
|
|
2727
|
+
{
|
|
2728
|
+
type: "wait-for-condition",
|
|
2729
|
+
stepId: step.id,
|
|
2730
|
+
pollAttempt: pollAttempt++
|
|
2731
|
+
}
|
|
2732
|
+
];
|
|
2733
|
+
timer.beginActive();
|
|
2734
|
+
try {
|
|
2735
|
+
await executeChain(step.params.conditionStepId, stepIndex, stepOutputs, loopVars, options, context, undefined, stateManager, pollPath);
|
|
2736
|
+
const updatedScope = { ...stepOutputs, ...loopVars };
|
|
2737
|
+
return evaluateExpression(step.params.condition, updatedScope, step.id);
|
|
2738
|
+
} finally {
|
|
2739
|
+
timer.endActive(step.id);
|
|
2740
|
+
}
|
|
2741
|
+
}, { maxAttempts, intervalMs, backoffMultiplier, timeoutMs });
|
|
2742
|
+
}
|
|
2743
|
+
|
|
2744
|
+
// src/executor/index.ts
|
|
2745
|
+
function validateWorkflowInputs(inputSchema, inputs) {
|
|
2746
|
+
if (!inputSchema || typeof inputSchema !== "object")
|
|
2747
|
+
return;
|
|
2748
|
+
const required = inputSchema.required;
|
|
2749
|
+
if (Array.isArray(required)) {
|
|
2750
|
+
const missing = required.filter((key) => typeof key === "string" && !(key in inputs));
|
|
2751
|
+
if (missing.length > 0) {
|
|
2752
|
+
throw new ValidationError("input", "TOOL_INPUT_VALIDATION_FAILED", `Workflow input validation failed: missing required input(s): ${missing.join(", ")}`, inputs);
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
const properties = inputSchema.properties;
|
|
2756
|
+
if (properties && typeof properties === "object") {
|
|
2757
|
+
for (const [key, value] of Object.entries(inputs)) {
|
|
2758
|
+
const propSchema = properties[key];
|
|
2759
|
+
if (propSchema && typeof propSchema === "object" && "type" in propSchema) {
|
|
2760
|
+
const expectedType = propSchema.type;
|
|
2761
|
+
const actualType = typeof value;
|
|
2762
|
+
if (expectedType === "integer" || expectedType === "number") {
|
|
2763
|
+
if (actualType !== "number") {
|
|
2764
|
+
throw new ValidationError("input", "TOOL_INPUT_VALIDATION_FAILED", `Workflow input validation failed: input '${key}' expected type '${expectedType}' but got '${actualType}'`, inputs);
|
|
2765
|
+
}
|
|
2766
|
+
} else if (expectedType === "array") {
|
|
2767
|
+
if (!Array.isArray(value)) {
|
|
2768
|
+
throw new ValidationError("input", "TOOL_INPUT_VALIDATION_FAILED", `Workflow input validation failed: input '${key}' expected type 'array' but got '${actualType}'`, inputs);
|
|
2769
|
+
}
|
|
2770
|
+
} else if (actualType !== expectedType) {
|
|
2771
|
+
throw new ValidationError("input", "TOOL_INPUT_VALIDATION_FAILED", `Workflow input validation failed: input '${key}' expected type '${expectedType}' but got '${actualType}'`, inputs);
|
|
2772
|
+
}
|
|
2773
|
+
}
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
function validateWorkflowOutput(outputSchema, output, endStepId) {
|
|
2778
|
+
const expectedType = outputSchema.type;
|
|
2779
|
+
if (typeof expectedType === "string") {
|
|
2780
|
+
if (expectedType === "object" && (typeof output !== "object" || output === null)) {
|
|
2781
|
+
throw new ValidationError(endStepId, "WORKFLOW_OUTPUT_VALIDATION_FAILED", `Workflow output validation failed: expected type 'object' but got '${output === null ? "null" : typeof output}'`, output);
|
|
2782
|
+
}
|
|
2783
|
+
if (expectedType === "array" && !Array.isArray(output)) {
|
|
2784
|
+
throw new ValidationError(endStepId, "WORKFLOW_OUTPUT_VALIDATION_FAILED", `Workflow output validation failed: expected type 'array' but got '${typeof output}'`, output);
|
|
2785
|
+
}
|
|
2786
|
+
if ((expectedType === "string" || expectedType === "boolean") && typeof output !== expectedType) {
|
|
2787
|
+
throw new ValidationError(endStepId, "WORKFLOW_OUTPUT_VALIDATION_FAILED", `Workflow output validation failed: expected type '${expectedType}' but got '${typeof output}'`, output);
|
|
2788
|
+
}
|
|
2789
|
+
if ((expectedType === "number" || expectedType === "integer") && typeof output !== "number") {
|
|
2790
|
+
throw new ValidationError(endStepId, "WORKFLOW_OUTPUT_VALIDATION_FAILED", `Workflow output validation failed: expected type '${expectedType}' but got '${typeof output}'`, output);
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
if (typeof output === "object" && output !== null && !Array.isArray(output)) {
|
|
2794
|
+
const required = outputSchema.required;
|
|
2795
|
+
if (Array.isArray(required)) {
|
|
2796
|
+
const missing = required.filter((key) => typeof key === "string" && !(key in output));
|
|
2797
|
+
if (missing.length > 0) {
|
|
2798
|
+
throw new ValidationError(endStepId, "WORKFLOW_OUTPUT_VALIDATION_FAILED", `Workflow output validation failed: missing required field(s): ${missing.join(", ")}`, output);
|
|
2799
|
+
}
|
|
2800
|
+
}
|
|
2801
|
+
const properties = outputSchema.properties;
|
|
2802
|
+
if (properties && typeof properties === "object") {
|
|
2803
|
+
for (const [key, value] of Object.entries(output)) {
|
|
2804
|
+
const propSchema = properties[key];
|
|
2805
|
+
if (propSchema && typeof propSchema === "object" && "type" in propSchema) {
|
|
2806
|
+
const propExpectedType = propSchema.type;
|
|
2807
|
+
const actualType = typeof value;
|
|
2808
|
+
if (propExpectedType === "integer" || propExpectedType === "number") {
|
|
2809
|
+
if (actualType !== "number") {
|
|
2810
|
+
throw new ValidationError(endStepId, "WORKFLOW_OUTPUT_VALIDATION_FAILED", `Workflow output validation failed: field '${key}' expected type '${propExpectedType}' but got '${actualType}'`, output);
|
|
2811
|
+
}
|
|
2812
|
+
} else if (propExpectedType === "array") {
|
|
2813
|
+
if (!Array.isArray(value)) {
|
|
2814
|
+
throw new ValidationError(endStepId, "WORKFLOW_OUTPUT_VALIDATION_FAILED", `Workflow output validation failed: field '${key}' expected type 'array' but got '${actualType}'`, output);
|
|
2815
|
+
}
|
|
2816
|
+
} else if (actualType !== propExpectedType) {
|
|
2817
|
+
throw new ValidationError(endStepId, "WORKFLOW_OUTPUT_VALIDATION_FAILED", `Workflow output validation failed: field '${key}' expected type '${propExpectedType}' but got '${actualType}'`, output);
|
|
2818
|
+
}
|
|
2819
|
+
}
|
|
2820
|
+
}
|
|
2821
|
+
}
|
|
2822
|
+
}
|
|
2823
|
+
}
|
|
2824
|
+
function resolveStepInputs(step, scope) {
|
|
2825
|
+
switch (step.type) {
|
|
2826
|
+
case "tool-call":
|
|
2827
|
+
return resolveToolCallInputs(step, scope);
|
|
2828
|
+
case "llm-prompt":
|
|
2829
|
+
return resolveLlmPromptInputs(step, scope);
|
|
2830
|
+
case "extract-data":
|
|
2831
|
+
return resolveExtractDataInputs(step, scope);
|
|
2832
|
+
case "switch-case":
|
|
2833
|
+
return resolveSwitchCaseInputs(step, scope);
|
|
2834
|
+
case "for-each":
|
|
2835
|
+
return resolveForEachInputs(step, scope);
|
|
2836
|
+
case "agent-loop":
|
|
2837
|
+
return resolveAgentLoopInputs(step, scope);
|
|
2838
|
+
default:
|
|
2839
|
+
return;
|
|
2840
|
+
}
|
|
2841
|
+
}
|
|
2842
|
+
async function executeStep(step, scope, stepIndex, stepOutputs, loopVars, options, context, timer, stateManager, execPath = []) {
|
|
2843
|
+
switch (step.type) {
|
|
2844
|
+
case "llm-prompt":
|
|
2845
|
+
return executeLlmPrompt(step, scope, options);
|
|
2846
|
+
case "extract-data":
|
|
2847
|
+
return executeExtractData(step, scope, options, timer.resolvedLimits);
|
|
2848
|
+
case "agent-loop":
|
|
2849
|
+
return executeAgentLoop(step, scope, options, timer.resolvedLimits);
|
|
2850
|
+
case "tool-call":
|
|
2851
|
+
return { output: await executeToolCall(step, scope, options.tools) };
|
|
2852
|
+
case "switch-case":
|
|
2853
|
+
return {
|
|
2854
|
+
output: await executeSwitchCase(step, scope, stepIndex, stepOutputs, loopVars, options, context, stateManager, execPath, executeChain)
|
|
2855
|
+
};
|
|
2856
|
+
case "for-each":
|
|
2857
|
+
return {
|
|
2858
|
+
output: await executeForEach(step, scope, stepIndex, stepOutputs, loopVars, options, context, stateManager, execPath, executeChain)
|
|
2859
|
+
};
|
|
2860
|
+
case "sleep":
|
|
2861
|
+
return { output: await executeSleep(step, scope, context, timer) };
|
|
2862
|
+
case "wait-for-condition":
|
|
2863
|
+
return {
|
|
2864
|
+
output: await executeWaitForCondition(step, scope, stepIndex, stepOutputs, loopVars, options, context, timer, stateManager, execPath, executeChain)
|
|
2865
|
+
};
|
|
2866
|
+
case "start":
|
|
2867
|
+
return { output: await executeStart() };
|
|
2868
|
+
case "end":
|
|
2869
|
+
return { output: await executeEnd(step, scope) };
|
|
2870
|
+
}
|
|
2871
|
+
}
|
|
2872
|
+
var DEFAULT_MAX_RETRIES = 3;
|
|
2873
|
+
var DEFAULT_BASE_DELAY_MS = 1000;
|
|
2874
|
+
async function retryStep(step, stepIndex, stepOutputs, loopVars, options, originalError, context, timer, stateManager, execPath = []) {
|
|
2875
|
+
const maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
2876
|
+
const baseDelay = options.retryDelayMs ?? DEFAULT_BASE_DELAY_MS;
|
|
2877
|
+
const scope = { ...stepOutputs, ...loopVars };
|
|
2878
|
+
for (let attempt = 1;attempt <= maxRetries; attempt++) {
|
|
2879
|
+
const retryStartedAt = new Date().toISOString();
|
|
2880
|
+
await context.sleep(`${step.id}_retry_${attempt}`, baseDelay * 2 ** (attempt - 1));
|
|
2881
|
+
try {
|
|
2882
|
+
return await executeStep(step, scope, stepIndex, stepOutputs, loopVars, options, context, timer, stateManager, execPath);
|
|
2883
|
+
} catch (e) {
|
|
2884
|
+
stateManager?.retryAttempted(step.id, execPath, {
|
|
2885
|
+
attempt,
|
|
2886
|
+
startedAt: retryStartedAt,
|
|
2887
|
+
failedAt: new Date().toISOString(),
|
|
2888
|
+
errorCode: e instanceof StepExecutionError ? e.code : "UNKNOWN",
|
|
2889
|
+
errorMessage: e instanceof Error ? e.message : String(e)
|
|
2890
|
+
});
|
|
2891
|
+
if (attempt === maxRetries)
|
|
2892
|
+
throw originalError;
|
|
2893
|
+
}
|
|
2894
|
+
}
|
|
2895
|
+
throw originalError;
|
|
2896
|
+
}
|
|
2897
|
+
async function recoverFromError(error, step, stepIndex, stepOutputs, loopVars, options, context, timer, stateManager, execPath = []) {
|
|
2898
|
+
switch (error.code) {
|
|
2899
|
+
case "LLM_RATE_LIMITED":
|
|
2900
|
+
case "LLM_NETWORK_ERROR":
|
|
2901
|
+
case "LLM_NO_CONTENT":
|
|
2902
|
+
case "LLM_OUTPUT_PARSE_ERROR":
|
|
2903
|
+
return retryStep(step, stepIndex, stepOutputs, loopVars, options, error, context, timer, stateManager, execPath);
|
|
2904
|
+
case "LLM_API_ERROR":
|
|
2905
|
+
if (error instanceof ExternalServiceError && error.isRetryable) {
|
|
2906
|
+
return retryStep(step, stepIndex, stepOutputs, loopVars, options, error, context, timer, stateManager, execPath);
|
|
2907
|
+
}
|
|
2908
|
+
throw error;
|
|
2909
|
+
default:
|
|
2910
|
+
throw error;
|
|
2911
|
+
}
|
|
2912
|
+
}
|
|
2913
|
+
var executeChain = async function executeChain2(startStepId, stepIndex, stepOutputs, loopVars, options, context, timer, stateManager, execPath = []) {
|
|
2914
|
+
let currentStepId = startStepId;
|
|
2915
|
+
let lastOutput;
|
|
2916
|
+
while (currentStepId) {
|
|
2917
|
+
const step = stepIndex.get(currentStepId);
|
|
2918
|
+
if (!step) {
|
|
2919
|
+
throw new Error(`Step '${currentStepId}' not found`);
|
|
2920
|
+
}
|
|
2921
|
+
timer?.checkTotal(step.id);
|
|
2922
|
+
const stepStartTime = Date.now();
|
|
2923
|
+
stateManager?.stepStarted(step.id, execPath);
|
|
2924
|
+
options.onStepStart?.(step.id, step);
|
|
2925
|
+
const scope = { ...stepOutputs, ...loopVars };
|
|
2926
|
+
const resolvedInputs = stateManager ? resolveStepInputs(step, scope) : undefined;
|
|
2927
|
+
let stepResult;
|
|
2928
|
+
try {
|
|
2929
|
+
timer?.beginActive();
|
|
2930
|
+
try {
|
|
2931
|
+
stepResult = await context.step(step.id, () => executeStep(step, scope, stepIndex, stepOutputs, loopVars, options, context, timer ?? new ExecutionTimer, stateManager, execPath));
|
|
2932
|
+
} finally {
|
|
2933
|
+
timer?.endActive(step.id);
|
|
2934
|
+
}
|
|
2935
|
+
} catch (e) {
|
|
2936
|
+
if (!(e instanceof StepExecutionError)) {
|
|
2937
|
+
const durationMs2 = Date.now() - stepStartTime;
|
|
2938
|
+
const wrappedError = new ExternalServiceError(step.id, "TOOL_EXECUTION_FAILED", e instanceof Error ? e.message : String(e), e, undefined, false);
|
|
2939
|
+
stateManager?.stepFailed(step.id, execPath, wrappedError, durationMs2, resolvedInputs);
|
|
2940
|
+
throw e;
|
|
2941
|
+
}
|
|
2942
|
+
stepResult = await recoverFromError(e, step, stepIndex, stepOutputs, loopVars, options, context, timer ?? new ExecutionTimer, stateManager, execPath);
|
|
2943
|
+
}
|
|
2944
|
+
const durationMs = Date.now() - stepStartTime;
|
|
2945
|
+
stepOutputs[step.id] = stepResult.output;
|
|
2946
|
+
lastOutput = stepResult.output;
|
|
2947
|
+
stateManager?.stepCompleted(step.id, execPath, stepResult.output, durationMs, resolvedInputs, stepResult.trace);
|
|
2948
|
+
options.onStepComplete?.(step.id, stepResult.output);
|
|
2949
|
+
currentStepId = step.nextStepId;
|
|
2950
|
+
}
|
|
2951
|
+
return lastOutput;
|
|
2952
|
+
};
|
|
2953
|
+
function validateWorkflowConfig(workflow, options) {
|
|
2954
|
+
const needsAgent = workflow.steps.some((s) => s.type === "llm-prompt" || s.type === "extract-data" || s.type === "agent-loop");
|
|
2955
|
+
if (needsAgent && !options.model) {
|
|
2956
|
+
const llmStep = workflow.steps.find((s) => s.type === "llm-prompt" || s.type === "extract-data" || s.type === "agent-loop");
|
|
2957
|
+
throw new ConfigurationError(llmStep?.id ?? "unknown", "AGENT_NOT_PROVIDED", "Workflow contains LLM/agent steps but no agent was provided");
|
|
2958
|
+
}
|
|
2959
|
+
for (const step of workflow.steps) {
|
|
2960
|
+
if (step.type === "tool-call") {
|
|
2961
|
+
const toolDef = options.tools[step.params.toolName];
|
|
2962
|
+
if (!toolDef) {
|
|
2963
|
+
throw new ConfigurationError(step.id, "TOOL_NOT_FOUND", `Tool '${step.params.toolName}' not found`);
|
|
2964
|
+
}
|
|
2965
|
+
if (!toolDef.execute) {
|
|
2966
|
+
throw new ConfigurationError(step.id, "TOOL_MISSING_EXECUTE", `Tool '${step.params.toolName}' has no execute function`);
|
|
2967
|
+
}
|
|
2968
|
+
}
|
|
2969
|
+
if (step.type === "agent-loop" && !options.agent) {
|
|
2970
|
+
for (const toolName of step.params.tools) {
|
|
2971
|
+
const toolDef = options.tools[toolName];
|
|
2972
|
+
if (!toolDef) {
|
|
2973
|
+
throw new ConfigurationError(step.id, "TOOL_NOT_FOUND", `Tool '${toolName}' referenced in agent-loop step not found`);
|
|
2974
|
+
}
|
|
2975
|
+
if (!toolDef.execute) {
|
|
2976
|
+
throw new ConfigurationError(step.id, "TOOL_MISSING_EXECUTE", `Tool '${toolName}' referenced in agent-loop step has no execute function`);
|
|
2977
|
+
}
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
}
|
|
2981
|
+
}
|
|
2982
|
+
async function executeWorkflow(workflow, options) {
|
|
2983
|
+
const stepIndex = new Map;
|
|
2984
|
+
for (const step of workflow.steps) {
|
|
2985
|
+
stepIndex.set(step.id, step);
|
|
2986
|
+
}
|
|
2987
|
+
const stepOutputs = {};
|
|
2988
|
+
const resolvedOptions = options;
|
|
2989
|
+
const resolvedContext = options.context ?? createDefaultDurableContext();
|
|
2990
|
+
const timer = new ExecutionTimer(options.limits);
|
|
2991
|
+
const stateManager = new ExecutionStateManager(options.onStateChange);
|
|
2992
|
+
stateManager.runStarted();
|
|
2993
|
+
try {
|
|
2994
|
+
validateWorkflowConfig(workflow, resolvedOptions);
|
|
2995
|
+
const inputs = options.inputs ?? {};
|
|
2996
|
+
validateWorkflowInputs(workflow.inputSchema, inputs);
|
|
2997
|
+
stepOutputs.input = inputs;
|
|
2998
|
+
const chainOutput = await executeChain(workflow.initialStepId, stepIndex, stepOutputs, {}, resolvedOptions, resolvedContext, timer, stateManager, []);
|
|
2999
|
+
if (workflow.outputSchema) {
|
|
3000
|
+
let endStepId = "unknown";
|
|
3001
|
+
for (const step of workflow.steps) {
|
|
3002
|
+
if (step.type === "end" && step.id in stepOutputs) {
|
|
3003
|
+
endStepId = step.id;
|
|
3004
|
+
}
|
|
3005
|
+
}
|
|
3006
|
+
validateWorkflowOutput(workflow.outputSchema, chainOutput, endStepId);
|
|
3007
|
+
}
|
|
3008
|
+
stateManager.runCompleted(chainOutput);
|
|
3009
|
+
return {
|
|
3010
|
+
success: true,
|
|
3011
|
+
stepOutputs,
|
|
3012
|
+
output: chainOutput,
|
|
3013
|
+
executionState: stateManager.currentState
|
|
3014
|
+
};
|
|
3015
|
+
} catch (e) {
|
|
3016
|
+
const error = e instanceof StepExecutionError ? e : new ExternalServiceError("unknown", "TOOL_EXECUTION_FAILED", e instanceof Error ? e.message : String(e), e, undefined, false);
|
|
3017
|
+
stateManager.runFailed(error);
|
|
3018
|
+
return {
|
|
3019
|
+
success: false,
|
|
3020
|
+
stepOutputs,
|
|
3021
|
+
error,
|
|
3022
|
+
executionState: stateManager.currentState
|
|
3023
|
+
};
|
|
3024
|
+
}
|
|
3025
|
+
}
|
|
3026
|
+
// src/generator/index.ts
|
|
3027
|
+
import { generateText as generateText2, stepCountIs as stepCountIs4, tool as tool3 } from "ai";
|
|
3028
|
+
import { type as arktype2 } from "arktype";
|
|
3029
|
+
|
|
3030
|
+
// src/types.ts
|
|
3031
|
+
import { type as type2 } from "arktype";
|
|
3032
|
+
var expressionSchema = type2({
|
|
3033
|
+
type: "'literal'",
|
|
3034
|
+
value: "unknown"
|
|
3035
|
+
}).or({
|
|
3036
|
+
type: "'jmespath'",
|
|
3037
|
+
expression: "string"
|
|
3038
|
+
}).or({
|
|
3039
|
+
type: "'template'",
|
|
3040
|
+
template: "string"
|
|
3041
|
+
}).describe("a value that must always be wrapped as an expression object — use { type: 'literal', value: ... } for any static value (strings, numbers, booleans, etc.), { 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), or { type: 'template', template: '...' } for string interpolation with embedded JMESPath expressions using ${...} syntax (e.g. 'Hello ${user.name}, order ${order.id}') — template expressions always resolve to a string");
|
|
3042
|
+
var toolCallParamsSchema = type2({
|
|
3043
|
+
type: "'tool-call'",
|
|
3044
|
+
params: {
|
|
3045
|
+
toolName: "string",
|
|
3046
|
+
toolInput: [
|
|
3047
|
+
{
|
|
3048
|
+
"[string]": expressionSchema
|
|
3049
|
+
},
|
|
3050
|
+
"@",
|
|
3051
|
+
"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"
|
|
3052
|
+
]
|
|
3053
|
+
}
|
|
3054
|
+
}).describe("a step that calls a tool with specified input parameters (which can be static values or expressions)");
|
|
3055
|
+
var switchCaseParamsSchema = type2({
|
|
3056
|
+
type: "'switch-case'",
|
|
3057
|
+
params: {
|
|
3058
|
+
switchOn: expressionSchema,
|
|
3059
|
+
cases: [
|
|
3060
|
+
{
|
|
3061
|
+
value: expressionSchema.or({ type: "'default'" }),
|
|
3062
|
+
branchBodyStepId: [
|
|
3063
|
+
"string",
|
|
3064
|
+
"@",
|
|
3065
|
+
"the id of the first step in the branch body chain to execute if this case matches"
|
|
3066
|
+
]
|
|
3067
|
+
},
|
|
3068
|
+
"[]"
|
|
3069
|
+
]
|
|
3070
|
+
}
|
|
3071
|
+
}).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");
|
|
3072
|
+
var forEachParamsSchema = type2({
|
|
3073
|
+
type: "'for-each'",
|
|
3074
|
+
params: {
|
|
3075
|
+
target: expressionSchema,
|
|
3076
|
+
itemName: [
|
|
3077
|
+
"string",
|
|
3078
|
+
"@",
|
|
3079
|
+
"the name to refer to the current item in the list within expressions in the loop body"
|
|
3080
|
+
],
|
|
3081
|
+
loopBodyStepId: [
|
|
3082
|
+
"string",
|
|
3083
|
+
"@",
|
|
3084
|
+
"the id of the first step in the loop body chain to execute for each item in the list"
|
|
3085
|
+
]
|
|
3086
|
+
}
|
|
3087
|
+
}).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");
|
|
3088
|
+
var llmPromptSchema = type2({
|
|
3089
|
+
type: "'llm-prompt'",
|
|
3090
|
+
params: {
|
|
3091
|
+
prompt: [
|
|
3092
|
+
"string",
|
|
3093
|
+
"@",
|
|
3094
|
+
"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})"
|
|
3095
|
+
],
|
|
3096
|
+
outputFormat: [
|
|
3097
|
+
"object",
|
|
3098
|
+
"@",
|
|
3099
|
+
"JSON schema specifying the output format expected from the LLM"
|
|
3100
|
+
]
|
|
3101
|
+
}
|
|
3102
|
+
}).describe("a step that prompts an LLM with a text prompt to produce an output in a specified format");
|
|
3103
|
+
var extractDataParamsSchema = type2({
|
|
3104
|
+
type: "'extract-data'",
|
|
3105
|
+
params: {
|
|
3106
|
+
sourceData: [expressionSchema, "@", "the data to extract information from"],
|
|
3107
|
+
outputFormat: [
|
|
3108
|
+
"object",
|
|
3109
|
+
"@",
|
|
3110
|
+
"JSON schema specifying the output format expected from the data extraction"
|
|
3111
|
+
]
|
|
3112
|
+
}
|
|
3113
|
+
}).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");
|
|
3114
|
+
var sleepParamsSchema = type2({
|
|
3115
|
+
type: "'sleep'",
|
|
3116
|
+
params: {
|
|
3117
|
+
durationMs: expressionSchema
|
|
3118
|
+
}
|
|
3119
|
+
}).describe("a step that pauses workflow execution for a specified duration in milliseconds; the durationMs parameter must evaluate to a non-negative number");
|
|
3120
|
+
var waitForConditionParamsSchema = type2({
|
|
3121
|
+
type: "'wait-for-condition'",
|
|
3122
|
+
params: {
|
|
3123
|
+
conditionStepId: [
|
|
3124
|
+
"string",
|
|
3125
|
+
"@",
|
|
3126
|
+
"the id of the first step in the condition-check chain that will be executed on each polling attempt; this chain runs until a step with no nextStepId, then the condition expression is evaluated"
|
|
3127
|
+
],
|
|
3128
|
+
condition: [
|
|
3129
|
+
expressionSchema,
|
|
3130
|
+
"@",
|
|
3131
|
+
"an expression evaluated after each execution of the condition-check chain; if it evaluates to a truthy value, the wait completes with that value as its output; all step outputs from the condition chain are available in scope for this expression"
|
|
3132
|
+
],
|
|
3133
|
+
"maxAttempts?": [
|
|
3134
|
+
expressionSchema,
|
|
3135
|
+
"@",
|
|
3136
|
+
"maximum number of polling attempts before giving up (default: 10)"
|
|
3137
|
+
],
|
|
3138
|
+
"intervalMs?": [
|
|
3139
|
+
expressionSchema,
|
|
3140
|
+
"@",
|
|
3141
|
+
"milliseconds to wait between polling attempts (default: 1000)"
|
|
3142
|
+
],
|
|
3143
|
+
"backoffMultiplier?": [
|
|
3144
|
+
expressionSchema,
|
|
3145
|
+
"@",
|
|
3146
|
+
"multiply the interval by this factor after each attempt (default: 1, i.e. no backoff; use 2 for exponential backoff)"
|
|
3147
|
+
],
|
|
3148
|
+
"timeoutMs?": [
|
|
3149
|
+
expressionSchema,
|
|
3150
|
+
"@",
|
|
3151
|
+
"hard timeout in milliseconds; if the total elapsed time exceeds this, the step fails regardless of remaining attempts"
|
|
3152
|
+
]
|
|
3153
|
+
}
|
|
3154
|
+
}).describe("a step that repeatedly executes a condition-check chain (starting at conditionStepId) and then evaluates the condition expression against the updated scope; if the condition expression evaluates to a truthy value, the step completes with that value as its output; otherwise it waits for intervalMs milliseconds (multiplied by backoffMultiplier after each attempt) and tries again, up to maxAttempts times or until timeoutMs milliseconds have elapsed; the condition-check chain runs until a step with no nextStepId, at which point the condition expression is evaluated; all step outputs from the condition chain are available in scope for the condition expression");
|
|
3155
|
+
var agentLoopParamsSchema = type2({
|
|
3156
|
+
type: "'agent-loop'",
|
|
3157
|
+
params: {
|
|
3158
|
+
instructions: [
|
|
3159
|
+
"string",
|
|
3160
|
+
"@",
|
|
3161
|
+
"a template string with task instructions for the agent; JMESPath expressions can be embedded using ${...} syntax (e.g. 'Research ${input.topic} and summarize findings'). All data from previous steps is available via their step ids, and loop variables are available within for-each loop bodies"
|
|
3162
|
+
],
|
|
3163
|
+
tools: [
|
|
3164
|
+
["string", "[]"],
|
|
3165
|
+
"@",
|
|
3166
|
+
"names of tools from the workflow's tool set that the agent is allowed to use"
|
|
3167
|
+
],
|
|
3168
|
+
outputFormat: [
|
|
3169
|
+
"object",
|
|
3170
|
+
"@",
|
|
3171
|
+
"JSON schema specifying the structured output format expected from the agent"
|
|
3172
|
+
],
|
|
3173
|
+
"maxSteps?": [
|
|
3174
|
+
expressionSchema,
|
|
3175
|
+
"@",
|
|
3176
|
+
"maximum number of tool-calling steps the agent may take (default: 10)"
|
|
3177
|
+
]
|
|
3178
|
+
}
|
|
3179
|
+
}).describe("a step that delegates work to an autonomous agent with its own tool-calling loop; USE SPARINGLY — this sacrifices the determinism that is the core value of the workflow DSL. Prefer explicit tool-call, llm-prompt, and control flow steps whenever the task can be decomposed into predictable operations");
|
|
3180
|
+
var startParamsSchema = type2({
|
|
3181
|
+
type: "'start'"
|
|
3182
|
+
}).describe("a step that marks the entry point of a workflow; a no-op marker whose execution continues to the next step");
|
|
3183
|
+
var endSchema = type2({
|
|
3184
|
+
type: "'end'",
|
|
3185
|
+
"params?": {
|
|
3186
|
+
output: expressionSchema
|
|
3187
|
+
}
|
|
3188
|
+
}).describe("a step that indicates the end of a branch; optionally specify an output expression whose evaluated value becomes the workflow's output");
|
|
3189
|
+
var workflowStepSchema = type2({
|
|
3190
|
+
id: /^[a-zA-Z_][a-zA-Z0-9_]+$/,
|
|
3191
|
+
name: "string",
|
|
3192
|
+
description: "string",
|
|
3193
|
+
"nextStepId?": "string"
|
|
3194
|
+
}).and(toolCallParamsSchema.or(llmPromptSchema).or(extractDataParamsSchema).or(switchCaseParamsSchema).or(forEachParamsSchema).or(sleepParamsSchema).or(waitForConditionParamsSchema).or(agentLoopParamsSchema).or(startParamsSchema).or(endSchema));
|
|
3195
|
+
var workflowDefinitionSchema = type2({
|
|
3196
|
+
initialStepId: "string",
|
|
3197
|
+
"inputSchema?": [
|
|
3198
|
+
"object",
|
|
3199
|
+
"@",
|
|
3200
|
+
"an optional JSON Schema object defining the inputs required to run the workflow; the executor validates provided inputs against this schema, and the validated inputs become available in JMESPath scope via the root identifier 'input' (e.g. input.fieldName)"
|
|
3201
|
+
],
|
|
3202
|
+
"outputSchema?": [
|
|
3203
|
+
"object",
|
|
3204
|
+
"@",
|
|
3205
|
+
"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"
|
|
3206
|
+
],
|
|
3207
|
+
steps: [
|
|
3208
|
+
[workflowStepSchema, "[]"],
|
|
3209
|
+
"@",
|
|
3210
|
+
"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"
|
|
3211
|
+
]
|
|
3212
|
+
});
|
|
3213
|
+
|
|
3214
|
+
// src/generator/prompt.ts
|
|
3215
|
+
import { asSchema as asSchema2 } from "ai";
|
|
3216
|
+
async function serializeToolsForPrompt(tools) {
|
|
3217
|
+
return JSON.stringify(await Promise.all(Object.entries(tools).map(async ([toolName, toolDef]) => ({
|
|
3218
|
+
name: toolName,
|
|
3219
|
+
description: toolDef.description,
|
|
3220
|
+
inputSchema: await asSchema2(toolDef.inputSchema).jsonSchema,
|
|
3221
|
+
outputSchema: toolDef.outputSchema ? await asSchema2(toolDef.outputSchema).jsonSchema : undefined
|
|
3222
|
+
}))));
|
|
3223
|
+
}
|
|
3224
|
+
function buildWorkflowGenerationPrompt(serializedTools, additionalInstructions) {
|
|
3225
|
+
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.
|
|
3226
|
+
|
|
3227
|
+
## Workflow Structure
|
|
3228
|
+
|
|
3229
|
+
A workflow has:
|
|
3230
|
+
- \`initialStepId\`: the id of the first step to execute
|
|
3231
|
+
- \`inputSchema\` (optional): a JSON Schema object defining the inputs required to run the workflow. When present, provided inputs are validated against this schema and become available in JMESPath expressions via the root identifier \`input\` (e.g. \`input.fieldName\`).
|
|
3232
|
+
- \`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.
|
|
3233
|
+
- \`steps\`: an array of step objects (order does not matter — execution flow is determined by nextStepId links)
|
|
3234
|
+
|
|
3235
|
+
## Step Common Fields
|
|
3236
|
+
|
|
3237
|
+
Every step has:
|
|
3238
|
+
- \`id\`: unique identifier matching /^[a-zA-Z_][a-zA-Z0-9_]+$/ (letters, digits, underscores; at least 2 characters)
|
|
3239
|
+
- \`name\`: human-readable name
|
|
3240
|
+
- \`description\`: what this step does
|
|
3241
|
+
- \`type\`: one of the step types below
|
|
3242
|
+
- \`nextStepId\` (optional): id of the next step to execute after this one
|
|
3243
|
+
|
|
3244
|
+
## Step Types
|
|
3245
|
+
|
|
3246
|
+
### start
|
|
3247
|
+
A no-op marker that indicates the entry point of a workflow. It takes no parameters. Workflow inputs are declared via \`inputSchema\` on the workflow definition itself, not on the start step.
|
|
3248
|
+
\`\`\`json
|
|
3249
|
+
{
|
|
3250
|
+
"type": "start"
|
|
3251
|
+
}
|
|
3252
|
+
\`\`\`
|
|
3253
|
+
|
|
3254
|
+
### tool-call
|
|
3255
|
+
Calls a tool with input parameters. All values in toolInput MUST be expression objects.
|
|
3256
|
+
\`\`\`json
|
|
3257
|
+
{
|
|
3258
|
+
"type": "tool-call",
|
|
3259
|
+
"params": {
|
|
3260
|
+
"toolName": "name-of-tool",
|
|
3261
|
+
"toolInput": {
|
|
3262
|
+
"paramName": { "type": "literal", "value": "static value" },
|
|
3263
|
+
"otherParam": { "type": "jmespath", "expression": "previous_step.someField" }
|
|
3264
|
+
}
|
|
3265
|
+
}
|
|
3266
|
+
}
|
|
3267
|
+
\`\`\`
|
|
3268
|
+
|
|
3269
|
+
### llm-prompt
|
|
3270
|
+
Prompts an LLM with a template string to produce structured output. Use \${...} syntax to embed JMESPath expressions in the prompt.
|
|
3271
|
+
\`\`\`json
|
|
3272
|
+
{
|
|
3273
|
+
"type": "llm-prompt",
|
|
3274
|
+
"params": {
|
|
3275
|
+
"prompt": "Classify this ticket: \${get_tickets.tickets[0].subject}",
|
|
3276
|
+
"outputFormat": { "type": "object", "properties": { "category": { "type": "string" } }, "required": ["category"] }
|
|
3277
|
+
}
|
|
3278
|
+
}
|
|
3279
|
+
\`\`\`
|
|
3280
|
+
|
|
3281
|
+
### extract-data
|
|
3282
|
+
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.
|
|
3283
|
+
\`\`\`json
|
|
3284
|
+
{
|
|
3285
|
+
"type": "extract-data",
|
|
3286
|
+
"params": {
|
|
3287
|
+
"sourceData": { "type": "jmespath", "expression": "previous_step.rawContent" },
|
|
3288
|
+
"outputFormat": { "type": "object", "properties": { ... }, "required": [...] }
|
|
3289
|
+
}
|
|
3290
|
+
}
|
|
3291
|
+
\`\`\`
|
|
3292
|
+
|
|
3293
|
+
### switch-case
|
|
3294
|
+
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.
|
|
3295
|
+
\`\`\`json
|
|
3296
|
+
{
|
|
3297
|
+
"type": "switch-case",
|
|
3298
|
+
"params": {
|
|
3299
|
+
"switchOn": { "type": "jmespath", "expression": "classify.category" },
|
|
3300
|
+
"cases": [
|
|
3301
|
+
{
|
|
3302
|
+
"value": { "type": "literal", "value": "critical" },
|
|
3303
|
+
"branchBodyStepId": "handle_critical"
|
|
3304
|
+
},
|
|
3305
|
+
{
|
|
3306
|
+
"value": { "type": "default" },
|
|
3307
|
+
"branchBodyStepId": "handle_other"
|
|
3308
|
+
}
|
|
3309
|
+
]
|
|
3310
|
+
}
|
|
3311
|
+
}
|
|
3312
|
+
\`\`\`
|
|
3313
|
+
|
|
3314
|
+
### for-each
|
|
3315
|
+
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.
|
|
3316
|
+
\`\`\`json
|
|
3317
|
+
{
|
|
3318
|
+
"type": "for-each",
|
|
3319
|
+
"params": {
|
|
3320
|
+
"target": { "type": "jmespath", "expression": "get_items.items" },
|
|
3321
|
+
"itemName": "item",
|
|
3322
|
+
"loopBodyStepId": "process_item"
|
|
3323
|
+
}
|
|
3324
|
+
}
|
|
3325
|
+
\`\`\`
|
|
3326
|
+
|
|
3327
|
+
### sleep
|
|
3328
|
+
Pauses workflow execution for a fixed duration. Use when you need to wait between operations (e.g., rate limiting, propagation delays).
|
|
3329
|
+
\`\`\`json
|
|
3330
|
+
{
|
|
3331
|
+
"type": "sleep",
|
|
3332
|
+
"params": {
|
|
3333
|
+
"durationMs": { "type": "literal", "value": 5000 }
|
|
3334
|
+
}
|
|
3335
|
+
}
|
|
3336
|
+
\`\`\`
|
|
3337
|
+
|
|
3338
|
+
### wait-for-condition
|
|
3339
|
+
Repeatedly executes a condition-check chain and evaluates a condition expression until it returns a truthy value. Use when you need to poll for a state change (e.g., waiting for a job to complete, a resource to become available).
|
|
3340
|
+
|
|
3341
|
+
The \`conditionStepId\` points to the first step of a sub-chain that will be executed on each polling attempt. After the chain runs, the \`condition\` expression is evaluated against the current scope (including all outputs from the condition chain). If the result is truthy, the wait-for-condition step completes with that value as its output. Otherwise, it waits and retries.
|
|
3342
|
+
|
|
3343
|
+
Optional parameters control polling behavior:
|
|
3344
|
+
- \`maxAttempts\`: maximum number of polling attempts (default: 10)
|
|
3345
|
+
- \`intervalMs\`: milliseconds between attempts (default: 1000)
|
|
3346
|
+
- \`backoffMultiplier\`: multiply interval by this after each attempt (default: 1, i.e. no backoff; use 2 for exponential)
|
|
3347
|
+
- \`timeoutMs\`: hard timeout in milliseconds (optional)
|
|
3348
|
+
\`\`\`json
|
|
3349
|
+
{
|
|
3350
|
+
"type": "wait-for-condition",
|
|
3351
|
+
"params": {
|
|
3352
|
+
"conditionStepId": "poll_status",
|
|
3353
|
+
"condition": { "type": "jmespath", "expression": "poll_status.status == 'complete'" },
|
|
3354
|
+
"maxAttempts": { "type": "literal", "value": 30 },
|
|
3355
|
+
"intervalMs": { "type": "literal", "value": 2000 },
|
|
3356
|
+
"backoffMultiplier": { "type": "literal", "value": 1.5 },
|
|
3357
|
+
"timeoutMs": { "type": "literal", "value": 120000 }
|
|
3358
|
+
}
|
|
3359
|
+
}
|
|
3360
|
+
\`\`\`
|
|
3361
|
+
|
|
3362
|
+
### agent-loop
|
|
3363
|
+
**USE SPARINGLY.** Delegates work to an autonomous agent with its own tool-calling loop. This step sacrifices the determinism that is the core value of the workflow DSL. Only use when the task genuinely cannot be decomposed into predictable tool-call, llm-prompt, and control flow steps (e.g., open-ended research tasks, complex multi-step reasoning that depends on intermediate results in unpredictable ways).
|
|
3364
|
+
|
|
3365
|
+
The agent receives the interpolated \`instructions\` as a prompt, has access to the listed \`tools\`, and must produce output matching \`outputFormat\`. The \`maxSteps\` parameter bounds the number of tool-calling iterations (default: 10).
|
|
3366
|
+
\`\`\`json
|
|
3367
|
+
{
|
|
3368
|
+
"type": "agent-loop",
|
|
3369
|
+
"params": {
|
|
3370
|
+
"instructions": "Research \${input.topic} using the available search tools and compile a summary with at least 3 sources.",
|
|
3371
|
+
"tools": ["web_search", "fetch_page"],
|
|
3372
|
+
"outputFormat": {
|
|
3373
|
+
"type": "object",
|
|
3374
|
+
"properties": {
|
|
3375
|
+
"summary": { "type": "string" },
|
|
3376
|
+
"sources": { "type": "array", "items": { "type": "string" } }
|
|
3377
|
+
},
|
|
3378
|
+
"required": ["summary", "sources"]
|
|
3379
|
+
},
|
|
3380
|
+
"maxSteps": { "type": "literal", "value": 15 }
|
|
3381
|
+
}
|
|
3382
|
+
}
|
|
3383
|
+
\`\`\`
|
|
3384
|
+
|
|
3385
|
+
### end
|
|
3386
|
+
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.
|
|
3387
|
+
\`\`\`json
|
|
3388
|
+
{
|
|
3389
|
+
"type": "end"
|
|
3390
|
+
}
|
|
3391
|
+
\`\`\`
|
|
3392
|
+
|
|
3393
|
+
With output (when the workflow declares an outputSchema):
|
|
3394
|
+
\`\`\`json
|
|
3395
|
+
{
|
|
3396
|
+
"type": "end",
|
|
3397
|
+
"params": {
|
|
3398
|
+
"output": { "type": "jmespath", "expression": "summarize.result" }
|
|
3399
|
+
}
|
|
3400
|
+
}
|
|
3401
|
+
\`\`\`
|
|
3402
|
+
|
|
3403
|
+
## Expression System
|
|
3404
|
+
|
|
3405
|
+
Every dynamic value must be an expression object:
|
|
3406
|
+
|
|
3407
|
+
1. **Literal** — for static values known at design time:
|
|
3408
|
+
\`{ "type": "literal", "value": <any value> }\`
|
|
3409
|
+
|
|
3410
|
+
2. **JMESPath** — for referencing data from previous steps, workflow inputs, or loop variables:
|
|
3411
|
+
\`{ "type": "jmespath", "expression": "<expression>" }\`
|
|
3412
|
+
The root of a JMESPath expression must be one of: the \`input\` alias (e.g. \`input.orderId\`) for workflow inputs, a step id (e.g. \`get_orders.orders\`) for previous step outputs, or a loop variable name (e.g. \`item.id\` within a for-each body).
|
|
3413
|
+
|
|
3414
|
+
3. **Template strings** (llm-prompt only) — embed JMESPath in the prompt string using \${...}:
|
|
3415
|
+
\`"Summarize: \${fetch_data.content}"\`
|
|
3416
|
+
These are NOT expression objects — they appear directly in the prompt string.
|
|
3417
|
+
|
|
3418
|
+
## Structural Rules
|
|
3419
|
+
|
|
3420
|
+
1. Step IDs must be unique and match /^[a-zA-Z_][a-zA-Z0-9_]+$/.
|
|
3421
|
+
2. Steps link via nextStepId. Omitting nextStepId ends the chain.
|
|
3422
|
+
3. Branch body chains (switch-case), loop body chains (for-each), and condition body chains (wait-for-condition) must terminate — their last step must NOT have a nextStepId. Do NOT point them back to the parent or outside the body.
|
|
3423
|
+
4. Only reference step IDs of steps that will have executed before the current step (no forward references).
|
|
3424
|
+
5. Do not create cycles (for-each handles iteration — you do not need to loop manually).
|
|
3425
|
+
6. end steps must NOT have a nextStepId.
|
|
3426
|
+
|
|
3427
|
+
## Available Tools
|
|
3428
|
+
|
|
3429
|
+
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.
|
|
3430
|
+
|
|
3431
|
+
${serializedTools}
|
|
3432
|
+
|
|
3433
|
+
## Common Mistakes
|
|
3434
|
+
|
|
3435
|
+
1. NEVER use bare primitives in toolInput. ALL values must be expression objects.
|
|
3436
|
+
WRONG: \`{ "email": "user@example.com" }\`
|
|
3437
|
+
RIGHT: \`{ "email": { "type": "literal", "value": "user@example.com" } }\`
|
|
3438
|
+
|
|
3439
|
+
2. Do NOT give end steps a nextStepId.
|
|
3440
|
+
|
|
3441
|
+
3. Branch/loop body chains must terminate (last step has no nextStepId). Do NOT point them outside their scope.
|
|
3442
|
+
|
|
3443
|
+
4. JMESPath expressions reference step outputs by step ID as the root identifier (e.g. \`"get_orders.orders[0].id"\`), and workflow inputs via the \`input\` alias (e.g. \`"input.orderId"\`).
|
|
3444
|
+
|
|
3445
|
+
5. For-each itemName is a scoped variable accessible ONLY within the loop body steps.
|
|
3446
|
+
|
|
3447
|
+
6. Step IDs must be at least 2 characters long.
|
|
3448
|
+
|
|
3449
|
+
7. for-each target must resolve to an ARRAY. Check the tool's outputSchema to determine the correct path.
|
|
3450
|
+
WRONG: \`"target": { "type": "jmespath", "expression": "get_orders" }\` (when get_orders returns an object with an \`orders\` array property)
|
|
3451
|
+
RIGHT: \`"target": { "type": "jmespath", "expression": "get_orders.orders" }\`
|
|
3452
|
+
|
|
3453
|
+
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.
|
|
3454
|
+
|
|
3455
|
+
9. wait-for-condition requires both \`conditionStepId\` (the chain to execute on each polling attempt) AND \`condition\` (the expression to evaluate after the chain runs). The condition expression is evaluated AFTER the chain runs, using the updated scope with all step outputs from the condition chain.
|
|
3456
|
+
|
|
3457
|
+
10. Prefer explicit tool-call, llm-prompt, switch-case, and for-each steps over agent-loop. Only use agent-loop when the task is genuinely open-ended and cannot be decomposed into deterministic steps.${additionalInstructions ? `
|
|
3458
|
+
|
|
3459
|
+
## Additional Instructions
|
|
3460
|
+
|
|
3461
|
+
${additionalInstructions}` : ""}`;
|
|
3462
|
+
}
|
|
3463
|
+
function formatDiagnostics(diagnostics) {
|
|
3464
|
+
const errors = diagnostics.filter((d) => d.severity === "error");
|
|
3465
|
+
if (errors.length === 0)
|
|
3466
|
+
return "No errors.";
|
|
3467
|
+
return errors.map((d) => {
|
|
3468
|
+
const loc = d.location ? ` (at step ${d.location.stepId}${d.location.field ? `, field ${d.location.field}` : ""})` : "";
|
|
3469
|
+
return `- [${d.code}] ${d.message}${loc}`;
|
|
3470
|
+
}).join(`
|
|
3471
|
+
`);
|
|
3472
|
+
}
|
|
3473
|
+
|
|
3474
|
+
// src/generator/index.ts
|
|
3475
|
+
async function generateWorkflow(options) {
|
|
3476
|
+
const {
|
|
3477
|
+
model,
|
|
3478
|
+
tools,
|
|
3479
|
+
task,
|
|
3480
|
+
maxRetries = 3,
|
|
3481
|
+
additionalInstructions
|
|
3482
|
+
} = options;
|
|
3483
|
+
const missingOutputSchema = Object.entries(tools).filter(([_, t]) => !t.outputSchema).map(([name]) => name);
|
|
3484
|
+
if (missingOutputSchema.length > 0) {
|
|
3485
|
+
throw new Error(`All tools must have an outputSchema. Missing: ${missingOutputSchema.join(", ")}`);
|
|
3486
|
+
}
|
|
3487
|
+
const serializedTools = await serializeToolsForPrompt(tools);
|
|
3488
|
+
const systemPrompt = buildWorkflowGenerationPrompt(serializedTools, additionalInstructions);
|
|
3489
|
+
let successWorkflow = null;
|
|
3490
|
+
let lastDiagnostics = [];
|
|
3491
|
+
let attempts = 0;
|
|
3492
|
+
const createWorkflowTool = tool3({
|
|
3493
|
+
description: "Create a workflow definition",
|
|
3494
|
+
inputSchema: workflowDefinitionSchema,
|
|
3495
|
+
execute: async (workflowDef) => {
|
|
3496
|
+
attempts++;
|
|
3497
|
+
const result = await compileWorkflow(workflowDef, { tools });
|
|
3498
|
+
lastDiagnostics = result.diagnostics;
|
|
3499
|
+
const errors = result.diagnostics.filter((d) => d.severity === "error");
|
|
3500
|
+
if (errors.length > 0) {
|
|
3501
|
+
return {
|
|
3502
|
+
success: false,
|
|
3503
|
+
errors: formatDiagnostics(result.diagnostics)
|
|
3504
|
+
};
|
|
3505
|
+
}
|
|
3506
|
+
successWorkflow = result.workflow ?? workflowDef;
|
|
3507
|
+
return { success: true };
|
|
3508
|
+
}
|
|
3509
|
+
});
|
|
3510
|
+
await generateText2({
|
|
3511
|
+
model,
|
|
3512
|
+
system: systemPrompt,
|
|
3513
|
+
prompt: `Create a workflow to accomplish the following task:
|
|
3514
|
+
|
|
3515
|
+
${task}`,
|
|
3516
|
+
tools: { createWorkflow: createWorkflowTool },
|
|
3517
|
+
toolChoice: { type: "tool", toolName: "createWorkflow" },
|
|
3518
|
+
stopWhen: [stepCountIs4(maxRetries + 1), () => successWorkflow !== null]
|
|
3519
|
+
});
|
|
3520
|
+
return {
|
|
3521
|
+
workflow: successWorkflow,
|
|
3522
|
+
diagnostics: lastDiagnostics,
|
|
3523
|
+
attempts
|
|
3524
|
+
};
|
|
3525
|
+
}
|
|
3526
|
+
function createWorkflowGeneratorTool(options) {
|
|
3527
|
+
const {
|
|
3528
|
+
model,
|
|
3529
|
+
tools: baseTools,
|
|
3530
|
+
maxRetries,
|
|
3531
|
+
additionalInstructions
|
|
3532
|
+
} = options;
|
|
3533
|
+
return tool3({
|
|
3534
|
+
description: "Generate a validated workflow definition from a natural language task description",
|
|
3535
|
+
inputSchema: arktype2({ task: "string" }),
|
|
3536
|
+
execute: async ({ task }) => {
|
|
3537
|
+
return generateWorkflow({
|
|
3538
|
+
model,
|
|
3539
|
+
tools: baseTools ?? {},
|
|
3540
|
+
task,
|
|
3541
|
+
maxRetries,
|
|
3542
|
+
additionalInstructions
|
|
3543
|
+
});
|
|
3544
|
+
}
|
|
3545
|
+
});
|
|
3546
|
+
}
|
|
3547
|
+
export {
|
|
3548
|
+
workflowDefinitionSchema,
|
|
3549
|
+
snapshotError,
|
|
3550
|
+
serializeToolsForPrompt,
|
|
3551
|
+
generateWorkflow,
|
|
3552
|
+
executeWorkflow,
|
|
3553
|
+
createWorkflowGeneratorTool,
|
|
3554
|
+
createDefaultDurableContext,
|
|
3555
|
+
compileWorkflow,
|
|
3556
|
+
buildWorkflowGenerationPrompt,
|
|
3557
|
+
applyDelta,
|
|
3558
|
+
ValidationError,
|
|
3559
|
+
StepExecutionError,
|
|
3560
|
+
OutputQualityError,
|
|
3561
|
+
ExternalServiceError,
|
|
3562
|
+
ExpressionError,
|
|
3563
|
+
ConfigurationError
|
|
3564
|
+
};
|
|
3565
|
+
|
|
3566
|
+
//# debugId=0F518A409A3FE44264756E2164756E21
|