@aid-framework/generator-ts-vercel 0.0.0 → 0.1.48
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 +201 -0
- package/README.md +123 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/emit/agents.d.ts +62 -0
- package/dist/emit/agents.d.ts.map +1 -0
- package/dist/emit/agents.js +256 -0
- package/dist/emit/agents.js.map +1 -0
- package/dist/emit/business.d.ts +23 -0
- package/dist/emit/business.d.ts.map +1 -0
- package/dist/emit/business.js +414 -0
- package/dist/emit/business.js.map +1 -0
- package/dist/emit/control.d.ts +40 -0
- package/dist/emit/control.d.ts.map +1 -0
- package/dist/emit/control.js +108 -0
- package/dist/emit/control.js.map +1 -0
- package/dist/emit/file.d.ts +64 -0
- package/dist/emit/file.d.ts.map +1 -0
- package/dist/emit/file.js +80 -0
- package/dist/emit/file.js.map +1 -0
- package/dist/emit/guardrails.d.ts +45 -0
- package/dist/emit/guardrails.d.ts.map +1 -0
- package/dist/emit/guardrails.js +192 -0
- package/dist/emit/guardrails.js.map +1 -0
- package/dist/emit/infrastructure.d.ts +15 -0
- package/dist/emit/infrastructure.d.ts.map +1 -0
- package/dist/emit/infrastructure.js +238 -0
- package/dist/emit/infrastructure.js.map +1 -0
- package/dist/emit/pipelines.d.ts +46 -0
- package/dist/emit/pipelines.d.ts.map +1 -0
- package/dist/emit/pipelines.js +1015 -0
- package/dist/emit/pipelines.js.map +1 -0
- package/dist/emit/prompts.d.ts +14 -0
- package/dist/emit/prompts.d.ts.map +1 -0
- package/dist/emit/prompts.js +61 -0
- package/dist/emit/prompts.js.map +1 -0
- package/dist/emit/retrieval.d.ts +19 -0
- package/dist/emit/retrieval.d.ts.map +1 -0
- package/dist/emit/retrieval.js +206 -0
- package/dist/emit/retrieval.js.map +1 -0
- package/dist/emit/schemas.d.ts +22 -0
- package/dist/emit/schemas.d.ts.map +1 -0
- package/dist/emit/schemas.js +145 -0
- package/dist/emit/schemas.js.map +1 -0
- package/dist/emit/text.d.ts +86 -0
- package/dist/emit/text.d.ts.map +1 -0
- package/dist/emit/text.js +242 -0
- package/dist/emit/text.js.map +1 -0
- package/dist/emit/tools.d.ts +15 -0
- package/dist/emit/tools.d.ts.map +1 -0
- package/dist/emit/tools.js +225 -0
- package/dist/emit/tools.js.map +1 -0
- package/dist/errors.d.ts +12 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +16 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +47 -0
- package/dist/index.js.map +1 -0
- package/dist/manifest.d.ts +48 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +88 -0
- package/dist/manifest.js.map +1 -0
- package/dist/names.d.ts +57 -0
- package/dist/names.d.ts.map +1 -0
- package/dist/names.js +294 -0
- package/dist/names.js.map +1 -0
- package/dist/placeholders.d.ts +48 -0
- package/dist/placeholders.d.ts.map +1 -0
- package/dist/placeholders.js +114 -0
- package/dist/placeholders.js.map +1 -0
- package/dist/plan.d.ts +18 -0
- package/dist/plan.d.ts.map +1 -0
- package/dist/plan.js +81 -0
- package/dist/plan.js.map +1 -0
- package/dist/validate.d.ts +88 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/validate.js +790 -0
- package/dist/validate.js.map +1 -0
- package/package.json +43 -14
|
@@ -0,0 +1,1015 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `generated/pipelines/*` and `generated/routes/*`.
|
|
3
|
+
*
|
|
4
|
+
* A pipeline is plain sequential async code, because that is what Phase 1 emits: no
|
|
5
|
+
* graph engine, no scheduler. Each step is a named `Step` that returns a new
|
|
6
|
+
* `PipelineContext`, so the step names survive into tracing and the values each step
|
|
7
|
+
* binds are readable in a diff.
|
|
8
|
+
*
|
|
9
|
+
* A route is deliberately *not* a Next.js module. It is a small
|
|
10
|
+
* `(request) => { status, body }` function, and the example's hand-written
|
|
11
|
+
* `app/api/<path>/route.ts` is the adapter that turns it into what Next serves. That
|
|
12
|
+
* split is forced by the ownership model: a path under `app/api` has no owner, so
|
|
13
|
+
* emitting it would break `validatePlan` and the next `aid gen`. See the README.
|
|
14
|
+
*/
|
|
15
|
+
import { TsVercelPlanError } from '../errors.js';
|
|
16
|
+
import { className, literal, memberAccess, propertyKey, toSnakeCase } from '../names.js';
|
|
17
|
+
import { problem, stepUnsupported } from '../validate.js';
|
|
18
|
+
import { generated, paths, pipelinePath, routePath } from './file.js';
|
|
19
|
+
import { stageTable, usesStage } from './guardrails.js';
|
|
20
|
+
import { requestTypeName } from './schemas.js';
|
|
21
|
+
import { exportStatement, fits, importStatement, moduleText, objectLiteral, sortedNames, } from './text.js';
|
|
22
|
+
/**
|
|
23
|
+
* How one binding is read out of the pipeline context.
|
|
24
|
+
*
|
|
25
|
+
* `event` is the one binding that is a real local in every emitted step; every other name
|
|
26
|
+
* lives in the pipeline context, so it is read back through `require` rather than emitted
|
|
27
|
+
* as an identifier that would not resolve. That is also what makes a composite body able
|
|
28
|
+
* to read the `item` its iteration seeded.
|
|
29
|
+
*
|
|
30
|
+
* `require` is typed `unknown`, so a *dotted* read has to cast to the binding's declared
|
|
31
|
+
* type to compile at all. Casting to the declared type rather than to `any` is what makes
|
|
32
|
+
* a field the spec does not declare a compile error in the emitted application instead of
|
|
33
|
+
* `undefined` at run time.
|
|
34
|
+
*/
|
|
35
|
+
function readBinding(head, tail, types) {
|
|
36
|
+
if (head === 'event')
|
|
37
|
+
return `event${tail}`;
|
|
38
|
+
const base = `context.require(${literal(head)})`;
|
|
39
|
+
if (tail.length === 0)
|
|
40
|
+
return base;
|
|
41
|
+
const type = types?.get(head);
|
|
42
|
+
return type === undefined ? `${base}${tail}` : `(${base} as ${type})${tail}`;
|
|
43
|
+
}
|
|
44
|
+
/** Render one `StepInputValue` as a TypeScript expression. */
|
|
45
|
+
function valueExpression(value, types) {
|
|
46
|
+
if (value.kind === 'literal')
|
|
47
|
+
return literal(value.value);
|
|
48
|
+
if (value.kind === 'reference') {
|
|
49
|
+
const [head, ...rest] = value.path;
|
|
50
|
+
if (head === undefined)
|
|
51
|
+
return 'undefined';
|
|
52
|
+
return readBinding(head, rest.map((part) => memberAccess(part)).join(''), types);
|
|
53
|
+
}
|
|
54
|
+
return templateToTs(value.template);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Turn an IR interpolation template into a TypeScript template literal.
|
|
58
|
+
*
|
|
59
|
+
* `{{ event.subject }}` becomes `${event.subject}`. The two forms use different
|
|
60
|
+
* delimiters on purpose: the IR's spelling is language-neutral, and rewriting it at
|
|
61
|
+
* emit time is the whole job of the target.
|
|
62
|
+
*
|
|
63
|
+
* `resolve` decides what a reference becomes. A prompt's template names its *variables*,
|
|
64
|
+
* which `generateStepModule` binds as object-literal keys, so the default is a bare
|
|
65
|
+
* identifier. A guard step's, a rerank step's and an agent step's template name pipeline
|
|
66
|
+
* *bindings* instead, and those are read back through `context.require` -- see
|
|
67
|
+
* `contextTemplate`.
|
|
68
|
+
*/
|
|
69
|
+
export function templateToTs(template, resolve = (path) => path.map(propertyKey).join('.')) {
|
|
70
|
+
const parts = [];
|
|
71
|
+
let index = 0;
|
|
72
|
+
while (index < template.length) {
|
|
73
|
+
const open = template.indexOf('{{', index);
|
|
74
|
+
if (open === -1) {
|
|
75
|
+
parts.push(escapeLiteral(template.slice(index)));
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
parts.push(escapeLiteral(template.slice(index, open)));
|
|
79
|
+
const close = template.indexOf('}}', open + 2);
|
|
80
|
+
if (close === -1) {
|
|
81
|
+
parts.push(escapeLiteral(template.slice(open)));
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
const path = template
|
|
85
|
+
.slice(open + 2, close)
|
|
86
|
+
.trim()
|
|
87
|
+
.split('.')
|
|
88
|
+
.map((part) => part.trim());
|
|
89
|
+
parts.push(`\${${resolve(path)}}`);
|
|
90
|
+
index = close + 2;
|
|
91
|
+
}
|
|
92
|
+
return `\`${parts.join('')}\``;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* The same rendering for a template that names pipeline bindings.
|
|
96
|
+
*
|
|
97
|
+
* `{{ handbook }}` becomes `${context.require('handbook')}`, matching what
|
|
98
|
+
* `valueExpression` does for a `reference` StepInputValue. `event` stays a bare local
|
|
99
|
+
* because it is the one binding every emitted step receives as an argument.
|
|
100
|
+
*/
|
|
101
|
+
export function contextTemplate(template, types) {
|
|
102
|
+
return templateToTs(template, (path) => {
|
|
103
|
+
const [head, ...rest] = path;
|
|
104
|
+
if (head === undefined)
|
|
105
|
+
return 'undefined';
|
|
106
|
+
return readBinding(head, rest.map((part) => memberAccess(part)).join(''), types);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
/** The first binding a template names, which is how a `retrieval` guard finds its input. */
|
|
110
|
+
export function firstReference(template) {
|
|
111
|
+
const open = template.indexOf('{{');
|
|
112
|
+
if (open === -1)
|
|
113
|
+
return undefined;
|
|
114
|
+
const close = template.indexOf('}}', open + 2);
|
|
115
|
+
if (close === -1)
|
|
116
|
+
return undefined;
|
|
117
|
+
const [head] = template
|
|
118
|
+
.slice(open + 2, close)
|
|
119
|
+
.trim()
|
|
120
|
+
.split('.');
|
|
121
|
+
return head?.trim();
|
|
122
|
+
}
|
|
123
|
+
function escapeLiteral(text) {
|
|
124
|
+
return text.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$\{/g, '\\${');
|
|
125
|
+
}
|
|
126
|
+
function isLeafStep(step) {
|
|
127
|
+
return (step.kind === 'generate' ||
|
|
128
|
+
step.kind === 'retrieve' ||
|
|
129
|
+
step.kind === 'tool' ||
|
|
130
|
+
step.kind === 'rerank' ||
|
|
131
|
+
step.kind === 'guard' ||
|
|
132
|
+
step.kind === 'agent');
|
|
133
|
+
}
|
|
134
|
+
function isBindingLeaf(step) {
|
|
135
|
+
return isLeafStep(step) && step.kind !== 'guard';
|
|
136
|
+
}
|
|
137
|
+
function isBindingStep(step) {
|
|
138
|
+
return isBindingLeaf(step) || step.kind === 'map' || step.kind === 'reduce';
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* The module-level constant one step is emitted as.
|
|
142
|
+
*
|
|
143
|
+
* A top-level binding step keeps the name it had before composites existed
|
|
144
|
+
* (`<pipeline><Binding>Step`), so the emitted tree does not churn; everything else -- a
|
|
145
|
+
* nested step, and a step that binds nothing -- carries its path instead.
|
|
146
|
+
*/
|
|
147
|
+
function stepConstant(pipeline, step, path) {
|
|
148
|
+
const suffix = isBindingStep(step) ? toSnakeCase(step.as) : step.kind;
|
|
149
|
+
const topLevel = path.length === 1;
|
|
150
|
+
// A nested name carries its whole path, prefixed with `step` so the index is not a
|
|
151
|
+
// leading digit (`className` would otherwise paper over that with a `T`).
|
|
152
|
+
const segments = isBindingStep(step) && topLevel ? [suffix] : ['step', ...path, suffix];
|
|
153
|
+
return `${camel(pipeline.id)}${className(segments.join('_'))}Step`;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* The `structuredOutput(...)` constant a `generate` step parses its reply with.
|
|
157
|
+
*
|
|
158
|
+
* Named after the binding, because that is how a spec reads it -- except for a nested
|
|
159
|
+
* step, where two arms may legally bind the same name. Those are named after the step's
|
|
160
|
+
* own constant instead, so the declaration is unique while nothing already emitted has
|
|
161
|
+
* to be renamed.
|
|
162
|
+
*/
|
|
163
|
+
function outputConstant(pipeline, step, path) {
|
|
164
|
+
const base = path.length === 1 ? step.as : stepConstant(pipeline, step, path);
|
|
165
|
+
return `${toSnakeCase(base).toUpperCase()}_OUTPUT`;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Every step in a pipeline, including the ones nested in a composite and the `generate`
|
|
169
|
+
* a `reduce` synthesises for its body.
|
|
170
|
+
*
|
|
171
|
+
* Import sets, output constants and "does this pipeline use X" are all questions about
|
|
172
|
+
* code, not about the top level, so they have to be answered over this walk. A nested
|
|
173
|
+
* `generate` that was not walked would emit a reference to an `_OUTPUT` constant the
|
|
174
|
+
* module never declares.
|
|
175
|
+
*/
|
|
176
|
+
function* walkSteps(ir, steps, parentPath = []) {
|
|
177
|
+
for (const [index, step] of steps.entries()) {
|
|
178
|
+
const path = [...parentPath, String(index)];
|
|
179
|
+
yield { step, path };
|
|
180
|
+
if (step.kind === 'branch') {
|
|
181
|
+
yield* walkSteps(ir, step.then, [...path, 'then']);
|
|
182
|
+
yield* walkSteps(ir, step.else, [...path, 'else']);
|
|
183
|
+
}
|
|
184
|
+
else if (step.kind === 'parallel') {
|
|
185
|
+
for (const [armIndex, arm] of step.branches.entries()) {
|
|
186
|
+
yield* walkSteps(ir, arm, [...path, 'arm', String(armIndex)]);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
else if (step.kind === 'map') {
|
|
190
|
+
yield* walkSteps(ir, [step.step], [...path, 'step']);
|
|
191
|
+
}
|
|
192
|
+
else if (step.kind === 'reduce') {
|
|
193
|
+
yield* walkSteps(ir, [reduceBody(ir, step)], [...path, 'item']);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
/** The binding the runtime's `reduceStep` collects; the body may not choose its own. */
|
|
198
|
+
const REDUCED_BINDING = 'reduced';
|
|
199
|
+
/** The context value `mapStep` and `reduceStep` seed per iteration. */
|
|
200
|
+
const ITERATION_ITEM = 'item';
|
|
201
|
+
/**
|
|
202
|
+
* The `generate` step a `reduce` runs once per item.
|
|
203
|
+
*
|
|
204
|
+
* A `reduce` has no step of its own, so it gets one: a `generate` whose only input is
|
|
205
|
+
* the iteration item, emitted by the same function the spec's own `generate` steps go
|
|
206
|
+
* through. That is what keeps the guardrails, the structured-output path and the request
|
|
207
|
+
* settings identical between the two, and `reduced` is the name the runtime collects
|
|
208
|
+
* rather than one the body would have chosen.
|
|
209
|
+
*/
|
|
210
|
+
function reduceBody(ir, step) {
|
|
211
|
+
const prompt = ir.prompts.find((candidate) => candidate.id === step.prompt);
|
|
212
|
+
const variable = prompt?.variables[0]?.name ?? ITERATION_ITEM;
|
|
213
|
+
return {
|
|
214
|
+
kind: 'generate',
|
|
215
|
+
prompt: step.prompt,
|
|
216
|
+
model: step.model,
|
|
217
|
+
as: REDUCED_BINDING,
|
|
218
|
+
input: { [variable]: { kind: 'reference', path: [ITERATION_ITEM] } },
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* The name and TypeScript type a step binds, or `undefined` for a step that binds nothing.
|
|
223
|
+
*
|
|
224
|
+
* `RetrievalResult`, `string`, `Array<Record<string, unknown>>` and `unknown[]` are what
|
|
225
|
+
* the runtime itself binds: a retrieval step binds a result object, an agent step binds
|
|
226
|
+
* the answer string, a `map` binds one record of the names its body bound per item, and a
|
|
227
|
+
* `reduce` binds one `reduced` value per item. A `branch`, `parallel`, `guard` or `emit`
|
|
228
|
+
* binds nothing of its own, so it has no entry and a read of it stays uncast.
|
|
229
|
+
*/
|
|
230
|
+
function bindingType(ir, step) {
|
|
231
|
+
switch (step.kind) {
|
|
232
|
+
case 'generate': {
|
|
233
|
+
const prompt = ir.prompts.find((candidate) => candidate.id === step.prompt);
|
|
234
|
+
const schema = prompt?.output.schema;
|
|
235
|
+
return {
|
|
236
|
+
name: step.as,
|
|
237
|
+
type: schema !== undefined && schema.kind === 'named' ? className(schema.name) : 'string',
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
case 'tool': {
|
|
241
|
+
const tool = ir.tools.find((candidate) => candidate.id === step.tool);
|
|
242
|
+
return { name: step.as, type: schemaNameFor(tool?.output) ?? 'string' };
|
|
243
|
+
}
|
|
244
|
+
case 'retrieve':
|
|
245
|
+
case 'rerank':
|
|
246
|
+
return { name: step.as, type: 'RetrievalResult' };
|
|
247
|
+
case 'agent':
|
|
248
|
+
return { name: step.as, type: 'string' };
|
|
249
|
+
case 'map':
|
|
250
|
+
return { name: step.as, type: 'Array<Record<string, unknown>>' };
|
|
251
|
+
case 'reduce':
|
|
252
|
+
return { name: step.as, type: 'unknown[]' };
|
|
253
|
+
default:
|
|
254
|
+
return undefined;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Every binding a pipeline's steps can leave, by name.
|
|
259
|
+
*
|
|
260
|
+
* A dotted read needs it: `require` is typed `unknown` and the runtime offers nothing
|
|
261
|
+
* narrower, so the emitter casts to the type the spec declared for that binding. The walk
|
|
262
|
+
* covers nested steps, because a `branch` arm's binding is read by a later top-level step.
|
|
263
|
+
* A name with no entry is one the gates have already refused to let a step read.
|
|
264
|
+
*/
|
|
265
|
+
function bindingTypes(ir, pipeline) {
|
|
266
|
+
const types = new Map();
|
|
267
|
+
for (const { step } of walkSteps(ir, pipeline.steps)) {
|
|
268
|
+
const binding = bindingType(ir, step);
|
|
269
|
+
if (binding !== undefined)
|
|
270
|
+
types.set(binding.name, binding.type);
|
|
271
|
+
}
|
|
272
|
+
return types;
|
|
273
|
+
}
|
|
274
|
+
function inputEntries(input, types) {
|
|
275
|
+
return Object.keys(input)
|
|
276
|
+
.sort()
|
|
277
|
+
.map((key) => [propertyKey(key), valueExpression(input[key], types)]);
|
|
278
|
+
}
|
|
279
|
+
function inputObject(input, indent = '', types) {
|
|
280
|
+
return objectLiteral(inputEntries(input, types), indent);
|
|
281
|
+
}
|
|
282
|
+
function camel(value) {
|
|
283
|
+
const pascal = className(value);
|
|
284
|
+
return `${pascal.charAt(0).toLowerCase()}${pascal.slice(1)}`;
|
|
285
|
+
}
|
|
286
|
+
function eventCast(ir, pipeline) {
|
|
287
|
+
return requestTypeName(ir, pipeline.id);
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* The module for one step, or the named diagnostic when this target has no emitter.
|
|
291
|
+
*
|
|
292
|
+
* D-4 lives here. The old form threw a bare `Error`, which reached a caller as an
|
|
293
|
+
* internal crash -- no code, no path, nothing a CLI could print as a finding. The
|
|
294
|
+
* refusal is now the *same* diagnostic the validate layer produces, wrapped in
|
|
295
|
+
* `TsVercelPlanError` so `problems[0].code` is `step-unsupported` however it is reached.
|
|
296
|
+
* The validator runs first and is the surface users see; this is the backstop that keeps
|
|
297
|
+
* the two from disagreeing.
|
|
298
|
+
*/
|
|
299
|
+
function leafModule(ir, pipeline, step, path, contextType) {
|
|
300
|
+
switch (step.kind) {
|
|
301
|
+
case 'emit':
|
|
302
|
+
// The step declares which value the answer is; the route reads that binding, so
|
|
303
|
+
// there is no function to emit.
|
|
304
|
+
return '';
|
|
305
|
+
case 'tool':
|
|
306
|
+
return toolStepModule(ir, pipeline, step, path, contextType);
|
|
307
|
+
case 'retrieve':
|
|
308
|
+
return retrieveStepModule(ir, pipeline, step, path, contextType);
|
|
309
|
+
case 'generate':
|
|
310
|
+
return generateStepModule(ir, pipeline, step, path, contextType);
|
|
311
|
+
case 'rerank':
|
|
312
|
+
return rerankStepModule(ir, pipeline, step, path, contextType);
|
|
313
|
+
case 'guard':
|
|
314
|
+
return guardStepModule(ir, pipeline, step, path, contextType);
|
|
315
|
+
case 'agent':
|
|
316
|
+
return agentStepModule(ir, pipeline, step, path, contextType);
|
|
317
|
+
default:
|
|
318
|
+
throw new TsVercelPlanError([stepUnsupported(step.kind, stepAt(pipeline, path))]);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
/** The four step kinds whose code is a wrapper over a runtime helper. */
|
|
322
|
+
function isCompositeKind(step) {
|
|
323
|
+
return (step.kind === 'branch' ||
|
|
324
|
+
step.kind === 'parallel' ||
|
|
325
|
+
step.kind === 'map' ||
|
|
326
|
+
step.kind === 'reduce');
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* The `Pipeline` a composite arm or body runs, from already-emitted steps.
|
|
330
|
+
*
|
|
331
|
+
* Built with `new Pipeline([...])` rather than `Pipeline.of(...)` because `of` names each
|
|
332
|
+
* step after the function, and the emitted constant is not the name the spec gave the
|
|
333
|
+
* binding -- the trace of a nested arm should read `answer`, not `triageAnswerStep`.
|
|
334
|
+
*/
|
|
335
|
+
function pipelineOf(steps, indent) {
|
|
336
|
+
const entries = steps.map((step) => `[${literal(step.name)}, ${step.constant}]`);
|
|
337
|
+
const inline = `new Pipeline([${entries.join(', ')}])`;
|
|
338
|
+
if (fits(indent, inline))
|
|
339
|
+
return inline;
|
|
340
|
+
return ['new Pipeline([', ...entries.map((entry) => `${indent} ${entry},`), `${indent}])`].join('\n');
|
|
341
|
+
}
|
|
342
|
+
/** The `branches:` value: one `Pipeline.of(...)` per arm, always one per line. */
|
|
343
|
+
function branchList(arms, indent) {
|
|
344
|
+
const values = arms.map((arm) => pipelineOf(arm, `${indent} `));
|
|
345
|
+
return ['[', ...values.map((value) => `${indent} ${value},`), `${indent}]`].join('\n');
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* One composite step: children first, then the wrapper that calls the runtime helper.
|
|
349
|
+
*
|
|
350
|
+
* The wrapper is an ordinary `Step` that builds the helper and calls it, rather than the
|
|
351
|
+
* helper itself exported as the step, because the helper takes its max depth and its
|
|
352
|
+
* name as options and a `Step` takes neither. Nesting is bounded by `MAX_STEP_DEPTH`,
|
|
353
|
+
* which is the runtime's own constant passed through rather than a number written here:
|
|
354
|
+
* a spec cannot declare a deeper nest than the runtime will execute, and the validator
|
|
355
|
+
* does not have to duplicate the bound for the two to agree.
|
|
356
|
+
*
|
|
357
|
+
* The wrapper registers under its constant even when it binds (`map`, `reduce`): the
|
|
358
|
+
* runtime binds through `withValues`, so the registered name is for tracing, and the
|
|
359
|
+
* constant is unique across arms where a binding name is not.
|
|
360
|
+
*/
|
|
361
|
+
function compositeStep(ir, pipeline, step, path, contextType) {
|
|
362
|
+
const constant = stepConstant(pipeline, step, path);
|
|
363
|
+
const common = [
|
|
364
|
+
['maxDepth', 'MAX_STEP_DEPTH'],
|
|
365
|
+
['name', literal(constant)],
|
|
366
|
+
];
|
|
367
|
+
let children;
|
|
368
|
+
let entries;
|
|
369
|
+
let doc;
|
|
370
|
+
if (step.kind === 'branch') {
|
|
371
|
+
const then = emitSteps(ir, pipeline, step.then, contextType, [...path, 'then']);
|
|
372
|
+
const otherwise = step.else.length === 0
|
|
373
|
+
? []
|
|
374
|
+
: emitSteps(ir, pipeline, step.else, contextType, [...path, 'else']);
|
|
375
|
+
children = [...then, ...otherwise];
|
|
376
|
+
entries = [
|
|
377
|
+
['when', literal(step.when)],
|
|
378
|
+
['then', pipelineOf(then, ' ')],
|
|
379
|
+
...(otherwise.length === 0
|
|
380
|
+
? []
|
|
381
|
+
: [['otherwise', pipelineOf(otherwise, ' ')]]),
|
|
382
|
+
...common,
|
|
383
|
+
];
|
|
384
|
+
doc =
|
|
385
|
+
"Run the arm the caller's principal selects. `when` is answered from the principal " +
|
|
386
|
+
'and nothing else, so no earlier binding can decide it.';
|
|
387
|
+
}
|
|
388
|
+
else if (step.kind === 'parallel') {
|
|
389
|
+
const arms = step.branches.map((arm, armIndex) => emitSteps(ir, pipeline, arm, contextType, [...path, 'arm', String(armIndex)]));
|
|
390
|
+
children = arms.flat();
|
|
391
|
+
entries = [['branches', branchList(arms, ' ')], ...common];
|
|
392
|
+
doc =
|
|
393
|
+
`Run all ${step.branches.length} arms concurrently under the deployment's one ` +
|
|
394
|
+
'concurrency cap, then merge what they bound in arm order.';
|
|
395
|
+
}
|
|
396
|
+
else if (step.kind === 'map') {
|
|
397
|
+
const body = emitSteps(ir, pipeline, [step.step], contextType, [...path, 'step']);
|
|
398
|
+
children = body;
|
|
399
|
+
entries = [
|
|
400
|
+
['over', literal(step.over)],
|
|
401
|
+
['as', literal(step.as)],
|
|
402
|
+
['step', pipelineOf(body, ' ')],
|
|
403
|
+
...common,
|
|
404
|
+
];
|
|
405
|
+
doc =
|
|
406
|
+
`Run the body once per item of \`${step.over}\`, under the concurrency cap, and ` +
|
|
407
|
+
`bind the results in item order as \`${step.as}\`.`;
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
410
|
+
const body = reduceBody(ir, step);
|
|
411
|
+
const bodyPath = [...path, 'item'];
|
|
412
|
+
const bodyConstant = stepConstant(pipeline, body, bodyPath);
|
|
413
|
+
children = [
|
|
414
|
+
{
|
|
415
|
+
text: generateStepModule(ir, pipeline, body, bodyPath, contextType),
|
|
416
|
+
constant: bodyConstant,
|
|
417
|
+
name: REDUCED_BINDING,
|
|
418
|
+
},
|
|
419
|
+
];
|
|
420
|
+
entries = [
|
|
421
|
+
['over', literal(step.over)],
|
|
422
|
+
['as', literal(step.as)],
|
|
423
|
+
['step', `new Pipeline([[${literal(REDUCED_BINDING)}, ${bodyConstant}]])`],
|
|
424
|
+
...common,
|
|
425
|
+
];
|
|
426
|
+
doc =
|
|
427
|
+
`Render \`${step.prompt}\` once per item of \`${step.over}\`, under the concurrency ` +
|
|
428
|
+
`cap, and bind the replies in item order as \`${step.as}\`.`;
|
|
429
|
+
}
|
|
430
|
+
const wrapper = [
|
|
431
|
+
`/** ${doc} */`,
|
|
432
|
+
`export const ${constant}: Step = async (context) => {`,
|
|
433
|
+
` const step = ${step.kind === 'map' || step.kind === 'reduce' ? `${step.kind}Step` : step.kind}({`,
|
|
434
|
+
// `then` is the runtime's own field name for a branch arm. It is a `Pipeline`, not a
|
|
435
|
+
// function, so the object is not thenable -- but the lint rule cannot know that.
|
|
436
|
+
...entries.flatMap(([key, value]) => key === 'then'
|
|
437
|
+
? [
|
|
438
|
+
' // biome-ignore lint/suspicious/noThenProperty: branch arm data, not a thenable',
|
|
439
|
+
` then: ${value},`,
|
|
440
|
+
]
|
|
441
|
+
: [` ${key}: ${value},`]),
|
|
442
|
+
' });',
|
|
443
|
+
' return await step(context);',
|
|
444
|
+
'};',
|
|
445
|
+
].join('\n');
|
|
446
|
+
const text = [...children.map((child) => child.text).filter((part) => part.length > 0), wrapper];
|
|
447
|
+
return { text: text.join('\n\n'), constant, name: constant };
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Every step of one list, flattened into the modules the pipeline file contains.
|
|
451
|
+
*
|
|
452
|
+
* A composite's children are emitted here rather than by `compositeStep`'s caller so the
|
|
453
|
+
* recursion has one owner: the wrapper's text already carries its children's, which is
|
|
454
|
+
* what lets a composite nest inside another without the outer one knowing.
|
|
455
|
+
*/
|
|
456
|
+
function emitSteps(ir, pipeline, steps, contextType, parentPath) {
|
|
457
|
+
const emitted = [];
|
|
458
|
+
for (const [index, step] of steps.entries()) {
|
|
459
|
+
const path = [...parentPath, String(index)];
|
|
460
|
+
if (step.kind === 'emit')
|
|
461
|
+
continue;
|
|
462
|
+
if (isCompositeKind(step)) {
|
|
463
|
+
emitted.push(compositeStep(ir, pipeline, step, path, contextType));
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
const constant = stepConstant(pipeline, step, path);
|
|
467
|
+
emitted.push({
|
|
468
|
+
text: leafModule(ir, pipeline, step, path, contextType),
|
|
469
|
+
constant,
|
|
470
|
+
name: isBindingStep(step) ? step.as : constant,
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
return emitted;
|
|
474
|
+
}
|
|
475
|
+
function eventLine(contextType, ...bodies) {
|
|
476
|
+
const usesEvent = bodies.some((body) => body.includes('event.'));
|
|
477
|
+
return usesEvent ? [` const event = context.require('event') as ${contextType};`] : [];
|
|
478
|
+
}
|
|
479
|
+
function toolStepModule(ir, pipeline, step, path, contextType) {
|
|
480
|
+
const constant = stepConstant(pipeline, step, path);
|
|
481
|
+
const tool = ir.tools.find((candidate) => candidate.id === step.tool);
|
|
482
|
+
const callId = `${pipeline.id}:${step.as}:${path.join(':')}`;
|
|
483
|
+
const output = tool === undefined ? undefined : schemaNameFor(tool.output);
|
|
484
|
+
const parseLine = output === undefined
|
|
485
|
+
? ` const value = JSON.parse(result.content) as unknown;`
|
|
486
|
+
: ` const value = ${output}.parse(JSON.parse(result.content));`;
|
|
487
|
+
const args = inputObject(step.input, '', bindingTypes(ir, pipeline));
|
|
488
|
+
const callLiteral = objectLiteral([
|
|
489
|
+
['id', literal(callId)],
|
|
490
|
+
['name', literal(step.tool)],
|
|
491
|
+
['arguments', args],
|
|
492
|
+
], ' ');
|
|
493
|
+
// A `tool-call` declaration is enforced here rather than in the agent loop, because a
|
|
494
|
+
// pipeline `tool` step is the one tool call no agent makes. The refusal has to happen
|
|
495
|
+
// before `execute`, or a guardrail would annotate a call it already ran.
|
|
496
|
+
const check = `enforceGuardrail(ENGINE.checkToolCall(TOOL_CALL_GUARDRAILS, ${literal(step.tool)}, call.arguments));`;
|
|
497
|
+
const checkLines = usesStage(ir, 'tool-call')
|
|
498
|
+
? ` ${check}`.length <= 100
|
|
499
|
+
? [` ${check}`]
|
|
500
|
+
: [
|
|
501
|
+
' enforceGuardrail(',
|
|
502
|
+
` ENGINE.checkToolCall(TOOL_CALL_GUARDRAILS, ${literal(step.tool)}, call.arguments),`,
|
|
503
|
+
' );',
|
|
504
|
+
]
|
|
505
|
+
: [];
|
|
506
|
+
return [
|
|
507
|
+
`/** \`${step.as}\`: call the \`${step.tool}\` tool. */`,
|
|
508
|
+
`export const ${constant}: Step = async (context) => {`,
|
|
509
|
+
' const deps = getDeps();',
|
|
510
|
+
...eventLine(contextType, args),
|
|
511
|
+
` const call = new ToolCall(${callLiteral});`,
|
|
512
|
+
...checkLines,
|
|
513
|
+
' const result = await deps.tools.execute(call);',
|
|
514
|
+
' if (result.isError) {',
|
|
515
|
+
` throw new PipelineError(\`tool ${step.tool} failed: \${result.content}\`);`,
|
|
516
|
+
' }',
|
|
517
|
+
parseLine,
|
|
518
|
+
` return context.withValues({ ${propertyKey(step.as)}: value });`,
|
|
519
|
+
'};',
|
|
520
|
+
].join('\n');
|
|
521
|
+
}
|
|
522
|
+
function schemaNameFor(ref) {
|
|
523
|
+
if (ref === undefined || ref.kind !== 'named')
|
|
524
|
+
return undefined;
|
|
525
|
+
return className(ref.name);
|
|
526
|
+
}
|
|
527
|
+
function retrieveStepModule(ir, pipeline, step, path, contextType) {
|
|
528
|
+
void ir;
|
|
529
|
+
const constant = stepConstant(pipeline, step, path);
|
|
530
|
+
const query = templateToTs(step.query);
|
|
531
|
+
const inline = ` const ${propertyKey(step.as)} = await deps.retriever(${literal(step.retriever)}).retrieve(${query});`;
|
|
532
|
+
const retrieveLine = inline.length <= 100
|
|
533
|
+
? [inline]
|
|
534
|
+
: [
|
|
535
|
+
` const ${propertyKey(step.as)} = await deps`,
|
|
536
|
+
` .retriever(${literal(step.retriever)})`,
|
|
537
|
+
` .retrieve(${query});`,
|
|
538
|
+
];
|
|
539
|
+
return [
|
|
540
|
+
`/** \`${step.as}\`: retrieve from the \`${step.retriever}\` binding. */`,
|
|
541
|
+
`export const ${constant}: Step = async (context) => {`,
|
|
542
|
+
' const deps = getDeps();',
|
|
543
|
+
...eventLine(contextType, query),
|
|
544
|
+
...retrieveLine,
|
|
545
|
+
` return context.withValues({ ${propertyKey(step.as)}: ${propertyKey(step.as)} });`,
|
|
546
|
+
'};',
|
|
547
|
+
].join('\n');
|
|
548
|
+
}
|
|
549
|
+
function generateStepModule(ir, pipeline, step, path, contextType) {
|
|
550
|
+
const constant = stepConstant(pipeline, step, path);
|
|
551
|
+
const prompt = ir.prompts.find((candidate) => candidate.id === step.prompt);
|
|
552
|
+
const types = bindingTypes(ir, pipeline);
|
|
553
|
+
const variables = prompt?.variables.map((variable) => variable.name) ?? Object.keys(step.input).sort();
|
|
554
|
+
const bindings = variables.map((name) => {
|
|
555
|
+
if (step.context === name) {
|
|
556
|
+
return `${propertyKey(name)}: (context.require(${literal(name)}) as RetrievalResult).context`;
|
|
557
|
+
}
|
|
558
|
+
const value = step.input[name];
|
|
559
|
+
if (value === undefined)
|
|
560
|
+
return `${propertyKey(name)}: context.require(${literal(name)})`;
|
|
561
|
+
return `${propertyKey(name)}: ${valueExpression(value, types)}`;
|
|
562
|
+
});
|
|
563
|
+
const outputConstantName = outputConstant(pipeline, step, path);
|
|
564
|
+
const inputCheck = usesStage(ir, 'input');
|
|
565
|
+
const outputCheck = usesStage(ir, 'output');
|
|
566
|
+
return [
|
|
567
|
+
`/** \`${step.as}\`: render \`${step.prompt}\` and run it through the gateway. */`,
|
|
568
|
+
`export const ${constant}: Step = async (context) => {`,
|
|
569
|
+
' const deps = getDeps();',
|
|
570
|
+
...eventLine(contextType, ...bindings),
|
|
571
|
+
` const rendered = deps.prompts.render(${literal(step.prompt)}, {`,
|
|
572
|
+
...bindings.map((binding) => ` ${binding},`),
|
|
573
|
+
' });',
|
|
574
|
+
...(inputCheck
|
|
575
|
+
? [' const verdict = enforceGuardrail(ENGINE.checkInput(INPUT_GUARDRAILS, rendered.text));']
|
|
576
|
+
: []),
|
|
577
|
+
' const call = async (text: string): Promise<string> => {',
|
|
578
|
+
' const result = await deps.gateway.generate(',
|
|
579
|
+
` ${literal(step.model)},`,
|
|
580
|
+
' new CompletionRequest({ messages: [Message.user(text)] }),',
|
|
581
|
+
` { fallbacks: FALLBACKS${memberAccess(step.model)} ?? [] },`,
|
|
582
|
+
' );',
|
|
583
|
+
...(outputCheck
|
|
584
|
+
? [' return enforceGuardrail(ENGINE.checkOutput(OUTPUT_GUARDRAILS, result.text)).text;']
|
|
585
|
+
: [' return result.text;']),
|
|
586
|
+
' };',
|
|
587
|
+
` const ${propertyKey(step.as)} = await runStructured(call, ${outputConstantName}, ${inputCheck ? 'verdict.text' : 'rendered.text'});`,
|
|
588
|
+
` return context.withValues({ ${propertyKey(step.as)}: ${propertyKey(step.as)} });`,
|
|
589
|
+
'};',
|
|
590
|
+
].join('\n');
|
|
591
|
+
}
|
|
592
|
+
/** The engine method one guard stage calls. `tool-call` is absent: a step names no tool. */
|
|
593
|
+
const GUARD_METHODS = {
|
|
594
|
+
input: 'checkInput',
|
|
595
|
+
output: 'checkOutput',
|
|
596
|
+
retrieval: 'checkRetrieval',
|
|
597
|
+
};
|
|
598
|
+
/** One step's address in the spec, which is what a plan finding points at. */
|
|
599
|
+
function stepAt(pipeline, path) {
|
|
600
|
+
return `pipelines.${pipeline.id}.steps.${path.join('.')}`;
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* A rerank step: rescore the passages a retrieval binding holds, and bind the reordered
|
|
604
|
+
* result.
|
|
605
|
+
*
|
|
606
|
+
* `rerank` returns a list of items, not a result, so the step rebuilds the
|
|
607
|
+
* `RetrievalResult` around the rescored items and replaces only their order: the
|
|
608
|
+
* retriever, the query and the citations the retrieval established survive, which is
|
|
609
|
+
* what lets a later step still read `.context` and `.top(n)` off the binding.
|
|
610
|
+
*
|
|
611
|
+
* The scorer comes from `rerankerFor`, not from `new LexicalReranker()`, so a declared
|
|
612
|
+
* model the runtime cannot honour fails loudly instead of being served by whatever
|
|
613
|
+
* scorer happens to exist. The plan gate refuses a non-lexical model before emit; the
|
|
614
|
+
* null check below is the backstop for a spec edited by hand.
|
|
615
|
+
*/
|
|
616
|
+
function rerankStepModule(ir, pipeline, step, path, contextType) {
|
|
617
|
+
const constant = stepConstant(pipeline, step, path);
|
|
618
|
+
const query = contextTemplate(step.query, bindingTypes(ir, pipeline));
|
|
619
|
+
const as = propertyKey(step.as);
|
|
620
|
+
const message = `rerank model ${step.model} did not resolve to a reranker`;
|
|
621
|
+
return [
|
|
622
|
+
`/** \`${step.as}\`: rescore the \`${step.over}\` passages and bind the top ${step.topN}. */`,
|
|
623
|
+
`export const ${constant}: Step = async (context) => {`,
|
|
624
|
+
...eventLine(contextType, query),
|
|
625
|
+
` const query = ${query};`,
|
|
626
|
+
` const over = context.require(${literal(step.over)}) as RetrievalResult;`,
|
|
627
|
+
` const reranker = rerankerFor(new RerankSpec({ model: ${literal(step.model)}, topN: ${step.topN} }));`,
|
|
628
|
+
' if (reranker === null) {',
|
|
629
|
+
` throw new ConfigurationError(${literal(message)});`,
|
|
630
|
+
' }',
|
|
631
|
+
` const ${as} = new RetrievalResult({`,
|
|
632
|
+
' retriever: over.retriever,',
|
|
633
|
+
' query: over.query,',
|
|
634
|
+
` items: await reranker.rerank(query, over.items, { topN: ${step.topN} }),`,
|
|
635
|
+
' });',
|
|
636
|
+
` return context.withValues({ ${as}: ${as} });`,
|
|
637
|
+
'};',
|
|
638
|
+
].join('\n');
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* A guard step: run one declared guardrail at one stage, over one payload.
|
|
642
|
+
*
|
|
643
|
+
* It binds nothing and returns the context unchanged. A step that decides and passes the
|
|
644
|
+
* context on is the whole of what a pipeline does with a `block`: the runtime raises, the
|
|
645
|
+
* pipeline wraps that as a `PipelineError`, and the route answers 400. Nothing here
|
|
646
|
+
* inspects the verdict, because the verdict a caller may ignore is exactly the one it
|
|
647
|
+
* must not -- and a `redact` at this stage is therefore a no-op on the bindings, which is
|
|
648
|
+
* why the step is emitted at all only to *refuse*.
|
|
649
|
+
*
|
|
650
|
+
* The step resolves the one guardrail it names through `guardrailById` rather than
|
|
651
|
+
* running the stage's whole table: unlike the route-level check, a guard step is an
|
|
652
|
+
* assertion about a named declaration.
|
|
653
|
+
*/
|
|
654
|
+
function guardStepModule(ir, pipeline, step, path, contextType) {
|
|
655
|
+
if (step.stage === 'tool-call') {
|
|
656
|
+
// The agent loop is the only caller of `checkToolCall` at plan time, so a pipeline
|
|
657
|
+
// step at this stage would check a tool it does not call.
|
|
658
|
+
throw new TsVercelPlanError([
|
|
659
|
+
stepUnsupported('guard at the tool-call stage', stepAt(pipeline, path)),
|
|
660
|
+
]);
|
|
661
|
+
}
|
|
662
|
+
const constant = stepConstant(pipeline, step, path);
|
|
663
|
+
const method = GUARD_METHODS[step.stage];
|
|
664
|
+
// The engine's stage methods take the *table* to run, so a single named declaration is
|
|
665
|
+
// handed over as a one-element table. The engine keeps only the declarations that
|
|
666
|
+
// declare this stage, which is why the plan refuses a step whose guardrail does not.
|
|
667
|
+
const config = `[guardrailById(${literal(step.guardrail)})]`;
|
|
668
|
+
const payloadLine = guardPayload(ir, step, pipeline, path);
|
|
669
|
+
return [
|
|
670
|
+
`/** \`${step.guardrail}\` at the \`${step.stage}\` stage: refuse the payload, bind nothing. */`,
|
|
671
|
+
`export const ${constant}: Step = async (context) => {`,
|
|
672
|
+
...eventLine(contextType, payloadLine.join('\n')),
|
|
673
|
+
...payloadLine,
|
|
674
|
+
` enforceGuardrail(ENGINE.${method}(${config}, payload));`,
|
|
675
|
+
' return context;',
|
|
676
|
+
'};',
|
|
677
|
+
].join('\n');
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* The payload a guard step hands the engine.
|
|
681
|
+
*
|
|
682
|
+
* At `input` and `output` it is the step's template rendered against the pipeline's
|
|
683
|
+
* bindings, because the engine reads `{ text }` there. At `retrieval` it is the *strings*
|
|
684
|
+
* a model would be handed -- `item.chunk.text` over the binding's items -- because that
|
|
685
|
+
* stage guards what would reach the prompt, not the result object. `tool-call` never
|
|
686
|
+
* reaches here; `guardStepModule` refuses it.
|
|
687
|
+
*/
|
|
688
|
+
function guardPayload(ir, step, pipeline, path) {
|
|
689
|
+
if (step.stage !== 'retrieval') {
|
|
690
|
+
return [` const payload = ${contextTemplate(step.text, bindingTypes(ir, pipeline))};`];
|
|
691
|
+
}
|
|
692
|
+
const binding = firstReference(step.text);
|
|
693
|
+
if (binding === undefined) {
|
|
694
|
+
throw new TsVercelPlanError([
|
|
695
|
+
problem('step-invalid', stepAt(pipeline, path), `a guard step at the retrieval stage must name the retrieval binding to guard, ` +
|
|
696
|
+
`and ${literal(step.text)} names none`),
|
|
697
|
+
]);
|
|
698
|
+
}
|
|
699
|
+
return [
|
|
700
|
+
` const over = context.require(${literal(binding)}) as RetrievalResult;`,
|
|
701
|
+
' const payload = over.items.map((item) => item.chunk.text);',
|
|
702
|
+
];
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* An agent step: run a declared agent over the rendered text, and bind its answer.
|
|
706
|
+
*
|
|
707
|
+
* The runtime the step builds is the app's one agent document over the app's gateway, so
|
|
708
|
+
* an agent a pipeline runs is the same agent the rest of the app declares -- tools,
|
|
709
|
+
* guardrails, memory and budget all come from the spec, not from the step. The step adds
|
|
710
|
+
* only the two things a pipeline knows: the text, and the principal the request carries.
|
|
711
|
+
*
|
|
712
|
+
* A run that stops short of `completed` -- budget exhausted, a guardrail that escalates --
|
|
713
|
+
* raises rather than binding a partial answer, because an `emit` downstream cannot tell a
|
|
714
|
+
* truncated answer from a finished one.
|
|
715
|
+
*/
|
|
716
|
+
function agentStepModule(ir, pipeline, step, path, contextType) {
|
|
717
|
+
const constant = stepConstant(pipeline, step, path);
|
|
718
|
+
const as = propertyKey(step.as);
|
|
719
|
+
const text = contextTemplate(step.text, bindingTypes(ir, pipeline));
|
|
720
|
+
return [
|
|
721
|
+
`/** \`${step.as}\`: run the \`${step.agent}\` agent and bind its answer. */`,
|
|
722
|
+
`export const ${constant}: Step = async (context) => {`,
|
|
723
|
+
' const deps = getDeps();',
|
|
724
|
+
...eventLine(contextType, text),
|
|
725
|
+
` const text = ${text};`,
|
|
726
|
+
` const outcome = await buildAgentRuntime(deps).run(${literal(step.agent)}, text, {`,
|
|
727
|
+
' principal: principalOf(context),',
|
|
728
|
+
' });',
|
|
729
|
+
" if (outcome.status !== 'completed' || outcome.answer === undefined) {",
|
|
730
|
+
` throw new PipelineError(\`agent ${step.agent} did not complete: \${outcome.status}\`);`,
|
|
731
|
+
' }',
|
|
732
|
+
` const ${as} = outcome.answer;`,
|
|
733
|
+
` return context.withValues({ ${as}: ${as} });`,
|
|
734
|
+
'};',
|
|
735
|
+
].join('\n');
|
|
736
|
+
}
|
|
737
|
+
/** The runtime helpers a set of steps needs, one entry per composite kind present. */
|
|
738
|
+
function compositeHelpers(steps) {
|
|
739
|
+
const kinds = new Set(steps.filter(isCompositeKind).map((step) => step.kind));
|
|
740
|
+
return [
|
|
741
|
+
...(kinds.has('branch') ? ['branch'] : []),
|
|
742
|
+
...(kinds.has('parallel') ? ['parallel'] : []),
|
|
743
|
+
...(kinds.has('map') ? ['mapStep'] : []),
|
|
744
|
+
...(kinds.has('reduce') ? ['reduceStep'] : []),
|
|
745
|
+
];
|
|
746
|
+
}
|
|
747
|
+
export function emitPipelineModule(ir, pipeline) {
|
|
748
|
+
const contextType = eventCast(ir, pipeline);
|
|
749
|
+
const steps = emitSteps(ir, pipeline, pipeline.steps, contextType, []);
|
|
750
|
+
const all = [...walkSteps(ir, pipeline.steps)].map((entry) => entry.step);
|
|
751
|
+
const hasStructured = all.some((step) => step.kind === 'generate');
|
|
752
|
+
const hasTool = all.some((step) => step.kind === 'tool');
|
|
753
|
+
const hasComposite = all.some(isCompositeKind);
|
|
754
|
+
const hasRerank = all.some((step) => step.kind === 'rerank');
|
|
755
|
+
const hasGuardStep = all.some((step) => step.kind === 'guard');
|
|
756
|
+
const hasAgentStep = all.some((step) => step.kind === 'agent');
|
|
757
|
+
// The routes check the whole stage table, so a route that generates at all runs the
|
|
758
|
+
// `input` and `output` declarations; a `tool` step runs the `tool-call` ones.
|
|
759
|
+
const inlineInput = hasStructured && usesStage(ir, 'input');
|
|
760
|
+
const inlineOutput = hasStructured && usesStage(ir, 'output');
|
|
761
|
+
const inlineToolCall = hasTool && usesStage(ir, 'tool-call');
|
|
762
|
+
const guardCheck = hasGuardStep || inlineInput || inlineOutput || inlineToolCall;
|
|
763
|
+
const usesRetrieved = all.some((step) => step.kind === 'generate' && step.context !== undefined) ||
|
|
764
|
+
all.some((step) => step.kind === 'rerank') ||
|
|
765
|
+
all.some((step) => step.kind === 'guard' && step.stage === 'retrieval');
|
|
766
|
+
const runtimeValues = [
|
|
767
|
+
...(hasRerank ? ['ConfigurationError', 'RerankSpec', 'rerankerFor'] : []),
|
|
768
|
+
...(hasComposite ? ['MAX_STEP_DEPTH'] : []),
|
|
769
|
+
...(hasStructured ? ['CompletionRequest', 'Message'] : []),
|
|
770
|
+
'Pipeline',
|
|
771
|
+
...(hasTool || hasAgentStep ? ['PipelineError'] : []),
|
|
772
|
+
...(hasRerank ? ['RetrievalResult'] : []),
|
|
773
|
+
...(hasTool ? ['ToolCall'] : []),
|
|
774
|
+
...(hasStructured ? ['runStructured', 'structuredOutput'] : []),
|
|
775
|
+
...compositeHelpers(all),
|
|
776
|
+
];
|
|
777
|
+
const guardrailNames = [
|
|
778
|
+
...(guardCheck ? ['ENGINE', 'enforceGuardrail'] : []),
|
|
779
|
+
...(hasGuardStep ? ['guardrailById'] : []),
|
|
780
|
+
...(inlineInput ? [stageTable('input')] : []),
|
|
781
|
+
...(inlineOutput ? [stageTable('output')] : []),
|
|
782
|
+
...(inlineToolCall ? [stageTable('tool-call')] : []),
|
|
783
|
+
];
|
|
784
|
+
const relativeImports = [
|
|
785
|
+
...(hasAgentStep
|
|
786
|
+
? [
|
|
787
|
+
{ specifier: '../agents.js', names: [{ name: 'buildAgentRuntime', type: false }] },
|
|
788
|
+
{ specifier: '../control.js', names: [{ name: 'principalOf', type: false }] },
|
|
789
|
+
]
|
|
790
|
+
: []),
|
|
791
|
+
{ specifier: '../deps.js', names: [{ name: 'getDeps', type: false }] },
|
|
792
|
+
...(hasStructured
|
|
793
|
+
? [{ specifier: '../gateway/models.js', names: [{ name: 'FALLBACKS', type: false }] }]
|
|
794
|
+
: []),
|
|
795
|
+
...(guardrailNames.length === 0
|
|
796
|
+
? []
|
|
797
|
+
: [
|
|
798
|
+
{
|
|
799
|
+
specifier: '../guardrails.js',
|
|
800
|
+
names: sortedNames(guardrailNames).map((name) => ({ name, type: false })),
|
|
801
|
+
},
|
|
802
|
+
]),
|
|
803
|
+
{ specifier: '../requests.js', names: [{ name: contextType, type: true }] },
|
|
804
|
+
].sort((a, b) => (a.specifier < b.specifier ? -1 : a.specifier > b.specifier ? 1 : 0));
|
|
805
|
+
const header = [
|
|
806
|
+
importStatement('@aid-framework/runtime', [
|
|
807
|
+
...sortedNames([...new Set(runtimeValues)]).map((name) => ({ name, type: false })),
|
|
808
|
+
...(usesRetrieved && !hasRerank ? [{ name: 'RetrievalResult', type: true }] : []),
|
|
809
|
+
{ name: 'Step', type: true },
|
|
810
|
+
]),
|
|
811
|
+
...relativeImports.map((entry) => importStatement(entry.specifier, entry.names)),
|
|
812
|
+
schemaImports(ir, pipeline),
|
|
813
|
+
]
|
|
814
|
+
.filter((line) => line.length > 0)
|
|
815
|
+
.join('\n');
|
|
816
|
+
const outputDeclaration = outputConstants(ir, pipeline);
|
|
817
|
+
const factory = [
|
|
818
|
+
'/** The declared pipeline, in step order. */',
|
|
819
|
+
`export function ${camel(pipeline.id)}Pipeline(): Pipeline {`,
|
|
820
|
+
' return new Pipeline([',
|
|
821
|
+
...steps.map((step) => ` [${literal(step.name)}, ${step.constant}],`),
|
|
822
|
+
' ]);',
|
|
823
|
+
'}',
|
|
824
|
+
].join('\n');
|
|
825
|
+
return generated(pipelinePath(toSnakeCase(pipeline.id)), moduleText(header, outputDeclaration, ...steps.map((step) => step.text).filter((text) => text.length > 0), factory));
|
|
826
|
+
}
|
|
827
|
+
function schemaImports(ir, pipeline) {
|
|
828
|
+
const names = [];
|
|
829
|
+
const add = (name) => {
|
|
830
|
+
if (name !== undefined && !names.includes(name))
|
|
831
|
+
names.push(name);
|
|
832
|
+
};
|
|
833
|
+
for (const { step } of walkSteps(ir, pipeline.steps)) {
|
|
834
|
+
if (step.kind === 'tool') {
|
|
835
|
+
const tool = ir.tools.find((candidate) => candidate.id === step.tool);
|
|
836
|
+
add(schemaNameFor(tool?.output));
|
|
837
|
+
}
|
|
838
|
+
if (step.kind === 'generate') {
|
|
839
|
+
const prompt = ir.prompts.find((candidate) => candidate.id === step.prompt);
|
|
840
|
+
const schema = prompt?.output.schema;
|
|
841
|
+
if (schema !== undefined && schema.kind === 'named')
|
|
842
|
+
add(className(schema.name));
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
if (names.length === 0)
|
|
846
|
+
return '';
|
|
847
|
+
return importStatement('../schemas.js', sortedNames(names).map((name) => ({ name, type: false })));
|
|
848
|
+
}
|
|
849
|
+
function outputConstants(ir, pipeline) {
|
|
850
|
+
const lines = [];
|
|
851
|
+
const emitted = new Set();
|
|
852
|
+
for (const { step, path } of walkSteps(ir, pipeline.steps)) {
|
|
853
|
+
if (step.kind !== 'generate')
|
|
854
|
+
continue;
|
|
855
|
+
const prompt = ir.prompts.find((candidate) => candidate.id === step.prompt);
|
|
856
|
+
const schema = prompt?.output.schema;
|
|
857
|
+
if (schema === undefined || schema.kind !== 'named')
|
|
858
|
+
continue;
|
|
859
|
+
const constant = outputConstant(pipeline, step, path);
|
|
860
|
+
if (emitted.has(constant))
|
|
861
|
+
continue;
|
|
862
|
+
emitted.add(constant);
|
|
863
|
+
lines.push(`const ${constant} = structuredOutput(${literal(schema.name)}, ${className(schema.name)});`);
|
|
864
|
+
}
|
|
865
|
+
if (lines.length === 0)
|
|
866
|
+
return '';
|
|
867
|
+
return lines.join('\n');
|
|
868
|
+
}
|
|
869
|
+
export function emitPipelinesIndex(ir) {
|
|
870
|
+
const imports = ir.pipelines.map((pipeline) => exportStatement(`./${toSnakeCase(pipeline.id)}.js`, [
|
|
871
|
+
[`${camel(pipeline.id)}Pipeline`, `${camel(pipeline.id)}Pipeline`],
|
|
872
|
+
]));
|
|
873
|
+
return generated(paths.pipelinesIndex, moduleText(...imports));
|
|
874
|
+
}
|
|
875
|
+
/**
|
|
876
|
+
* The structured-output class a `generate` step's reply parses to, when it declares one.
|
|
877
|
+
*
|
|
878
|
+
* The route reads the value the *last binding step* left, so the cast has to be derived
|
|
879
|
+
* from that step rather than from the first `generate` in the file: since composites
|
|
880
|
+
* exist those are no longer the same step, and a cast to the wrong class is a compile
|
|
881
|
+
* error in the emitted application rather than a wrong answer at run time.
|
|
882
|
+
*/
|
|
883
|
+
/**
|
|
884
|
+
* The schema the route casts the answer to, when the last binding has one.
|
|
885
|
+
*
|
|
886
|
+
* A `generate` step's answer is the prompt's declared output type and a `tool` step's is
|
|
887
|
+
* its output type, so both are objects the route can spread into the body. Every other
|
|
888
|
+
* binding -- an agent's answer is the obvious one -- is a string or a result object with
|
|
889
|
+
* no declared shape, and the route wraps those under `answer` instead.
|
|
890
|
+
*/
|
|
891
|
+
function structuredAnswer(ir, step) {
|
|
892
|
+
if (step.kind === 'generate') {
|
|
893
|
+
const prompt = ir.prompts.find((candidate) => candidate.id === step.prompt);
|
|
894
|
+
const schema = prompt?.output.schema;
|
|
895
|
+
return schema !== undefined && schema.kind === 'named' ? className(schema.name) : undefined;
|
|
896
|
+
}
|
|
897
|
+
if (step.kind === 'tool') {
|
|
898
|
+
const tool = ir.tools.find((candidate) => candidate.id === step.tool);
|
|
899
|
+
return schemaNameFor(tool?.output);
|
|
900
|
+
}
|
|
901
|
+
return undefined;
|
|
902
|
+
}
|
|
903
|
+
/**
|
|
904
|
+
* Whether a pipeline can raise a guardrail block, which is what decides the route's 400.
|
|
905
|
+
*
|
|
906
|
+
* A block arrives wrapped in the `PipelineError` the runtime raises around a failed step,
|
|
907
|
+
* so the route cannot answer 400 without digging it out of the cause chain -- and it
|
|
908
|
+
* should not pay for that in the routes that cannot block at all. An agent step counts
|
|
909
|
+
* because the agent loop enforces the guardrails its declaration names.
|
|
910
|
+
*/
|
|
911
|
+
function pipelineCanBlock(ir, pipeline) {
|
|
912
|
+
const all = [...walkSteps(ir, pipeline.steps)].map((entry) => entry.step);
|
|
913
|
+
return (all.some((step) => step.kind === 'guard') ||
|
|
914
|
+
all.some((step) => step.kind === 'agent') ||
|
|
915
|
+
(all.some((step) => step.kind === 'generate') &&
|
|
916
|
+
(usesStage(ir, 'input') || usesStage(ir, 'output'))) ||
|
|
917
|
+
(all.some((step) => step.kind === 'tool') && usesStage(ir, 'tool-call')));
|
|
918
|
+
}
|
|
919
|
+
export function emitRouteModule(ir, pipeline) {
|
|
920
|
+
const handler = `handle${className(pipeline.id)}`;
|
|
921
|
+
const requestType = eventCast(ir, pipeline);
|
|
922
|
+
const requestSchema = requestType;
|
|
923
|
+
const finalStep = [...pipeline.steps].reverse().find(isBindingStep);
|
|
924
|
+
const answer = finalStep === undefined ? undefined : structuredAnswer(ir, finalStep);
|
|
925
|
+
const trigger = pipeline.trigger.kind === 'http' ? pipeline.trigger : { method: 'POST', path: '/' };
|
|
926
|
+
const canBlock = pipelineCanBlock(ir, pipeline);
|
|
927
|
+
const header = [
|
|
928
|
+
importStatement('@aid-framework/runtime', [
|
|
929
|
+
{ name: 'PipelineError', type: false },
|
|
930
|
+
{ name: 'PipelineContext', type: true },
|
|
931
|
+
]),
|
|
932
|
+
importStatement('../control.js', [{ name: 'seeded', type: false }]),
|
|
933
|
+
...(canBlock
|
|
934
|
+
? [importStatement('../guardrails.js', [{ name: 'guardrailBlock', type: false }])]
|
|
935
|
+
: []),
|
|
936
|
+
importStatement(`../pipelines/${toSnakeCase(pipeline.id)}.js`, [
|
|
937
|
+
{ name: `${camel(pipeline.id)}Pipeline`, type: false },
|
|
938
|
+
]),
|
|
939
|
+
importStatement('../requests.js', [{ name: requestSchema, type: false }]),
|
|
940
|
+
...(answer === undefined
|
|
941
|
+
? []
|
|
942
|
+
: [importStatement('../schemas.js', [{ name: answer, type: true }])]),
|
|
943
|
+
]
|
|
944
|
+
.filter((line) => line.length > 0)
|
|
945
|
+
.join('\n');
|
|
946
|
+
const body = [
|
|
947
|
+
'/** The method and path this handler answers, as declared in the spec. */',
|
|
948
|
+
`export const ROUTE = { method: ${literal(trigger.method.toUpperCase())}, path: ${literal(trigger.path)} } as const;`,
|
|
949
|
+
'',
|
|
950
|
+
'/** The request half of the Fetch API that a route handler actually uses. */',
|
|
951
|
+
'export interface RouteRequest {',
|
|
952
|
+
' readonly json: () => Promise<unknown>;',
|
|
953
|
+
'}',
|
|
954
|
+
'',
|
|
955
|
+
'export interface RouteResponse {',
|
|
956
|
+
' readonly status: number;',
|
|
957
|
+
' readonly body: unknown;',
|
|
958
|
+
'}',
|
|
959
|
+
'',
|
|
960
|
+
'/**',
|
|
961
|
+
' * Run the pipeline for one request. The body is validated before the pipeline runs;',
|
|
962
|
+
' * a guardrail that refuses the request is a 400, and any other pipeline failure is a',
|
|
963
|
+
' * 502 -- the request was well formed, the run was not.',
|
|
964
|
+
' */',
|
|
965
|
+
`export async function ${handler}(request: RouteRequest): Promise<RouteResponse> {`,
|
|
966
|
+
' let payload: unknown;',
|
|
967
|
+
' try {',
|
|
968
|
+
' payload = await request.json();',
|
|
969
|
+
' } catch {',
|
|
970
|
+
" return { status: 400, body: { error: 'request body is not valid JSON' } };",
|
|
971
|
+
' }',
|
|
972
|
+
` const parsed = ${requestSchema}.safeParse(payload);`,
|
|
973
|
+
' if (!parsed.success) {',
|
|
974
|
+
" return { status: 400, body: { error: 'request body does not match the declared schema' } };",
|
|
975
|
+
' }',
|
|
976
|
+
' let context: PipelineContext;',
|
|
977
|
+
' try {',
|
|
978
|
+
` context = await ${camel(pipeline.id)}Pipeline().run(seeded(parsed.data));`,
|
|
979
|
+
' } catch (error) {',
|
|
980
|
+
...(canBlock
|
|
981
|
+
? [
|
|
982
|
+
' const block = guardrailBlock(error);',
|
|
983
|
+
' if (block !== undefined) {',
|
|
984
|
+
' return {',
|
|
985
|
+
' status: 400,',
|
|
986
|
+
' body: { error: block.message, guardrail: block.guardrail, stage: block.stage },',
|
|
987
|
+
' };',
|
|
988
|
+
' }',
|
|
989
|
+
]
|
|
990
|
+
: []),
|
|
991
|
+
' if (error instanceof PipelineError) {',
|
|
992
|
+
' return { status: 502, body: { error: error.message } };',
|
|
993
|
+
' }',
|
|
994
|
+
' throw error;',
|
|
995
|
+
' }',
|
|
996
|
+
finalStep === undefined
|
|
997
|
+
? ' return { status: 204, body: null };'
|
|
998
|
+
: [
|
|
999
|
+
` const answer = context.require(${literal(finalStep.as)}) as ${answer ?? 'unknown'};`,
|
|
1000
|
+
answer === undefined
|
|
1001
|
+
? ' return { status: 200, body: { answer } };'
|
|
1002
|
+
: ' return { status: 200, body: { ...answer } };',
|
|
1003
|
+
].join('\n'),
|
|
1004
|
+
'}',
|
|
1005
|
+
].join('\n');
|
|
1006
|
+
return generated(routePath(toSnakeCase(pipeline.id)), moduleText(header, body));
|
|
1007
|
+
}
|
|
1008
|
+
export function emitRoutesIndex(ir) {
|
|
1009
|
+
const lines = ir.pipelines.map((pipeline) => exportStatement(`./${toSnakeCase(pipeline.id)}.js`, [
|
|
1010
|
+
[`handle${className(pipeline.id)}`, `handle${className(pipeline.id)}`],
|
|
1011
|
+
['ROUTE', `${toSnakeCase(pipeline.id).toUpperCase()}_ROUTE`],
|
|
1012
|
+
]));
|
|
1013
|
+
return generated(paths.routesIndex, moduleText(...lines));
|
|
1014
|
+
}
|
|
1015
|
+
//# sourceMappingURL=pipelines.js.map
|