@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
package/dist/validate.js
ADDED
|
@@ -0,0 +1,790 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What this target refuses to emit, and why.
|
|
3
|
+
*
|
|
4
|
+
* The generator consumes the IR and nothing else: no spec re-reading, no reach into
|
|
5
|
+
* `packages/spec`. Everything it needs to decide feasibility is already in the IR, so
|
|
6
|
+
* these checks are total over the IR and run before a single byte is rendered.
|
|
7
|
+
*
|
|
8
|
+
* The rule is fail-closed. A construct with no emitter is a problem, not a comment in
|
|
9
|
+
* the output: a generated app that boots while silently skipping a step is worse than
|
|
10
|
+
* one that was never written.
|
|
11
|
+
*/
|
|
12
|
+
import { isCompositeStep, isRequiredCapability, } from '@aid-framework/ir';
|
|
13
|
+
import { TS_VERCEL_MANIFEST, TS_VERCEL_TARGET } from './manifest.js';
|
|
14
|
+
import { modelCapabilityMember, providerAdapter } from './names.js';
|
|
15
|
+
import { templatePlaceholders } from './placeholders.js';
|
|
16
|
+
/**
|
|
17
|
+
* The guardrail policies the TypeScript runtime implements, mirroring its `DETECTORS`
|
|
18
|
+
* table one for one.
|
|
19
|
+
*
|
|
20
|
+
* The IR's vocabulary is wider than the engine's: `jailbreak`, `moderation` and
|
|
21
|
+
* `toxicity` are declared policy kinds with no detector behind them, and an emitted app
|
|
22
|
+
* that named one would raise on the first request that reached it. Refusing them here
|
|
23
|
+
* rather than at run time is the same rule the rest of this file follows.
|
|
24
|
+
*/
|
|
25
|
+
const IMPLEMENTED_POLICIES = new Set(['pii-detect', 'pii-redact', 'prompt-injection', 'secret-scan']);
|
|
26
|
+
/**
|
|
27
|
+
* The policies whose firing the runtime can mask rather than only report.
|
|
28
|
+
*
|
|
29
|
+
* `prompt-injection` detects intent, so there is no value to strip: the runtime raises
|
|
30
|
+
* instead of returning a cleaned payload, which makes `action: redact` a lie.
|
|
31
|
+
*/
|
|
32
|
+
const REDACTABLE_POLICIES = new Set(['pii-detect', 'pii-redact', 'secret-scan']);
|
|
33
|
+
/** Names a pipeline step may not bind, because the runtime already owns them. */
|
|
34
|
+
export const RESERVED_BINDINGS = ['context', 'deps', 'event', 'response', 'steps'];
|
|
35
|
+
const PRIMITIVE_TYPES = new Set([
|
|
36
|
+
'string',
|
|
37
|
+
'number',
|
|
38
|
+
'integer',
|
|
39
|
+
'boolean',
|
|
40
|
+
]);
|
|
41
|
+
/**
|
|
42
|
+
* Metrics this target can emit a working module for.
|
|
43
|
+
*
|
|
44
|
+
* Deliberately narrower than `MetricShape['kind']`: `cost` and `latency` are budgets the
|
|
45
|
+
* runtime enforces outside a metric, `tool-trace` needs a trace the runtime does not
|
|
46
|
+
* record yet, and `llm-judge` is not deterministic enough for a CI threshold. Declaring
|
|
47
|
+
* one would generate a module that silently scores nothing, so the plan refuses instead.
|
|
48
|
+
*/
|
|
49
|
+
const SUPPORTED_METRICS = new Set([
|
|
50
|
+
'json-schema',
|
|
51
|
+
'retrieval',
|
|
52
|
+
'exact',
|
|
53
|
+
'contains',
|
|
54
|
+
'assert',
|
|
55
|
+
]);
|
|
56
|
+
export function problem(code, path, message) {
|
|
57
|
+
return { code, path, message };
|
|
58
|
+
}
|
|
59
|
+
/** Whether a type reference can be rendered as a zod schema. */
|
|
60
|
+
function refRenderable(ref, declared) {
|
|
61
|
+
if (ref.kind === 'primitive')
|
|
62
|
+
return PRIMITIVE_TYPES.has(ref.name);
|
|
63
|
+
return declared.has(ref.name);
|
|
64
|
+
}
|
|
65
|
+
export function checkIrVersion(ir) {
|
|
66
|
+
if (ir.irVersion !== TS_VERCEL_MANIFEST.irRange.replace(/^\^/, '')) {
|
|
67
|
+
return [
|
|
68
|
+
problem('ir-version-unsupported', 'irVersion', `IR ${ir.irVersion} is outside this generator's range ${TS_VERCEL_MANIFEST.irRange}`),
|
|
69
|
+
];
|
|
70
|
+
}
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
export function checkTarget(ir) {
|
|
74
|
+
if (ir.project.targets.includes(TS_VERCEL_TARGET))
|
|
75
|
+
return [];
|
|
76
|
+
return [
|
|
77
|
+
problem('target-unsupported', 'project.targets', `the spec does not declare ${TS_VERCEL_TARGET}; declared targets: ${ir.project.targets.join(', ') || '(none)'}`),
|
|
78
|
+
];
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* `deployments`: only the concurrency cap is emitted, and only what the host can honour.
|
|
82
|
+
*
|
|
83
|
+
* The route handler this target emits answers with one buffered body, so a deployment
|
|
84
|
+
* asking for streaming is refused rather than emitted as something that only looks
|
|
85
|
+
* streamed. `secrets` needs no check: the adapters read a declared secret by name from
|
|
86
|
+
* the environment, so the declaration is documentation, not an artifact to emit.
|
|
87
|
+
*/
|
|
88
|
+
export function checkDeployments(ir) {
|
|
89
|
+
const problems = [];
|
|
90
|
+
for (const deployment of ir.deployments) {
|
|
91
|
+
const at = `deployments.${deployment.id}`;
|
|
92
|
+
if (deployment.streaming) {
|
|
93
|
+
problems.push(problem('deployment-unsupported', at, 'streaming needs a streaming host; the emitted route answers with one buffered body'));
|
|
94
|
+
}
|
|
95
|
+
if (deployment.concurrency !== undefined && deployment.concurrency < 1) {
|
|
96
|
+
problems.push(problem('deployment-unsupported', `${at}.concurrency`, `concurrency is the cap the emitter passes to the runtime's limiter, so it must be at least 1, got ${deployment.concurrency}`));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return problems;
|
|
100
|
+
}
|
|
101
|
+
export function checkObservability(ir) {
|
|
102
|
+
if (ir.observability.redact.length === 0)
|
|
103
|
+
return [];
|
|
104
|
+
return [
|
|
105
|
+
problem('observability-redact-unsupported', 'observability.redact', 'the runtime exposes no redaction hook, so declared redaction paths would not be honoured'),
|
|
106
|
+
];
|
|
107
|
+
}
|
|
108
|
+
export function checkRuntime(ir) {
|
|
109
|
+
if (ir.runtime.controlFlow === 'plain-async')
|
|
110
|
+
return [];
|
|
111
|
+
return [
|
|
112
|
+
problem('runtime-unsupported', 'runtime.controlFlow', `control flow ${ir.runtime.controlFlow} has no emitter; only plain-async is supported`),
|
|
113
|
+
];
|
|
114
|
+
}
|
|
115
|
+
export function checkTypes(ir) {
|
|
116
|
+
const problems = [];
|
|
117
|
+
const declared = new Set(ir.types.map((shape) => shape.id));
|
|
118
|
+
for (const shape of ir.types) {
|
|
119
|
+
problems.push(...checkTypeShape(shape, declared));
|
|
120
|
+
}
|
|
121
|
+
return problems;
|
|
122
|
+
}
|
|
123
|
+
function checkTypeShape(shape, declared) {
|
|
124
|
+
const problems = [];
|
|
125
|
+
const seen = new Set();
|
|
126
|
+
for (const field of shape.fields) {
|
|
127
|
+
const at = `types.${shape.id}.fields.${field.name}`;
|
|
128
|
+
if (seen.has(field.name)) {
|
|
129
|
+
problems.push(problem('field-unsupported', at, `duplicate field ${field.name}`));
|
|
130
|
+
}
|
|
131
|
+
seen.add(field.name);
|
|
132
|
+
if (field.type.list) {
|
|
133
|
+
problems.push(problem('type-unsupported', at, 'list-typed fields have no Phase 1 emitter; the source runtime does not model them'));
|
|
134
|
+
}
|
|
135
|
+
if (field.type.kind === 'primitive' && !PRIMITIVE_TYPES.has(field.type.name)) {
|
|
136
|
+
problems.push(problem('type-unsupported', at, `primitive ${field.type.name} has no zod mapping`));
|
|
137
|
+
}
|
|
138
|
+
if (field.type.kind === 'named' && !declared.has(field.type.name)) {
|
|
139
|
+
problems.push(problem('named-type-unresolved', at, `type ${field.type.name} is not declared`));
|
|
140
|
+
}
|
|
141
|
+
if (field.enum !== undefined && field.enum.length === 0) {
|
|
142
|
+
problems.push(problem('field-unsupported', at, 'an empty enum cannot be rendered as a literal'));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return problems;
|
|
146
|
+
}
|
|
147
|
+
export function checkModels(ir) {
|
|
148
|
+
const problems = [];
|
|
149
|
+
const promptModels = new Set(ir.prompts.map((prompt) => prompt.model));
|
|
150
|
+
for (const model of ir.models) {
|
|
151
|
+
const at = `models.${model.id}`;
|
|
152
|
+
if (providerAdapter(model.provider) === undefined) {
|
|
153
|
+
problems.push(problem('model-provider-unsupported', at, `provider ${model.provider} has no TypeScript adapter; the runtime ships fake and openai-compatible only`));
|
|
154
|
+
}
|
|
155
|
+
for (const capability of model.capabilities) {
|
|
156
|
+
if (modelCapabilityMember(capability) === undefined) {
|
|
157
|
+
problems.push(problem('model-capability-unsupported', at, `capability ${capability} has no runtime member, so the emitted ModelSpec would silently drop it`));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (promptModels.has(model.id) && !model.capabilities.includes('json-schema')) {
|
|
161
|
+
problems.push(problem('model-missing-capability', at, 'the model backs a prompt, but declares no json-schema capability, so a structured prompt cannot run on it'));
|
|
162
|
+
}
|
|
163
|
+
for (const fallback of model.fallbacks) {
|
|
164
|
+
if (!ir.models.some((other) => other.id === fallback)) {
|
|
165
|
+
problems.push(problem('reference-unresolved', `${at}.fallbacks`, `fallback model ${fallback} is not declared`));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return problems;
|
|
170
|
+
}
|
|
171
|
+
export function checkPrompts(ir) {
|
|
172
|
+
const problems = [];
|
|
173
|
+
const declared = new Set(ir.types.map((shape) => shape.id));
|
|
174
|
+
const models = new Set(ir.models.map((model) => model.id));
|
|
175
|
+
for (const prompt of ir.prompts) {
|
|
176
|
+
problems.push(...checkPromptShape(prompt, declared, models));
|
|
177
|
+
}
|
|
178
|
+
return problems;
|
|
179
|
+
}
|
|
180
|
+
function checkPromptShape(prompt, declared, models) {
|
|
181
|
+
const problems = [];
|
|
182
|
+
const at = `prompts.${prompt.id}`;
|
|
183
|
+
if (!models.has(prompt.model)) {
|
|
184
|
+
problems.push(problem('prompt-model-unresolved', at, `model ${prompt.model} is not declared`));
|
|
185
|
+
}
|
|
186
|
+
if (prompt.output.kind === 'structured') {
|
|
187
|
+
const schema = prompt.output.schema;
|
|
188
|
+
if (schema === undefined || !refRenderable(schema, declared)) {
|
|
189
|
+
problems.push(problem('prompt-output-unsupported', at, 'a structured prompt needs a resolvable schema, or there is nothing for zod to validate'));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
else if (prompt.output.kind === 'json') {
|
|
193
|
+
problems.push(problem('prompt-output-unsupported', at, 'json output has no schema to validate against; declare `structured` instead'));
|
|
194
|
+
}
|
|
195
|
+
for (const variable of prompt.variables) {
|
|
196
|
+
if (variable.type.list) {
|
|
197
|
+
problems.push(problem('prompt-variable-unsupported', at, `variable ${variable.name} is list-typed`));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
problems.push(...checkPromptTemplate(prompt, at));
|
|
201
|
+
return problems;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* The template's placeholders, checked against what the runtime can actually render.
|
|
205
|
+
*
|
|
206
|
+
* A generated prompt is stored as a `str.format` body, which cannot express attribute
|
|
207
|
+
* access and refuses a value the template never uses. Both defects would pass every
|
|
208
|
+
* byte-level golden and fail on the first request, so they are refused here instead.
|
|
209
|
+
*/
|
|
210
|
+
function checkPromptTemplate(prompt, at) {
|
|
211
|
+
const problems = [];
|
|
212
|
+
const declared = new Set(prompt.variables.map((variable) => variable.name));
|
|
213
|
+
for (const name of templatePlaceholders(prompt.template)) {
|
|
214
|
+
if (name.includes('.')) {
|
|
215
|
+
problems.push(problem('unsupported-placeholder', `${at}.template`, `'{{ ${name} }}' is a dotted placeholder; the runtime renders with str.format ` +
|
|
216
|
+
'keywords, which cannot express attribute access. Flatten it to a declared variable'));
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (!declared.has(name)) {
|
|
220
|
+
problems.push(problem('unsupported-placeholder', `${at}.template`, `'{{ ${name} }}' is not a declared variable, so the generated render call ` +
|
|
221
|
+
'would ask the registry for a value nothing supplies'));
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return problems;
|
|
225
|
+
}
|
|
226
|
+
export function checkTools(ir) {
|
|
227
|
+
const problems = [];
|
|
228
|
+
const declared = new Set(ir.types.map((shape) => shape.id));
|
|
229
|
+
for (const tool of ir.tools) {
|
|
230
|
+
problems.push(...checkToolShape(tool, declared));
|
|
231
|
+
}
|
|
232
|
+
return problems;
|
|
233
|
+
}
|
|
234
|
+
function checkToolShape(tool, declared) {
|
|
235
|
+
const problems = [];
|
|
236
|
+
const at = `tools.${tool.id}`;
|
|
237
|
+
if (tool.handler.kind === 'mcp') {
|
|
238
|
+
problems.push(problem('tool-handler-unsupported', at, 'MCP tool handlers are a Phase 2 concern; no adapter is emitted'));
|
|
239
|
+
}
|
|
240
|
+
if (tool.auth !== undefined) {
|
|
241
|
+
problems.push(problem('tool-auth-unsupported', at, 'the runtime has no authorization hook, so a declared policy would not be enforced'));
|
|
242
|
+
}
|
|
243
|
+
/* `requiresConfirmation` is honoured by the agent loop, and this target emits no agent
|
|
244
|
+
* route: a pipeline step calls a tool directly, with nothing in between to ask a
|
|
245
|
+
* question. Emitting the flag would be a promise no route keeps. */
|
|
246
|
+
if (tool.requiresConfirmation) {
|
|
247
|
+
problems.push(problem('tool-confirmation-unsupported', at, 'this target emits no agent route, and a pipeline step has no confirmation step, ' +
|
|
248
|
+
'so the flag would not be honoured'));
|
|
249
|
+
}
|
|
250
|
+
if (!refRenderable(tool.input, declared) || !refRenderable(tool.output, declared)) {
|
|
251
|
+
problems.push(problem('type-unsupported', at, 'a tool input or output schema is not renderable'));
|
|
252
|
+
}
|
|
253
|
+
if (tool.handler.kind === 'business' && tool.handler.symbol === undefined) {
|
|
254
|
+
problems.push(problem('tool-handler-unsupported', at, 'a business handler needs a symbol to seed'));
|
|
255
|
+
}
|
|
256
|
+
return problems;
|
|
257
|
+
}
|
|
258
|
+
export function checkRetrievers(ir) {
|
|
259
|
+
const problems = [];
|
|
260
|
+
const embeddings = new Set(ir.embeddings.map((shape) => shape.id));
|
|
261
|
+
const stores = new Map(ir.stores.map((shape) => [shape.id, shape]));
|
|
262
|
+
const models = new Map(ir.models.map((shape) => [shape.id, shape]));
|
|
263
|
+
for (const retriever of ir.retrievers) {
|
|
264
|
+
const at = `retrievers.${retriever.id}`;
|
|
265
|
+
const store = stores.get(retriever.store);
|
|
266
|
+
if (store === undefined) {
|
|
267
|
+
problems.push(problem('retriever-store-unsupported', at, `store ${retriever.store} is not declared`));
|
|
268
|
+
}
|
|
269
|
+
else if (store.kind !== 'memory' && store.kind !== 'pgvector') {
|
|
270
|
+
problems.push(problem('retriever-store-unsupported', at, `store kind ${store.kind} has no runtime adapter; memory and pgvector do`));
|
|
271
|
+
}
|
|
272
|
+
if (!embeddings.has(retriever.embedModel)) {
|
|
273
|
+
problems.push(problem('retriever-embedding-unresolved', at, `embedModel ${retriever.embedModel} is not an embeddings entry`));
|
|
274
|
+
}
|
|
275
|
+
if (retriever.source.kind !== 'file') {
|
|
276
|
+
problems.push(problem('retriever-source-unsupported', at, `source kind ${retriever.source.kind} is not wired; the runtime ships a file connector only`));
|
|
277
|
+
}
|
|
278
|
+
if (retriever.filters !== undefined && retriever.filters.length > 0) {
|
|
279
|
+
problems.push(problem('retriever-filters-unsupported', at, 'filters would have to be pushed into every store adapter, which spec v0.1 does not do'));
|
|
280
|
+
}
|
|
281
|
+
if (retriever.rerank !== undefined) {
|
|
282
|
+
const model = models.get(retriever.rerank.model);
|
|
283
|
+
if (model === undefined) {
|
|
284
|
+
problems.push(problem('reference-unresolved', `${at}.rerank`, `rerank model ${retriever.rerank.model} is not declared`));
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return problems;
|
|
289
|
+
}
|
|
290
|
+
export function checkPipelines(ir) {
|
|
291
|
+
const problems = [];
|
|
292
|
+
const declarations = {
|
|
293
|
+
tools: new Set(ir.tools.map((tool) => tool.id)),
|
|
294
|
+
prompts: new Map(ir.prompts.map((prompt) => [prompt.id, prompt])),
|
|
295
|
+
retrievers: new Set(ir.retrievers.map((retriever) => retriever.id)),
|
|
296
|
+
guardrails: new Map(ir.guardrails.map((guardrail) => [guardrail.id, guardrail])),
|
|
297
|
+
agents: new Set(ir.agents.map((agent) => agent.id)),
|
|
298
|
+
};
|
|
299
|
+
for (const pipeline of ir.pipelines) {
|
|
300
|
+
problems.push(...checkPipelineShape(pipeline, declarations));
|
|
301
|
+
}
|
|
302
|
+
return problems;
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* One reason for every kind this target cannot emit, so the gap is named in one place.
|
|
306
|
+
*
|
|
307
|
+
* Exported because the emitter raises the identical diagnostic when it is reached without
|
|
308
|
+
* the validator having run -- see D-4. The message names the target rather than a layer,
|
|
309
|
+
* so a CLI can print it without the reader knowing the roadmap.
|
|
310
|
+
*/
|
|
311
|
+
export function stepUnsupported(kind, at) {
|
|
312
|
+
return problem('step-unsupported', at, `the Next.js target has no emitter for step kind ${kind}; remove the step or generate the Python target`);
|
|
313
|
+
}
|
|
314
|
+
function checkPipelineShape(pipeline, declarations) {
|
|
315
|
+
const problems = [];
|
|
316
|
+
const at = `pipelines.${pipeline.id}`;
|
|
317
|
+
if (pipeline.trigger.kind !== 'http') {
|
|
318
|
+
problems.push(problem('trigger-unsupported', `${at}.trigger`, `trigger kind ${pipeline.trigger.kind} has no Next.js host; only an http trigger is served`));
|
|
319
|
+
}
|
|
320
|
+
problems.push(...checkSteps(pipeline.steps, `${at}.steps`, declarations, new Set()));
|
|
321
|
+
return problems;
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* One step list, checked end to end, recursing into the composites.
|
|
325
|
+
*
|
|
326
|
+
* `at` is the path of the list (not of the step), so a nested list extends the same shape
|
|
327
|
+
* the emitter uses: `pipelines.<id>.steps.2.then.0` names the first step of the third
|
|
328
|
+
* step's `then` arm both here and in the emitted diagnostic.
|
|
329
|
+
*/
|
|
330
|
+
function checkSteps(steps, at, declarations, bound) {
|
|
331
|
+
const problems = [];
|
|
332
|
+
const bind = (name, stepAt) => {
|
|
333
|
+
if (RESERVED_BINDINGS.includes(name)) {
|
|
334
|
+
problems.push(problem('reference-unresolved', stepAt, `binding ${name} collides with a runtime name`));
|
|
335
|
+
}
|
|
336
|
+
if (bound.has(name)) {
|
|
337
|
+
problems.push(problem('reference-unresolved', stepAt, `binding ${name} is already bound`));
|
|
338
|
+
}
|
|
339
|
+
bound.add(name);
|
|
340
|
+
};
|
|
341
|
+
for (const [index, step] of steps.entries()) {
|
|
342
|
+
const stepAt = `${at}.${index}`;
|
|
343
|
+
if (isCompositeStep(step)) {
|
|
344
|
+
problems.push(...checkComposite(step, stepAt, declarations, bound));
|
|
345
|
+
// `map` and `reduce` bind their result; `branch` and `parallel` bind only inside an
|
|
346
|
+
// arm, and which arm ran is not knowable at generation time, so a later step may
|
|
347
|
+
// not read those. Not adding them is what makes that a refusal rather than a
|
|
348
|
+
// reference the emitted route resolves to `undefined`.
|
|
349
|
+
if (step.kind === 'map' || step.kind === 'reduce')
|
|
350
|
+
bind(step.as, stepAt);
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
// `rerank`, `guard` and `agent` are leaves with their own emitters. Each is checked
|
|
354
|
+
// against what its emitted call actually does rather than refused wholesale, because
|
|
355
|
+
// a refusal here would now be a refusal of something the runtime can run.
|
|
356
|
+
if (step.kind === 'rerank') {
|
|
357
|
+
problems.push(...checkRerankStep(step, stepAt, bound));
|
|
358
|
+
bind(step.as, stepAt);
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
if (step.kind === 'guard') {
|
|
362
|
+
problems.push(...checkGuardStep(step, stepAt, declarations, bound));
|
|
363
|
+
// A `guard` binds nothing -- there is no `as` -- so only the shapes above bind.
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
if (step.kind === 'agent') {
|
|
367
|
+
problems.push(...checkAgentStep(step, stepAt, declarations, bound));
|
|
368
|
+
bind(step.as, stepAt);
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
problems.push(...checkStepShape(step, stepAt, declarations, bound));
|
|
372
|
+
if (step.kind !== 'emit')
|
|
373
|
+
bind(step.as, stepAt);
|
|
374
|
+
}
|
|
375
|
+
return problems;
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* A composite's arms, each checked against the bindings that exist *before* the composite.
|
|
379
|
+
*
|
|
380
|
+
* The fresh `Set` per arm is what keeps a `branch` from letting its `then` arm see what its
|
|
381
|
+
* `else` arm bound: at run time exactly one of them executes, so a spec that depends on
|
|
382
|
+
* both is a bug the generator can see and the runtime could not.
|
|
383
|
+
*/
|
|
384
|
+
function checkComposite(step, at, declarations, bound) {
|
|
385
|
+
const problems = [];
|
|
386
|
+
if (step.kind === 'branch') {
|
|
387
|
+
problems.push(...checkSteps(step.then, `${at}.then`, declarations, new Set(bound)));
|
|
388
|
+
problems.push(...checkSteps(step.else, `${at}.else`, declarations, new Set(bound)));
|
|
389
|
+
}
|
|
390
|
+
else if (step.kind === 'parallel') {
|
|
391
|
+
for (const [arm, steps] of step.branches.entries()) {
|
|
392
|
+
problems.push(...checkSteps(steps, `${at}.arm.${arm}`, declarations, new Set(bound)));
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
else if (step.kind === 'map') {
|
|
396
|
+
problems.push(...checkSteps([step.step], `${at}.step`, declarations, new Set(bound)));
|
|
397
|
+
}
|
|
398
|
+
else {
|
|
399
|
+
const prompt = declarations.prompts.get(step.prompt);
|
|
400
|
+
if (prompt !== undefined && prompt.variables.length > 1) {
|
|
401
|
+
problems.push(problem('step-unsupported', at, `a reduce prompt takes exactly one variable -- the iteration item -- but ${step.prompt} declares ${prompt.variables.length}`));
|
|
402
|
+
}
|
|
403
|
+
problems.push(...checkSteps([reduceBody(step)], `${at}.item`, declarations, new Set(bound)));
|
|
404
|
+
}
|
|
405
|
+
return problems;
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* The `generate` a `reduce` body is checked as, mirrored from the emitter.
|
|
409
|
+
*
|
|
410
|
+
* Duplicated rather than shared for the same reason `retrievalEvalHasRetriever` is: the
|
|
411
|
+
* validator has to refuse what the emitter could only render wrongly, so it cannot call
|
|
412
|
+
* the code it is guarding. A drift here shows up as a `reduce` whose body prompt is
|
|
413
|
+
* missing a variable, which is refused rather than emitted with an unbound input.
|
|
414
|
+
*/
|
|
415
|
+
function reduceBody(step) {
|
|
416
|
+
return {
|
|
417
|
+
kind: 'generate',
|
|
418
|
+
prompt: step.prompt,
|
|
419
|
+
model: step.model,
|
|
420
|
+
as: 'reduced',
|
|
421
|
+
input: { item: { kind: 'reference', path: ['item'] } },
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
function checkStepShape(step, at, declarations, bound) {
|
|
425
|
+
const { tools, prompts, retrievers } = declarations;
|
|
426
|
+
const problems = [];
|
|
427
|
+
if (step.kind === 'tool') {
|
|
428
|
+
if (!tools.has(step.tool)) {
|
|
429
|
+
problems.push(problem('reference-unresolved', at, `tool ${step.tool} is not declared`));
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
if (step.kind === 'retrieve' && !retrievers.has(step.retriever)) {
|
|
433
|
+
problems.push(problem('reference-unresolved', at, `retriever ${step.retriever} is not declared`));
|
|
434
|
+
}
|
|
435
|
+
if (step.kind === 'generate') {
|
|
436
|
+
const prompt = prompts.get(step.prompt);
|
|
437
|
+
if (prompt === undefined) {
|
|
438
|
+
problems.push(problem('reference-unresolved', at, `prompt ${step.prompt} is not declared`));
|
|
439
|
+
}
|
|
440
|
+
else if (prompt.output.kind !== 'structured') {
|
|
441
|
+
problems.push(problem('step-unsupported', at, `prompt ${step.prompt} emits text, but this target emits structured answers only`));
|
|
442
|
+
}
|
|
443
|
+
if (step.context !== undefined && !bound.has(step.context)) {
|
|
444
|
+
problems.push(problem('reference-unresolved', at, `context binding ${step.context} is not bound at this step`));
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
if (step.kind === 'emit') {
|
|
448
|
+
// The terminal `emit` names the event the pipeline hands back. There is no event bus
|
|
449
|
+
// in Phase 1, and the route reads the bound value directly, so the step needs no
|
|
450
|
+
// emitted binding -- but it is part of the spec language and must not be refused.
|
|
451
|
+
return problems;
|
|
452
|
+
}
|
|
453
|
+
return problems;
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* The placeholders in a rendered template that name nothing the step can read.
|
|
457
|
+
*
|
|
458
|
+
* `event` is the one name a step may read without binding it: the route seeds the request
|
|
459
|
+
* into the pipeline context, which is how a `generate` step already reads it.
|
|
460
|
+
*/
|
|
461
|
+
function unboundNames(template, bound) {
|
|
462
|
+
// A placeholder is `root.member...` and the emitted code reads it as
|
|
463
|
+
// `context.require('root').member`, so the root is what must be bound. Comparing the
|
|
464
|
+
// whole dotted name would refuse `{{ handbook.context }}` -- a property of a binding
|
|
465
|
+
// that *is* bound -- and would also miss `event`, which no step binds.
|
|
466
|
+
const roots = templatePlaceholders(template).map((name) => name.split('.')[0] ?? name);
|
|
467
|
+
return [...new Set(roots)].filter((name) => name !== 'event' && !bound.has(name));
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* A `rerank` step, checked against the one reranker the runtime implements.
|
|
471
|
+
*
|
|
472
|
+
* `rerankerFor` resolves an alias by looking for `lexical` in the model name and throws
|
|
473
|
+
* `FeatureDeferredError` for anything else, so a spec naming a cross-encoder would emit an
|
|
474
|
+
* app that fails on its first request. The plan refuses it with the same reason.
|
|
475
|
+
*/
|
|
476
|
+
function checkRerankStep(step, at, bound) {
|
|
477
|
+
const problems = [];
|
|
478
|
+
if (!bound.has(step.over)) {
|
|
479
|
+
problems.push(problem('reference-unresolved', at, `rerank reads binding ${step.over}, which is not bound at this step`));
|
|
480
|
+
}
|
|
481
|
+
for (const name of unboundNames(step.query, bound)) {
|
|
482
|
+
problems.push(problem('reference-unresolved', `${at}.query`, `binding ${name} is not bound at this step`));
|
|
483
|
+
}
|
|
484
|
+
if (!step.model.toLowerCase().includes('lexical')) {
|
|
485
|
+
problems.push(problem('step-unsupported', at, `the runtime resolves only a lexical reranker, so model ${step.model} would throw FeatureDeferredError on the first request; name a lexical model`));
|
|
486
|
+
}
|
|
487
|
+
if (!Number.isInteger(step.topN) || step.topN < 1) {
|
|
488
|
+
problems.push(problem('step-invalid', at, `topN must be a positive integer, got ${step.topN}`));
|
|
489
|
+
}
|
|
490
|
+
return problems;
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* A `guard` step: the guardrail it names, the stage it runs at, and its operand.
|
|
494
|
+
*
|
|
495
|
+
* The stage is the load-bearing part. `input` and `output` take a rendered payload and
|
|
496
|
+
* `retrieval` takes one binding's passages -- exactly what the emitted call passes to
|
|
497
|
+
* `checkRetrieval`. `tool-call` is refused, because the only thing that can hand the engine
|
|
498
|
+
* a call's arguments is the call site: the emitted `tool` step checks its own call inline,
|
|
499
|
+
* and a `guard` step standing beside it has no call to inspect.
|
|
500
|
+
*
|
|
501
|
+
* `retrieval` additionally requires the payload to name exactly one binding, because that
|
|
502
|
+
* binding is the result the step guards; naming two would silently guard the first.
|
|
503
|
+
*/
|
|
504
|
+
function checkGuardStep(step, at, declarations, bound) {
|
|
505
|
+
const problems = [];
|
|
506
|
+
const guardrail = declarations.guardrails.get(step.guardrail);
|
|
507
|
+
if (guardrail === undefined) {
|
|
508
|
+
problems.push(problem('reference-unresolved', at, `guardrail ${step.guardrail} is not declared`));
|
|
509
|
+
}
|
|
510
|
+
else if (!guardrail.stages.includes(step.stage)) {
|
|
511
|
+
problems.push(problem('step-invalid', at, `guardrail ${step.guardrail} does not declare the ${step.stage} stage (declared: ${guardrail.stages.join(', ')}), so this step would enforce nothing`));
|
|
512
|
+
}
|
|
513
|
+
if (step.stage === 'tool-call') {
|
|
514
|
+
problems.push(problem('step-unsupported', at, 'a guard step cannot check the tool-call stage: a call’s arguments exist only at the call site, so the emitted tool step checks its own call'));
|
|
515
|
+
return problems;
|
|
516
|
+
}
|
|
517
|
+
const names = unboundNames(step.text, bound);
|
|
518
|
+
if (step.stage === 'retrieval') {
|
|
519
|
+
const referenced = [
|
|
520
|
+
...new Set(templatePlaceholders(step.text).map((name) => name.split('.')[0] ?? name)),
|
|
521
|
+
];
|
|
522
|
+
if (referenced.length !== 1) {
|
|
523
|
+
problems.push(problem('step-invalid', `${at}.text`, `a retrieval guard inspects exactly one retrieval binding, but this text names ${referenced.length}`));
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
for (const name of names) {
|
|
527
|
+
problems.push(problem('reference-unresolved', `${at}.text`, `binding ${name} is not bound at this step`));
|
|
528
|
+
}
|
|
529
|
+
return problems;
|
|
530
|
+
}
|
|
531
|
+
/** An `agent` step: the agent it names, and the bindings its text reads. */
|
|
532
|
+
function checkAgentStep(step, at, declarations, bound) {
|
|
533
|
+
const problems = [];
|
|
534
|
+
if (!declarations.agents.has(step.agent)) {
|
|
535
|
+
problems.push(problem('reference-unresolved', at, `agent ${step.agent} is not declared`));
|
|
536
|
+
}
|
|
537
|
+
for (const name of unboundNames(step.text, bound)) {
|
|
538
|
+
problems.push(problem('reference-unresolved', `${at}.text`, `binding ${name} is not bound at this step`));
|
|
539
|
+
}
|
|
540
|
+
return problems;
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* The retriever a retrieval eval samples, resolved the same way the emitter resolves it.
|
|
544
|
+
*
|
|
545
|
+
* Duplicated on purpose: the validator must refuse a plan the emitter could only render
|
|
546
|
+
* as an empty id, so it cannot delegate the question to the code it is guarding.
|
|
547
|
+
*/
|
|
548
|
+
function retrievalEvalHasRetriever(ir, evaluation) {
|
|
549
|
+
if (evaluation.target.kind === 'pipeline') {
|
|
550
|
+
const pipeline = ir.pipelines.find((candidate) => candidate.id === evaluation.target.id);
|
|
551
|
+
if (pipeline?.steps.some((step) => step.kind === 'retrieve'))
|
|
552
|
+
return true;
|
|
553
|
+
}
|
|
554
|
+
return ir.retrievers.length === 1;
|
|
555
|
+
}
|
|
556
|
+
export function checkEvals(ir) {
|
|
557
|
+
const problems = [];
|
|
558
|
+
const pipelines = new Set(ir.pipelines.map((pipeline) => pipeline.id));
|
|
559
|
+
const prompts = new Set(ir.prompts.map((prompt) => prompt.id));
|
|
560
|
+
for (const evaluation of ir.evals) {
|
|
561
|
+
const at = `evals.${evaluation.id}`;
|
|
562
|
+
if (evaluation.target.kind === 'pipeline' && !pipelines.has(evaluation.target.id)) {
|
|
563
|
+
problems.push(problem('eval-target-unresolved', at, `pipeline ${evaluation.target.id} is not declared`));
|
|
564
|
+
}
|
|
565
|
+
if (evaluation.target.kind === 'prompt' && !prompts.has(evaluation.target.id)) {
|
|
566
|
+
problems.push(problem('eval-target-unresolved', at, `prompt ${evaluation.target.id} is not declared`));
|
|
567
|
+
}
|
|
568
|
+
if (evaluation.dataset.source === 'synthetic') {
|
|
569
|
+
problems.push(problem('eval-dataset-unsupported', at, 'a synthetic dataset has no file to load'));
|
|
570
|
+
}
|
|
571
|
+
for (const metric of evaluation.metrics) {
|
|
572
|
+
if (metric.kind === 'retrieval' && !retrievalEvalHasRetriever(ir, evaluation)) {
|
|
573
|
+
problems.push(problem('eval-target-unresolved', at, 'a retrieval eval needs a retriever: name one through a pipeline target or declare exactly one'));
|
|
574
|
+
}
|
|
575
|
+
if (!SUPPORTED_METRICS.has(metric.kind)) {
|
|
576
|
+
problems.push(problem('eval-metric-unsupported', at, `metric ${metric.kind} has no emitted binding; supported: ${[...SUPPORTED_METRICS].join(', ')}`));
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
return problems;
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* The guardrail declarations this target cannot honour.
|
|
584
|
+
*
|
|
585
|
+
* The IR layer has already refused an unknown policy, an empty stage list and a `tools`
|
|
586
|
+
* entry that names nothing, so what is left is the part only a *target* knows: which
|
|
587
|
+
* policies its engine can evaluate, which actions its routes can carry out, and which
|
|
588
|
+
* fields its stages actually see.
|
|
589
|
+
*/
|
|
590
|
+
export function checkGuardrails(ir) {
|
|
591
|
+
const problems = [];
|
|
592
|
+
for (const guardrail of ir.guardrails) {
|
|
593
|
+
const at = `guardrails.${guardrail.id}`;
|
|
594
|
+
for (const policy of guardrail.policies) {
|
|
595
|
+
if (IMPLEMENTED_POLICIES.has(policy))
|
|
596
|
+
continue;
|
|
597
|
+
problems.push(problem('guardrail-unsupported', `${at}.policies`, `the TypeScript guardrail engine implements ${[...IMPLEMENTED_POLICIES].join(', ')} and would raise ConfigurationError on '${policy}'; use an implemented policy`));
|
|
598
|
+
}
|
|
599
|
+
if (guardrail.action === 'escalate') {
|
|
600
|
+
// Escalation is a question put to a human, answered by resuming a paused run. The
|
|
601
|
+
// emitted app has no resume route (see the agents module), so an escalated request
|
|
602
|
+
// would stop with nobody to answer -- which is a block wearing a different name.
|
|
603
|
+
problems.push(problem('guardrail-unsupported', `${at}.action`, "escalation needs a resume route, and this target emits none; use action 'block' to refuse the request or 'flag' to record it and continue"));
|
|
604
|
+
}
|
|
605
|
+
if (guardrail.action !== 'redact')
|
|
606
|
+
continue;
|
|
607
|
+
for (const policy of guardrail.policies) {
|
|
608
|
+
if (!IMPLEMENTED_POLICIES.has(policy) || REDACTABLE_POLICIES.has(policy))
|
|
609
|
+
continue;
|
|
610
|
+
problems.push(problem('guardrail-unsupported', `${at}.policies`, `'${guardrail.id}' redacts, but policy '${policy}' detects intent rather than a maskable value, so the runtime raises instead of returning a cleaned payload; redact with ${[...REDACTABLE_POLICIES].join(', ')}, or use action 'block'`));
|
|
611
|
+
}
|
|
612
|
+
if (guardrail.stages.includes('tool-call')) {
|
|
613
|
+
// The emitted tool step checks its call and lets the verdict stand; it does not
|
|
614
|
+
// re-bind the arguments, so a redaction here would be recorded and then ignored.
|
|
615
|
+
problems.push(problem('guardrail-unsupported', `${at}.stages`, "a redact guardrail at the tool-call stage would be checked but never applied -- the emitted tool step does not re-bind a call's arguments; use action 'block', or redact at input, output or retrieval"));
|
|
616
|
+
}
|
|
617
|
+
for (const name of guardrail.redact) {
|
|
618
|
+
if (name === 'text')
|
|
619
|
+
continue;
|
|
620
|
+
problems.push(problem('guardrail-unsupported', `${at}.redact`, `'${guardrail.id}' redacts field '${name}', but the input, output and retrieval payloads this target builds are exactly { text }, which is the only name that has anything to mask`));
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
return problems;
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* The agent declarations this target cannot honour.
|
|
627
|
+
*
|
|
628
|
+
* The emitted `agents.ts` turns the whole collection into one `AgentRuntime` document, so
|
|
629
|
+
* every agent a pipeline step can name has to be one the runtime can build. Most of these
|
|
630
|
+
* are the runtime's own `fromIr` refusals moved to plan time; the rest are the two things
|
|
631
|
+
* an emitted app cannot offer an agent -- a pause a route could resume, and a harness
|
|
632
|
+
* dependency it does not install.
|
|
633
|
+
*/
|
|
634
|
+
export function checkAgents(ir) {
|
|
635
|
+
const problems = [];
|
|
636
|
+
const promptById = new Map(ir.prompts.map((prompt) => [prompt.id, prompt]));
|
|
637
|
+
const toolIds = new Set(ir.tools.map((tool) => tool.id));
|
|
638
|
+
const memoryIds = new Set(ir.memory.map((memory) => memory.id));
|
|
639
|
+
const guardrailIds = new Set(ir.guardrails.map((guardrail) => guardrail.id));
|
|
640
|
+
const modelIds = new Set(ir.models.map((model) => model.id));
|
|
641
|
+
for (const agent of ir.agents) {
|
|
642
|
+
const at = `agents.${agent.id}`;
|
|
643
|
+
// `AgentDefinition` has no retrievers: the loop builds its messages from the resolved
|
|
644
|
+
// prompt and `memory` and never queries one, so the declaration would be dropped.
|
|
645
|
+
if (agent.retrievers.length > 0) {
|
|
646
|
+
problems.push(problem('agent-unsupported', `${at}.retrievers`, `the agent loop has no retrieval step, so ${agent.retrievers.join(', ')} would never run; inline the retrieved context in the prompt instead`));
|
|
647
|
+
}
|
|
648
|
+
// The loop returns the model's text as `answer` and validates no schema, so a declared
|
|
649
|
+
// structured output would be a promise nothing keeps.
|
|
650
|
+
if (agent.structuredOutput !== undefined) {
|
|
651
|
+
problems.push(problem('agent-unsupported', `${at}.structuredOutput`, "the agent loop performs no structured-output validation, so this declaration would not be enforced; declare the schema on the agent's prompt"));
|
|
652
|
+
}
|
|
653
|
+
// The instructions prompt becomes the system message through its *template*, verbatim:
|
|
654
|
+
// `fromIr` reads it and the loop never renders variables, so a prompt with variables
|
|
655
|
+
// would reach the model as unrendered `{{name}}` text.
|
|
656
|
+
const instructions = promptById.get(agent.instructions);
|
|
657
|
+
if (instructions === undefined) {
|
|
658
|
+
problems.push(problem('reference-unresolved', `${at}.instructions`, `prompt ${agent.instructions} is not declared`));
|
|
659
|
+
}
|
|
660
|
+
else if (instructions.variables.length > 0) {
|
|
661
|
+
problems.push(problem('agent-unsupported', `${at}.instructions`, `prompt ${instructions.id} declares variables (${instructions.variables
|
|
662
|
+
.map((variable) => variable.name)
|
|
663
|
+
.join(', ')}) that the agent loop never renders, so the model would receive the template verbatim; give the agent a variable-free prompt`));
|
|
664
|
+
}
|
|
665
|
+
for (const toolId of agent.tools) {
|
|
666
|
+
if (!toolIds.has(toolId)) {
|
|
667
|
+
problems.push(problem('reference-unresolved', `${at}.tools`, `tool ${toolId} is not declared`));
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
for (const memoryId of agent.memory) {
|
|
671
|
+
if (!memoryIds.has(memoryId)) {
|
|
672
|
+
problems.push(problem('reference-unresolved', `${at}.memory`, `memory ${memoryId} is not declared`));
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
for (const guardrailId of agent.guardrails) {
|
|
676
|
+
if (!guardrailIds.has(guardrailId)) {
|
|
677
|
+
problems.push(problem('reference-unresolved', `${at}.guardrails`, `guardrail ${guardrailId} is not declared`));
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
if (!modelIds.has(agent.model)) {
|
|
681
|
+
problems.push(problem('reference-unresolved', `${at}.model`, `model ${agent.model} is not declared`));
|
|
682
|
+
}
|
|
683
|
+
// The runtime refuses every other engine, so naming one here would make the module's
|
|
684
|
+
// first `run` throw. `native` is the one engine whose harness this app installs.
|
|
685
|
+
if (agent.engine !== 'native') {
|
|
686
|
+
problems.push(problem('agent-unsupported', `${at}.engine`, `the emitted app installs no '${agent.engine}' harness, so this engine would raise on the first run; use engine 'native'`));
|
|
687
|
+
}
|
|
688
|
+
// A pause is a question put to a human, answered by resuming the run. Nothing in an
|
|
689
|
+
// emitted app can resume one: there is no `/resume` route, and the pipeline step that
|
|
690
|
+
// runs an agent raises rather than binding a paused outcome.
|
|
691
|
+
if (agent.humanInTheLoop !== undefined) {
|
|
692
|
+
problems.push(problem('agent-unsupported', `${at}.humanInTheLoop`, 'a paused run needs a resume route, and this target emits none; remove the human-in-the-loop gate or run the agent outside the pipeline'));
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
// A model the emitted document reaches must be priced: `modelDefinition` throws on an
|
|
696
|
+
// unpriced model because no budget can be enforced against one, and it is reached for
|
|
697
|
+
// exactly the agents' own models plus their fallback closure -- the same set emitted.
|
|
698
|
+
const models = new Map(ir.models.map((model) => [model.id, model]));
|
|
699
|
+
for (const alias of reachableAliases(ir)) {
|
|
700
|
+
const model = models.get(alias);
|
|
701
|
+
if (model === undefined || model.costProfile !== undefined)
|
|
702
|
+
continue;
|
|
703
|
+
problems.push(problem('agent-unsupported', `models.${model.id}.costProfile`, `model ${model.id} resolves no cost profile, and the agent loop refuses an unpriced model because no budget can be enforced against one; this model is an agent's own or reached through its fallbacks, so the emitted document needs it -- use a catalogued modelId, or drop the fallback that reaches this one`));
|
|
704
|
+
}
|
|
705
|
+
return problems;
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* Every model alias the emitted agent document resolves, following fallbacks.
|
|
709
|
+
*
|
|
710
|
+
* Mirrors the runtime's `reachableModels`: a fallback's own fallbacks are reachable too, so
|
|
711
|
+
* a walk that stopped at one hop would miss the model the runtime actually builds.
|
|
712
|
+
*/
|
|
713
|
+
function reachableAliases(ir) {
|
|
714
|
+
const fallbacks = new Map(ir.models.map((model) => [model.id, model.fallbacks]));
|
|
715
|
+
const reachable = new Set();
|
|
716
|
+
const queue = ir.agents.map((agent) => agent.model);
|
|
717
|
+
while (queue.length > 0) {
|
|
718
|
+
const alias = queue.pop();
|
|
719
|
+
if (reachable.has(alias))
|
|
720
|
+
continue;
|
|
721
|
+
reachable.add(alias);
|
|
722
|
+
queue.push(...(fallbacks.get(alias) ?? []));
|
|
723
|
+
}
|
|
724
|
+
return reachable;
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* The memory declarations this target cannot honour, refused rather than approximated.
|
|
728
|
+
*
|
|
729
|
+
* This mirrors the runtime's `checkMemorySupported` plus the two fields its
|
|
730
|
+
* `memoryDefinition` and `#storeFor` refuse. The runtime raises on all of them, so a plan
|
|
731
|
+
* that emitted one would produce an app that fails on the first request; catching them here
|
|
732
|
+
* turns that into a generation failure that names the declaration.
|
|
733
|
+
*/
|
|
734
|
+
export function checkMemory(ir) {
|
|
735
|
+
const problems = [];
|
|
736
|
+
for (const memory of ir.memory) {
|
|
737
|
+
const at = `memory.${memory.id}`;
|
|
738
|
+
if (memory.kind !== 'buffer' && memory.kind !== 'window') {
|
|
739
|
+
problems.push(problem('memory-unsupported', `${at}.kind`, `the runtime implements buffer and window memory and refuses '${memory.kind}' by name; serve it as a buffer, or declare the kind once the runtime grows a summarizer or an index`));
|
|
740
|
+
}
|
|
741
|
+
if (memory.summarizeModel !== undefined) {
|
|
742
|
+
problems.push(problem('memory-unsupported', `${at}.summarizeModel`, 'no summarizer is implemented, so this declaration would be ignored'));
|
|
743
|
+
}
|
|
744
|
+
if (memory.ttl !== undefined) {
|
|
745
|
+
problems.push(problem('memory-unsupported', `${at}.ttl`, 'expiry enforcement is not implemented, so entries would never expire'));
|
|
746
|
+
}
|
|
747
|
+
if (memory.redact.length > 0) {
|
|
748
|
+
problems.push(problem('memory-unsupported', `${at}.redact`, 'memory redaction is not implemented, so the declared fields would still be stored and replayed'));
|
|
749
|
+
}
|
|
750
|
+
if (memory.backend !== undefined && memory.backend !== 'in-process') {
|
|
751
|
+
problems.push(problem('memory-unsupported', `${at}.backend`, `the emitted app registers no MemoryStore, so backend '${memory.backend}' would raise ConfigurationError on the first write; omit 'backend', or set it to 'in-process'`));
|
|
752
|
+
}
|
|
753
|
+
if (memory.store !== undefined) {
|
|
754
|
+
problems.push(problem('memory-unsupported', `${at}.store`, "the runtime selects its store from 'backend' and never reads 'store'; set 'backend' instead"));
|
|
755
|
+
}
|
|
756
|
+
if (!Number.isInteger(memory.tokenBudget) || memory.tokenBudget < 1) {
|
|
757
|
+
problems.push(problem('memory-unsupported', `${at}.tokenBudget`, `the runtime needs a positive integer token budget, got ${memory.tokenBudget}`));
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
return problems;
|
|
761
|
+
}
|
|
762
|
+
/** Every check, in a fixed order so a failing run reports the same problems twice. */
|
|
763
|
+
export function checkPlan(ir) {
|
|
764
|
+
return [
|
|
765
|
+
...checkIrVersion(ir),
|
|
766
|
+
...checkTarget(ir),
|
|
767
|
+
...checkDeployments(ir),
|
|
768
|
+
...checkObservability(ir),
|
|
769
|
+
...checkRuntime(ir),
|
|
770
|
+
...checkTypes(ir),
|
|
771
|
+
...checkModels(ir),
|
|
772
|
+
...checkPrompts(ir),
|
|
773
|
+
...checkTools(ir),
|
|
774
|
+
...checkGuardrails(ir),
|
|
775
|
+
...checkAgents(ir),
|
|
776
|
+
...checkMemory(ir),
|
|
777
|
+
...checkRetrievers(ir),
|
|
778
|
+
...checkPipelines(ir),
|
|
779
|
+
...checkEvals(ir),
|
|
780
|
+
];
|
|
781
|
+
}
|
|
782
|
+
/**
|
|
783
|
+
* The capabilities the IR actually requires, by name. Kept here because the generator
|
|
784
|
+
* is the side that knows what it will honour; the CLI's gate is fed the manifest's
|
|
785
|
+
* declaration and the two are compared in a test.
|
|
786
|
+
*/
|
|
787
|
+
export function declaredCapabilities() {
|
|
788
|
+
return TS_VERCEL_MANIFEST.capabilities.filter((capability) => isRequiredCapability(capability));
|
|
789
|
+
}
|
|
790
|
+
//# sourceMappingURL=validate.js.map
|