@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.
Files changed (80) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +123 -0
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/emit/agents.d.ts +62 -0
  5. package/dist/emit/agents.d.ts.map +1 -0
  6. package/dist/emit/agents.js +256 -0
  7. package/dist/emit/agents.js.map +1 -0
  8. package/dist/emit/business.d.ts +23 -0
  9. package/dist/emit/business.d.ts.map +1 -0
  10. package/dist/emit/business.js +414 -0
  11. package/dist/emit/business.js.map +1 -0
  12. package/dist/emit/control.d.ts +40 -0
  13. package/dist/emit/control.d.ts.map +1 -0
  14. package/dist/emit/control.js +108 -0
  15. package/dist/emit/control.js.map +1 -0
  16. package/dist/emit/file.d.ts +64 -0
  17. package/dist/emit/file.d.ts.map +1 -0
  18. package/dist/emit/file.js +80 -0
  19. package/dist/emit/file.js.map +1 -0
  20. package/dist/emit/guardrails.d.ts +45 -0
  21. package/dist/emit/guardrails.d.ts.map +1 -0
  22. package/dist/emit/guardrails.js +192 -0
  23. package/dist/emit/guardrails.js.map +1 -0
  24. package/dist/emit/infrastructure.d.ts +15 -0
  25. package/dist/emit/infrastructure.d.ts.map +1 -0
  26. package/dist/emit/infrastructure.js +238 -0
  27. package/dist/emit/infrastructure.js.map +1 -0
  28. package/dist/emit/pipelines.d.ts +46 -0
  29. package/dist/emit/pipelines.d.ts.map +1 -0
  30. package/dist/emit/pipelines.js +1015 -0
  31. package/dist/emit/pipelines.js.map +1 -0
  32. package/dist/emit/prompts.d.ts +14 -0
  33. package/dist/emit/prompts.d.ts.map +1 -0
  34. package/dist/emit/prompts.js +61 -0
  35. package/dist/emit/prompts.js.map +1 -0
  36. package/dist/emit/retrieval.d.ts +19 -0
  37. package/dist/emit/retrieval.d.ts.map +1 -0
  38. package/dist/emit/retrieval.js +206 -0
  39. package/dist/emit/retrieval.js.map +1 -0
  40. package/dist/emit/schemas.d.ts +22 -0
  41. package/dist/emit/schemas.d.ts.map +1 -0
  42. package/dist/emit/schemas.js +145 -0
  43. package/dist/emit/schemas.js.map +1 -0
  44. package/dist/emit/text.d.ts +86 -0
  45. package/dist/emit/text.d.ts.map +1 -0
  46. package/dist/emit/text.js +242 -0
  47. package/dist/emit/text.js.map +1 -0
  48. package/dist/emit/tools.d.ts +15 -0
  49. package/dist/emit/tools.d.ts.map +1 -0
  50. package/dist/emit/tools.js +225 -0
  51. package/dist/emit/tools.js.map +1 -0
  52. package/dist/errors.d.ts +12 -0
  53. package/dist/errors.d.ts.map +1 -0
  54. package/dist/errors.js +16 -0
  55. package/dist/errors.js.map +1 -0
  56. package/dist/index.d.ts +36 -0
  57. package/dist/index.d.ts.map +1 -0
  58. package/dist/index.js +47 -0
  59. package/dist/index.js.map +1 -0
  60. package/dist/manifest.d.ts +48 -0
  61. package/dist/manifest.d.ts.map +1 -0
  62. package/dist/manifest.js +88 -0
  63. package/dist/manifest.js.map +1 -0
  64. package/dist/names.d.ts +57 -0
  65. package/dist/names.d.ts.map +1 -0
  66. package/dist/names.js +294 -0
  67. package/dist/names.js.map +1 -0
  68. package/dist/placeholders.d.ts +48 -0
  69. package/dist/placeholders.d.ts.map +1 -0
  70. package/dist/placeholders.js +114 -0
  71. package/dist/placeholders.js.map +1 -0
  72. package/dist/plan.d.ts +18 -0
  73. package/dist/plan.d.ts.map +1 -0
  74. package/dist/plan.js +81 -0
  75. package/dist/plan.js.map +1 -0
  76. package/dist/validate.d.ts +88 -0
  77. package/dist/validate.d.ts.map +1 -0
  78. package/dist/validate.js +790 -0
  79. package/dist/validate.js.map +1 -0
  80. 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