@aid-framework/ir 0.0.0-stage → 0.1.39
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 +134 -2
- package/dist/.tsbuildinfo +1 -0
- package/dist/build.d.ts +74 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +1079 -0
- package/dist/build.js.map +1 -0
- package/dist/capabilities.d.ts +65 -0
- package/dist/capabilities.d.ts.map +1 -0
- package/dist/capabilities.js +176 -0
- package/dist/capabilities.js.map +1 -0
- package/dist/catalog/index.d.ts +36 -0
- package/dist/catalog/index.d.ts.map +1 -0
- package/dist/catalog/index.js +43 -0
- package/dist/catalog/index.js.map +1 -0
- package/dist/catalog/models.json +52 -0
- package/dist/diagnostics.d.ts +90 -0
- package/dist/diagnostics.d.ts.map +1 -0
- package/dist/diagnostics.js +103 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/gates/agents.d.ts +34 -0
- package/dist/gates/agents.d.ts.map +1 -0
- package/dist/gates/agents.js +233 -0
- package/dist/gates/agents.js.map +1 -0
- package/dist/gates/capability.d.ts +15 -0
- package/dist/gates/capability.d.ts.map +1 -0
- package/dist/gates/capability.js +81 -0
- package/dist/gates/capability.js.map +1 -0
- package/dist/gates/contract.d.ts +35 -0
- package/dist/gates/contract.d.ts.map +1 -0
- package/dist/gates/contract.js +19 -0
- package/dist/gates/contract.js.map +1 -0
- package/dist/gates/cost.d.ts +40 -0
- package/dist/gates/cost.d.ts.map +1 -0
- package/dist/gates/cost.js +167 -0
- package/dist/gates/cost.js.map +1 -0
- package/dist/gates/deploy.d.ts +23 -0
- package/dist/gates/deploy.d.ts.map +1 -0
- package/dist/gates/deploy.js +44 -0
- package/dist/gates/deploy.js.map +1 -0
- package/dist/gates/eval-coverage.d.ts +11 -0
- package/dist/gates/eval-coverage.d.ts.map +1 -0
- package/dist/gates/eval-coverage.js +50 -0
- package/dist/gates/eval-coverage.js.map +1 -0
- package/dist/gates/guardrails.d.ts +24 -0
- package/dist/gates/guardrails.d.ts.map +1 -0
- package/dist/gates/guardrails.js +112 -0
- package/dist/gates/guardrails.js.map +1 -0
- package/dist/gates/hitl.d.ts +17 -0
- package/dist/gates/hitl.d.ts.map +1 -0
- package/dist/gates/hitl.js +40 -0
- package/dist/gates/hitl.js.map +1 -0
- package/dist/gates/index.d.ts +66 -0
- package/dist/gates/index.d.ts.map +1 -0
- package/dist/gates/index.js +77 -0
- package/dist/gates/index.js.map +1 -0
- package/dist/gates/memory.d.ts +13 -0
- package/dist/gates/memory.d.ts.map +1 -0
- package/dist/gates/memory.js +67 -0
- package/dist/gates/memory.js.map +1 -0
- package/dist/gates/normalize.d.ts +26 -0
- package/dist/gates/normalize.d.ts.map +1 -0
- package/dist/gates/normalize.js +228 -0
- package/dist/gates/normalize.js.map +1 -0
- package/dist/gates/orchestration.d.ts +17 -0
- package/dist/gates/orchestration.d.ts.map +1 -0
- package/dist/gates/orchestration.js +46 -0
- package/dist/gates/orchestration.js.map +1 -0
- package/dist/gates/referential.d.ts +12 -0
- package/dist/gates/referential.d.ts.map +1 -0
- package/dist/gates/referential.js +554 -0
- package/dist/gates/referential.js.map +1 -0
- package/dist/gates/security.d.ts +22 -0
- package/dist/gates/security.d.ts.map +1 -0
- package/dist/gates/security.js +225 -0
- package/dist/gates/security.js.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/policy/ast.d.ts +61 -0
- package/dist/policy/ast.d.ts.map +1 -0
- package/dist/policy/ast.js +48 -0
- package/dist/policy/ast.js.map +1 -0
- package/dist/policy/index.d.ts +7 -0
- package/dist/policy/index.d.ts.map +1 -0
- package/dist/policy/index.js +4 -0
- package/dist/policy/index.js.map +1 -0
- package/dist/policy/lexer.d.ts +27 -0
- package/dist/policy/lexer.d.ts.map +1 -0
- package/dist/policy/lexer.js +147 -0
- package/dist/policy/lexer.js.map +1 -0
- package/dist/policy/parser.d.ts +15 -0
- package/dist/policy/parser.d.ts.map +1 -0
- package/dist/policy/parser.js +202 -0
- package/dist/policy/parser.js.map +1 -0
- package/dist/predicate/ast.d.ts +125 -0
- package/dist/predicate/ast.d.ts.map +1 -0
- package/dist/predicate/ast.js +111 -0
- package/dist/predicate/ast.js.map +1 -0
- package/dist/predicate/index.d.ts +7 -0
- package/dist/predicate/index.d.ts.map +1 -0
- package/dist/predicate/index.js +4 -0
- package/dist/predicate/index.js.map +1 -0
- package/dist/predicate/lexer.d.ts +30 -0
- package/dist/predicate/lexer.d.ts.map +1 -0
- package/dist/predicate/lexer.js +176 -0
- package/dist/predicate/lexer.js.map +1 -0
- package/dist/predicate/parser.d.ts +16 -0
- package/dist/predicate/parser.d.ts.map +1 -0
- package/dist/predicate/parser.js +317 -0
- package/dist/predicate/parser.js.map +1 -0
- package/dist/schema/ir.schema.json +1144 -0
- package/dist/serialize.d.ts +15 -0
- package/dist/serialize.d.ts.map +1 -0
- package/dist/serialize.js +53 -0
- package/dist/serialize.js.map +1 -0
- package/dist/shapes.d.ts +671 -0
- package/dist/shapes.d.ts.map +1 -0
- package/dist/shapes.js +159 -0
- package/dist/shapes.js.map +1 -0
- package/package.json +41 -3
package/dist/build.js
ADDED
|
@@ -0,0 +1,1079 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The builder: a validated spec document value -> IR v0.1.
|
|
3
|
+
*
|
|
4
|
+
* The builder never throws for a user-recoverable condition. It applies Phase 0
|
|
5
|
+
* defaults (so generated code never supplies a default of its own), sorts every
|
|
6
|
+
* object-derived collection by stable id (so YAML key order cannot change the IR),
|
|
7
|
+
* and reports `ir/*` diagnostics through the same shape every other gate uses.
|
|
8
|
+
*/
|
|
9
|
+
import { SPEC_VERSION } from '@aid-framework/spec';
|
|
10
|
+
import { deriveRequiredCapabilities, isModelCapability, SPEC_MODEL_CAPABILITY_MAP, } from './capabilities.js';
|
|
11
|
+
import { catalogCostProfile, catalogModel } from './catalog/index.js';
|
|
12
|
+
import { parsePolicyExpr } from './policy/index.js';
|
|
13
|
+
import { parsePredicateExpr } from './predicate/index.js';
|
|
14
|
+
import { AGENT_ENGINES, AGENT_ON_BUDGET_EXHAUSTED, DEFAULT_MAX_CONTEXT_TOKENS, DEFAULT_OBSERVABILITY, DEFAULT_RUNTIME, IR_VERSION, isGuardrailPolicyKind, isIndexMetric, isProvider, isVectorStoreKind, modelIdentity, PRIMITIVE_TYPES, pipelineIdentity, promptIdentity, RETRIEVAL_TRAITS, toolIdentity, } from './shapes.js';
|
|
15
|
+
export const DEFAULT_TARGET = 'py-fastapi';
|
|
16
|
+
export const DEFAULT_TOOL_TIMEOUT_MS = 30_000;
|
|
17
|
+
export const DEFAULT_TOOL_RETRIES = 0;
|
|
18
|
+
export const DEFAULT_IDEMPOTENCY = 'none';
|
|
19
|
+
export const DEFAULT_EVAL_MAX_REGRESSION = 0;
|
|
20
|
+
export const DEFAULT_EVAL_SAMPLES = 5;
|
|
21
|
+
/**
|
|
22
|
+
* Retrieval defaults. The spec schema requires every one of these fields, so they
|
|
23
|
+
* are only reached by a value that skipped validation; they mirror the §6 example
|
|
24
|
+
* rather than inventing a policy a valid spec could disagree with.
|
|
25
|
+
*/
|
|
26
|
+
export const DEFAULT_SOURCE_KIND = 'file';
|
|
27
|
+
export const DEFAULT_CHUNK_STRATEGY = 'recursive';
|
|
28
|
+
export const DEFAULT_CHUNK_SIZE = 800;
|
|
29
|
+
export const DEFAULT_CHUNK_OVERLAP = 0;
|
|
30
|
+
export const DEFAULT_INDEX_METRIC = 'cosine';
|
|
31
|
+
export const DEFAULT_RETRIEVER_TOP_K = 5;
|
|
32
|
+
export const DEFAULT_REFRESH_MODE = 'full';
|
|
33
|
+
/** Same posture as the retrieval defaults: reached only by a value that skipped validation. */
|
|
34
|
+
export const DEFAULT_MEMORY_TOKEN_BUDGET = 4096;
|
|
35
|
+
const BINDING_PATTERN = /^[a-z][a-z0-9_]{0,63}$/;
|
|
36
|
+
const TYPE_NAME_PATTERN = /^([A-Za-z][A-Za-z0-9]*)(\[\])?$/;
|
|
37
|
+
const EVAL_TARGET_PATTERN = /^([a-z][a-z0-9-]*):([a-z][a-z0-9_]{0,63})$/;
|
|
38
|
+
const SOLE_PLACEHOLDER_PATTERN = /^\s*\{\{\s*([A-Za-z_][A-Za-z0-9_.]*)\s*\}\}\s*$/;
|
|
39
|
+
export function buildIR(value, options = {}) {
|
|
40
|
+
const root = asRecord(value) ?? {};
|
|
41
|
+
const context = {
|
|
42
|
+
file: options.file,
|
|
43
|
+
diagnostics: [],
|
|
44
|
+
typeNames: new Set(Object.keys(asRecord(root.types) ?? {})),
|
|
45
|
+
};
|
|
46
|
+
const project = buildProject(root.project);
|
|
47
|
+
const types = buildTypes(root.types, context);
|
|
48
|
+
const models = buildModels(root.models);
|
|
49
|
+
const prompts = buildPrompts(root.prompts, context);
|
|
50
|
+
const tools = buildTools(root.tools, context);
|
|
51
|
+
const stores = buildStores(root.stores);
|
|
52
|
+
const retrievers = buildRetrievers(root.retrievers);
|
|
53
|
+
const memory = buildMemory(root.memory);
|
|
54
|
+
const guardrails = buildGuardrails(root.guardrails);
|
|
55
|
+
const agents = buildAgents(root.agents, context);
|
|
56
|
+
const deployments = buildDeployments(root.deployments);
|
|
57
|
+
const embeddings = buildEmbeddings(models);
|
|
58
|
+
const sensitive = sensitiveNames({ types, prompts });
|
|
59
|
+
const observability = buildObservability(root.observability, sensitive);
|
|
60
|
+
const promptModels = new Map(prompts.map((prompt) => [prompt.id, prompt.model]));
|
|
61
|
+
const pipelines = buildPipelines(root.pipelines, observability, sensitive, promptModels, context);
|
|
62
|
+
const evals = buildEvals(root.evals, context);
|
|
63
|
+
const ir = {
|
|
64
|
+
irVersion: IR_VERSION,
|
|
65
|
+
specVersion: asString(root.specVersion) ?? SPEC_VERSION,
|
|
66
|
+
project,
|
|
67
|
+
types,
|
|
68
|
+
models,
|
|
69
|
+
prompts,
|
|
70
|
+
tools,
|
|
71
|
+
retrievers,
|
|
72
|
+
embeddings,
|
|
73
|
+
stores,
|
|
74
|
+
memory,
|
|
75
|
+
agents,
|
|
76
|
+
pipelines,
|
|
77
|
+
evals,
|
|
78
|
+
guardrails,
|
|
79
|
+
deployments,
|
|
80
|
+
observability,
|
|
81
|
+
runtime: { ...DEFAULT_RUNTIME },
|
|
82
|
+
requiredCapabilities: [],
|
|
83
|
+
};
|
|
84
|
+
ir.requiredCapabilities = deriveRequiredCapabilities(ir);
|
|
85
|
+
return { ir, diagnostics: context.diagnostics };
|
|
86
|
+
}
|
|
87
|
+
function buildProject(raw) {
|
|
88
|
+
const record = asRecord(raw) ?? {};
|
|
89
|
+
const name = asString(record.name) ?? '';
|
|
90
|
+
const namespace = asString(record.namespace) ?? name;
|
|
91
|
+
const targets = asStringArray(record.targets);
|
|
92
|
+
return {
|
|
93
|
+
name,
|
|
94
|
+
namespace,
|
|
95
|
+
description: asString(record.description),
|
|
96
|
+
targets: targets.length === 0 ? [DEFAULT_TARGET] : targets,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function buildTypes(raw, context) {
|
|
100
|
+
const record = asRecord(raw) ?? {};
|
|
101
|
+
return sortedKeys(record).map((id) => {
|
|
102
|
+
const entry = asRecord(record[id]) ?? {};
|
|
103
|
+
const fields = asRecord(entry.fields) ?? {};
|
|
104
|
+
return {
|
|
105
|
+
id,
|
|
106
|
+
kind: 'struct',
|
|
107
|
+
description: asString(entry.description),
|
|
108
|
+
fields: sortedKeys(fields).map((fieldName) => buildTypeField(fieldName, asRecord(fields[fieldName]), context)),
|
|
109
|
+
};
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
function buildTypeField(name, raw, context) {
|
|
113
|
+
const record = asRecord(raw) ?? {};
|
|
114
|
+
const field = {
|
|
115
|
+
name,
|
|
116
|
+
type: buildTypeRef(record.type, context),
|
|
117
|
+
required: asBoolean(record.required) ?? false,
|
|
118
|
+
description: asString(record.description),
|
|
119
|
+
minimum: asNumber(record.minimum),
|
|
120
|
+
maximum: asNumber(record.maximum),
|
|
121
|
+
};
|
|
122
|
+
const values = Array.isArray(record.enum)
|
|
123
|
+
? record.enum.filter((item) => typeof item === 'string' || typeof item === 'number' || typeof item === 'boolean')
|
|
124
|
+
: undefined;
|
|
125
|
+
if (values !== undefined)
|
|
126
|
+
field.enum = values;
|
|
127
|
+
if (record.sensitive === true || isSensitiveName(name))
|
|
128
|
+
field.sensitive = true;
|
|
129
|
+
return field;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Resolves a `fieldType` string against the spec's declared type names. An
|
|
133
|
+
* unresolvable name is kept as `resolved: false`; the referential gate reports it
|
|
134
|
+
* with the document path, so this stays a pure projection.
|
|
135
|
+
*/
|
|
136
|
+
export function buildTypeRef(raw, context) {
|
|
137
|
+
const text = asString(raw) ?? '';
|
|
138
|
+
const match = TYPE_NAME_PATTERN.exec(text);
|
|
139
|
+
if (match === null) {
|
|
140
|
+
return { kind: 'named', name: text, list: false, resolved: false };
|
|
141
|
+
}
|
|
142
|
+
const name = match[1];
|
|
143
|
+
const list = match[2] === '[]';
|
|
144
|
+
if (PRIMITIVE_TYPES.includes(name)) {
|
|
145
|
+
return { kind: 'primitive', name: name, list };
|
|
146
|
+
}
|
|
147
|
+
return { kind: 'named', name, list, resolved: context.typeNames.has(name) };
|
|
148
|
+
}
|
|
149
|
+
function buildModels(raw) {
|
|
150
|
+
const record = asRecord(raw) ?? {};
|
|
151
|
+
return sortedKeys(record).map((id) => {
|
|
152
|
+
const entry = asRecord(record[id]) ?? {};
|
|
153
|
+
const modelId = asString(entry.modelId) ?? id;
|
|
154
|
+
const explicitCostProfile = asString(entry.costProfile);
|
|
155
|
+
const resolved = catalogCostProfile(modelId, explicitCostProfile);
|
|
156
|
+
const catalog = catalogModel(modelId);
|
|
157
|
+
const model = {
|
|
158
|
+
id,
|
|
159
|
+
identity: modelIdentity(id),
|
|
160
|
+
provider: asProvider(entry.provider),
|
|
161
|
+
modelId,
|
|
162
|
+
capabilities: asStringArray(entry.capabilities)
|
|
163
|
+
// The spec's `capabilities` enum is closed to four spellings that all map onto the
|
|
164
|
+
// model vocabulary, so no spec-derived caller reaches this filter: `checkSpecText`
|
|
165
|
+
// rejects anything else first, and `lowerSpec` returns before calling `buildIR`.
|
|
166
|
+
// It stays because `buildIR` takes `unknown` and promises `ModelCapability[]`, so a
|
|
167
|
+
// caller that skips validation gets a silent drop rather than a mistyped IR.
|
|
168
|
+
.map(mapSpecCapability)
|
|
169
|
+
.filter(isModelCapability),
|
|
170
|
+
params: buildParams(entry.params),
|
|
171
|
+
limits: buildLimits(entry.limits, catalog?.limits),
|
|
172
|
+
fallbacks: asStringArray(entry.fallbacks),
|
|
173
|
+
traits: [],
|
|
174
|
+
};
|
|
175
|
+
if (resolved !== undefined) {
|
|
176
|
+
model.costProfile = resolved.profile;
|
|
177
|
+
model.costProfileId = resolved.id;
|
|
178
|
+
}
|
|
179
|
+
else if (explicitCostProfile !== undefined) {
|
|
180
|
+
model.costProfileId = explicitCostProfile;
|
|
181
|
+
}
|
|
182
|
+
const cache = buildCache(entry.cache);
|
|
183
|
+
if (cache !== undefined)
|
|
184
|
+
model.cache = cache;
|
|
185
|
+
const baseUrl = asString(entry.baseUrl);
|
|
186
|
+
const apiKeyEnv = asString(entry.apiKeyEnv);
|
|
187
|
+
const description = asString(entry.description);
|
|
188
|
+
if (baseUrl !== undefined)
|
|
189
|
+
model.baseUrl = baseUrl;
|
|
190
|
+
if (apiKeyEnv !== undefined)
|
|
191
|
+
model.apiKeyEnv = apiKeyEnv;
|
|
192
|
+
if (description !== undefined)
|
|
193
|
+
model.description = description;
|
|
194
|
+
return model;
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
function buildCache(raw) {
|
|
198
|
+
const record = asRecord(raw);
|
|
199
|
+
if (record === undefined)
|
|
200
|
+
return undefined;
|
|
201
|
+
const prompt = asBoolean(record.prompt);
|
|
202
|
+
const semantic = buildSemanticCache(record.semantic);
|
|
203
|
+
return prompt === undefined && semantic === undefined ? undefined : { prompt, semantic };
|
|
204
|
+
}
|
|
205
|
+
function buildSemanticCache(raw) {
|
|
206
|
+
const record = asRecord(raw);
|
|
207
|
+
if (record === undefined)
|
|
208
|
+
return undefined;
|
|
209
|
+
const threshold = asNumber(record.threshold);
|
|
210
|
+
const ttlSeconds = asNumber(record.ttlSeconds);
|
|
211
|
+
if (threshold === undefined || ttlSeconds === undefined)
|
|
212
|
+
return undefined;
|
|
213
|
+
return { threshold, ttlSeconds };
|
|
214
|
+
}
|
|
215
|
+
function mapSpecCapability(value) {
|
|
216
|
+
const mapped = SPEC_MODEL_CAPABILITY_MAP;
|
|
217
|
+
return mapped[value] ?? value;
|
|
218
|
+
}
|
|
219
|
+
function asProvider(raw) {
|
|
220
|
+
const value = asString(raw);
|
|
221
|
+
return value !== undefined && isProvider(value) ? value : 'fake';
|
|
222
|
+
}
|
|
223
|
+
function buildParams(raw) {
|
|
224
|
+
const record = asRecord(raw) ?? {};
|
|
225
|
+
const params = {};
|
|
226
|
+
const temperature = asNumber(record.temperature);
|
|
227
|
+
const topP = asNumber(record.topP);
|
|
228
|
+
const maxTokens = asNumber(record.maxTokens);
|
|
229
|
+
const seed = asNumber(record.seed);
|
|
230
|
+
if (temperature !== undefined)
|
|
231
|
+
params.temperature = temperature;
|
|
232
|
+
if (topP !== undefined)
|
|
233
|
+
params.topP = topP;
|
|
234
|
+
if (maxTokens !== undefined)
|
|
235
|
+
params.maxTokens = maxTokens;
|
|
236
|
+
if (seed !== undefined)
|
|
237
|
+
params.seed = seed;
|
|
238
|
+
const stop = asStringArray(record.stop);
|
|
239
|
+
if (stop.length > 0)
|
|
240
|
+
params.stop = stop;
|
|
241
|
+
return params;
|
|
242
|
+
}
|
|
243
|
+
function buildLimits(raw, catalogLimits) {
|
|
244
|
+
const record = asRecord(raw) ?? {};
|
|
245
|
+
const limits = {
|
|
246
|
+
contextTokens: asNumber(record.maxInputTokens) ?? catalogLimits?.contextTokens ?? DEFAULT_MAX_CONTEXT_TOKENS,
|
|
247
|
+
};
|
|
248
|
+
const maxOutputTokens = asNumber(record.maxOutputTokens) ?? catalogLimits?.maxOutputTokens;
|
|
249
|
+
const rpm = asNumber(record.rpm) ?? catalogLimits?.rpm;
|
|
250
|
+
const tpm = asNumber(record.tpm) ?? catalogLimits?.tpm;
|
|
251
|
+
if (maxOutputTokens !== undefined)
|
|
252
|
+
limits.maxOutputTokens = maxOutputTokens;
|
|
253
|
+
if (rpm !== undefined)
|
|
254
|
+
limits.rpm = rpm;
|
|
255
|
+
if (tpm !== undefined)
|
|
256
|
+
limits.tpm = tpm;
|
|
257
|
+
return limits;
|
|
258
|
+
}
|
|
259
|
+
function buildPrompts(raw, context) {
|
|
260
|
+
const record = asRecord(raw) ?? {};
|
|
261
|
+
return sortedKeys(record).map((id) => {
|
|
262
|
+
const entry = asRecord(record[id]) ?? {};
|
|
263
|
+
const outputRecord = asRecord(entry.output) ?? {};
|
|
264
|
+
const kind = asString(outputRecord.kind) ?? 'text';
|
|
265
|
+
const version = asNumber(entry.version) ?? 1;
|
|
266
|
+
return {
|
|
267
|
+
id,
|
|
268
|
+
version,
|
|
269
|
+
identity: promptIdentity(id, version),
|
|
270
|
+
model: asString(entry.model) ?? '',
|
|
271
|
+
template: asString(entry.template) ?? '',
|
|
272
|
+
variables: buildPromptVariables(entry.variables, context),
|
|
273
|
+
output: buildPromptOutput(kind, outputRecord.schema, context),
|
|
274
|
+
params: {},
|
|
275
|
+
examples: [],
|
|
276
|
+
guardrails: [],
|
|
277
|
+
evals: [],
|
|
278
|
+
traits: [],
|
|
279
|
+
};
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
/** `schema` is declared only for `structured`, so no other kind carries a TypeRef. */
|
|
283
|
+
function buildPromptOutput(kind, schema, context) {
|
|
284
|
+
if (kind === 'structured')
|
|
285
|
+
return { kind: 'structured', schema: buildTypeRef(schema, context) };
|
|
286
|
+
if (kind === 'json')
|
|
287
|
+
return { kind: 'json' };
|
|
288
|
+
return { kind: 'text' };
|
|
289
|
+
}
|
|
290
|
+
function buildPromptVariables(raw, context) {
|
|
291
|
+
const record = asRecord(raw) ?? {};
|
|
292
|
+
return sortedKeys(record).map((name) => {
|
|
293
|
+
const entry = asRecord(record[name]) ?? {};
|
|
294
|
+
const variable = {
|
|
295
|
+
name,
|
|
296
|
+
type: buildTypeRef(entry.type ?? 'string', context),
|
|
297
|
+
required: asBoolean(entry.required) ?? false,
|
|
298
|
+
};
|
|
299
|
+
const description = asString(entry.description);
|
|
300
|
+
if (description !== undefined)
|
|
301
|
+
variable.description = description;
|
|
302
|
+
if (entry.sensitive === true || isSensitiveName(name))
|
|
303
|
+
variable.sensitive = true;
|
|
304
|
+
if ('default' in entry && isJsonValue(entry.default))
|
|
305
|
+
variable.default = entry.default;
|
|
306
|
+
return variable;
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
/** Guards against `undefined`/functions leaking into the IR via a hand-written document. */
|
|
310
|
+
function isJsonValue(value) {
|
|
311
|
+
if (value === null || typeof value === 'boolean' || typeof value === 'string')
|
|
312
|
+
return true;
|
|
313
|
+
if (typeof value === 'number')
|
|
314
|
+
return Number.isFinite(value);
|
|
315
|
+
if (Array.isArray(value))
|
|
316
|
+
return value.every(isJsonValue);
|
|
317
|
+
if (typeof value !== 'object')
|
|
318
|
+
return false;
|
|
319
|
+
return Object.values(value).every(isJsonValue);
|
|
320
|
+
}
|
|
321
|
+
function buildTools(raw, context) {
|
|
322
|
+
const record = asRecord(raw) ?? {};
|
|
323
|
+
return sortedKeys(record).map((id) => {
|
|
324
|
+
const entry = asRecord(record[id]) ?? {};
|
|
325
|
+
const handlerRecord = asRecord(entry.handler) ?? {};
|
|
326
|
+
const authSource = asString(entry.auth);
|
|
327
|
+
const sideEffects = asSideEffects(entry.sideEffects);
|
|
328
|
+
const tool = {
|
|
329
|
+
id,
|
|
330
|
+
identity: toolIdentity(id),
|
|
331
|
+
name: id,
|
|
332
|
+
description: asString(entry.description) ?? '',
|
|
333
|
+
input: buildTypeRef(entry.input, context),
|
|
334
|
+
output: buildTypeRef(entry.output, context),
|
|
335
|
+
sideEffects,
|
|
336
|
+
// A destructive tool requires confirmation unless the spec says otherwise.
|
|
337
|
+
requiresConfirmation: asBoolean(entry.requiresConfirmation) ?? sideEffects === 'destructive',
|
|
338
|
+
timeoutMs: asNumber(entry.timeoutMs) ?? DEFAULT_TOOL_TIMEOUT_MS,
|
|
339
|
+
retries: asNumber(entry.retries) ?? DEFAULT_TOOL_RETRIES,
|
|
340
|
+
idempotency: asIdempotency(entry.idempotency),
|
|
341
|
+
handler: buildToolHandler(handlerRecord),
|
|
342
|
+
traits: [],
|
|
343
|
+
};
|
|
344
|
+
if (authSource !== undefined) {
|
|
345
|
+
const parsed = parsePolicyExpr(authSource, {
|
|
346
|
+
path: ['tools', id, 'auth'],
|
|
347
|
+
file: context.file,
|
|
348
|
+
});
|
|
349
|
+
context.diagnostics.push(...parsed.diagnostics);
|
|
350
|
+
if (parsed.ast !== undefined)
|
|
351
|
+
tool.auth = parsed.ast;
|
|
352
|
+
}
|
|
353
|
+
return tool;
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
function buildToolHandler(record) {
|
|
357
|
+
const kind = asString(record.kind);
|
|
358
|
+
if (kind === 'generated' || kind === 'mcp') {
|
|
359
|
+
const handler = { kind };
|
|
360
|
+
const symbol = asString(record.symbol);
|
|
361
|
+
if (symbol !== undefined)
|
|
362
|
+
handler.symbol = symbol;
|
|
363
|
+
const mcp = asRecord(record.mcp);
|
|
364
|
+
if (mcp !== undefined) {
|
|
365
|
+
handler.mcp = { server: asString(mcp.server) ?? '', tool: asString(mcp.tool) ?? '' };
|
|
366
|
+
}
|
|
367
|
+
return handler;
|
|
368
|
+
}
|
|
369
|
+
const handler = { kind: 'business' };
|
|
370
|
+
const symbol = asString(record.symbol);
|
|
371
|
+
if (symbol !== undefined)
|
|
372
|
+
handler.symbol = symbol;
|
|
373
|
+
return handler;
|
|
374
|
+
}
|
|
375
|
+
function asSideEffects(raw) {
|
|
376
|
+
const value = asString(raw);
|
|
377
|
+
return value === 'read' || value === 'write' || value === 'destructive' || value === 'none'
|
|
378
|
+
? value
|
|
379
|
+
: 'none';
|
|
380
|
+
}
|
|
381
|
+
function buildStores(raw) {
|
|
382
|
+
const record = asRecord(raw) ?? {};
|
|
383
|
+
return sortedKeys(record).map((id) => {
|
|
384
|
+
const entry = asRecord(record[id]) ?? {};
|
|
385
|
+
const store = {
|
|
386
|
+
id,
|
|
387
|
+
kind: asStoreKind(entry.kind),
|
|
388
|
+
traits: [],
|
|
389
|
+
};
|
|
390
|
+
const uri = asString(entry.uri);
|
|
391
|
+
const dimensions = asNumber(entry.dimensions);
|
|
392
|
+
if (uri !== undefined)
|
|
393
|
+
store.uri = uri;
|
|
394
|
+
if (dimensions !== undefined)
|
|
395
|
+
store.dimensions = dimensions;
|
|
396
|
+
return store;
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
function asStoreKind(raw) {
|
|
400
|
+
const value = asString(raw);
|
|
401
|
+
return value !== undefined && isVectorStoreKind(value) ? value : 'memory';
|
|
402
|
+
}
|
|
403
|
+
function buildRetrievers(raw) {
|
|
404
|
+
const record = asRecord(raw) ?? {};
|
|
405
|
+
return sortedKeys(record).map((id) => {
|
|
406
|
+
const entry = asRecord(record[id]) ?? {};
|
|
407
|
+
const source = asRecord(entry.source) ?? {};
|
|
408
|
+
const chunking = asRecord(entry.chunking) ?? {};
|
|
409
|
+
const index = asRecord(entry.index) ?? {};
|
|
410
|
+
const refresh = asRecord(entry.refresh) ?? {};
|
|
411
|
+
const retriever = {
|
|
412
|
+
id,
|
|
413
|
+
source: buildRetrieverSource(source),
|
|
414
|
+
chunking: {
|
|
415
|
+
strategy: asChunkStrategy(chunking.strategy),
|
|
416
|
+
size: asNumber(chunking.size) ?? DEFAULT_CHUNK_SIZE,
|
|
417
|
+
overlap: asNumber(chunking.overlap) ?? DEFAULT_CHUNK_OVERLAP,
|
|
418
|
+
keepMetadata: asStringArray(chunking.keepMetadata),
|
|
419
|
+
},
|
|
420
|
+
embedModel: asString(entry.embedModel) ?? '',
|
|
421
|
+
store: asString(entry.store) ?? '',
|
|
422
|
+
index: {
|
|
423
|
+
metric: asIndexMetric(index.metric),
|
|
424
|
+
hybrid: asBoolean(index.hybrid) ?? false,
|
|
425
|
+
},
|
|
426
|
+
topK: asNumber(entry.topK) ?? DEFAULT_RETRIEVER_TOP_K,
|
|
427
|
+
refresh: buildRefresh(refresh),
|
|
428
|
+
traits: buildRetrievalTraits(entry.traits),
|
|
429
|
+
};
|
|
430
|
+
const rerank = asRecord(entry.rerank);
|
|
431
|
+
if (rerank !== undefined) {
|
|
432
|
+
retriever.rerank = {
|
|
433
|
+
model: asString(rerank.model) ?? '',
|
|
434
|
+
topN: asNumber(rerank.topN) ?? retriever.topK,
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
const filters = asStringArray(entry.filters);
|
|
438
|
+
if (filters.length > 0)
|
|
439
|
+
retriever.filters = filters;
|
|
440
|
+
const params = asRecord(index.params);
|
|
441
|
+
if (params !== undefined)
|
|
442
|
+
retriever.index.params = params;
|
|
443
|
+
return retriever;
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
function buildRetrieverSource(raw) {
|
|
447
|
+
const value = asString(raw.kind);
|
|
448
|
+
const kind = value === 's3' || value === 'http' || value === 'db' || value === 'mcp' ? value : 'file';
|
|
449
|
+
const source = { kind, uri: asString(raw.uri) ?? '' };
|
|
450
|
+
const glob = asString(raw.glob);
|
|
451
|
+
if (glob !== undefined)
|
|
452
|
+
source.glob = glob;
|
|
453
|
+
return source;
|
|
454
|
+
}
|
|
455
|
+
function buildRefresh(raw) {
|
|
456
|
+
const mode = asString(raw.mode);
|
|
457
|
+
const refresh = {
|
|
458
|
+
mode: mode === 'incremental' ? 'incremental' : DEFAULT_REFRESH_MODE,
|
|
459
|
+
};
|
|
460
|
+
const schedule = asString(raw.schedule);
|
|
461
|
+
if (schedule !== undefined)
|
|
462
|
+
refresh.schedule = schedule;
|
|
463
|
+
return refresh;
|
|
464
|
+
}
|
|
465
|
+
function asChunkStrategy(raw) {
|
|
466
|
+
const value = asString(raw);
|
|
467
|
+
return value === 'fixed' || value === 'semantic' || value === 'code'
|
|
468
|
+
? value
|
|
469
|
+
: DEFAULT_CHUNK_STRATEGY;
|
|
470
|
+
}
|
|
471
|
+
function asIndexMetric(raw) {
|
|
472
|
+
const value = asString(raw);
|
|
473
|
+
return value !== undefined && isIndexMetric(value) ? value : DEFAULT_INDEX_METRIC;
|
|
474
|
+
}
|
|
475
|
+
function buildRetrievalTraits(raw) {
|
|
476
|
+
return asStringArray(raw)
|
|
477
|
+
.filter((name) => RETRIEVAL_TRAITS.includes(name))
|
|
478
|
+
.map((name) => ({ name }));
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* One entry per model declaring `embeddings`, keyed by the model alias so
|
|
482
|
+
* `retriever.embedModel` and `store.dimensions` can be checked against it.
|
|
483
|
+
*/
|
|
484
|
+
function buildEmbeddings(models) {
|
|
485
|
+
return models
|
|
486
|
+
.filter((model) => model.capabilities.includes('embeddings'))
|
|
487
|
+
.map((model) => {
|
|
488
|
+
const embedding = {
|
|
489
|
+
id: model.id,
|
|
490
|
+
provider: model.provider,
|
|
491
|
+
modelId: model.modelId,
|
|
492
|
+
traits: [],
|
|
493
|
+
};
|
|
494
|
+
const dimensions = catalogModel(model.modelId)?.dimensions;
|
|
495
|
+
if (dimensions !== undefined)
|
|
496
|
+
embedding.dimensions = dimensions;
|
|
497
|
+
if (model.costProfile !== undefined)
|
|
498
|
+
embedding.costProfile = model.costProfile;
|
|
499
|
+
return embedding;
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
function asIdempotency(raw) {
|
|
503
|
+
const value = asString(raw);
|
|
504
|
+
return value === 'key' || value === 'natural' || value === 'none' ? value : DEFAULT_IDEMPOTENCY;
|
|
505
|
+
}
|
|
506
|
+
function buildPipelines(raw, observability, sensitive, promptModels, context) {
|
|
507
|
+
const record = asRecord(raw) ?? {};
|
|
508
|
+
return sortedKeys(record).map((id) => {
|
|
509
|
+
const entry = asRecord(record[id]) ?? {};
|
|
510
|
+
const trigger = asRecord(asRecord(entry.trigger)?.http) ?? {};
|
|
511
|
+
const pipeline = {
|
|
512
|
+
id,
|
|
513
|
+
identity: pipelineIdentity(id),
|
|
514
|
+
trigger: {
|
|
515
|
+
kind: 'http',
|
|
516
|
+
method: asString(trigger.method) ?? 'post',
|
|
517
|
+
path: asString(trigger.path) ?? '/',
|
|
518
|
+
},
|
|
519
|
+
steps: buildStepList(entry.steps, promptModels, context, ['pipelines', id, 'steps']),
|
|
520
|
+
errorPolicy: buildErrorPolicy(entry.errorPolicy),
|
|
521
|
+
observability: buildObservability(entry.observability, sensitive, observability),
|
|
522
|
+
};
|
|
523
|
+
const description = asString(entry.description);
|
|
524
|
+
if (description !== undefined)
|
|
525
|
+
pipeline.description = description;
|
|
526
|
+
return pipeline;
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
function buildErrorPolicy(raw) {
|
|
530
|
+
const record = asRecord(raw) ?? {};
|
|
531
|
+
return {
|
|
532
|
+
retries: asNumber(record.retries) ?? 0,
|
|
533
|
+
fallback: asString(record.fallback),
|
|
534
|
+
deadLetter: asString(record.deadLetter),
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
function buildStepList(raw, promptModels, context, path) {
|
|
538
|
+
return asArray(raw).map((step, index) => buildStep(step, promptModels, context, [...path, index]));
|
|
539
|
+
}
|
|
540
|
+
function buildStep(raw, promptModels, context, path) {
|
|
541
|
+
const record = asRecord(raw) ?? {};
|
|
542
|
+
const generate = asRecord(record.generate);
|
|
543
|
+
if (generate !== undefined) {
|
|
544
|
+
const prompt = asString(generate.prompt) ?? '';
|
|
545
|
+
const step = {
|
|
546
|
+
kind: 'generate',
|
|
547
|
+
prompt,
|
|
548
|
+
model: promptModels.get(prompt) ?? '',
|
|
549
|
+
input: buildStepInput(generate.input),
|
|
550
|
+
as: asString(generate.as) ?? '',
|
|
551
|
+
};
|
|
552
|
+
const contextName = asString(generate.context);
|
|
553
|
+
if (contextName !== undefined)
|
|
554
|
+
step.context = contextName;
|
|
555
|
+
return step;
|
|
556
|
+
}
|
|
557
|
+
const retrieve = asRecord(record.retrieve);
|
|
558
|
+
if (retrieve !== undefined) {
|
|
559
|
+
return buildRetrieveStep(retrieve);
|
|
560
|
+
}
|
|
561
|
+
const tool = asRecord(record.tool);
|
|
562
|
+
if (tool !== undefined) {
|
|
563
|
+
return {
|
|
564
|
+
kind: 'tool',
|
|
565
|
+
tool: asString(tool.tool) ?? '',
|
|
566
|
+
input: buildStepInput(tool.input),
|
|
567
|
+
as: asString(tool.as) ?? '',
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
const branch = asRecord(record.branch);
|
|
571
|
+
if (branch !== undefined) {
|
|
572
|
+
return buildBranchStep(branch, promptModels, context, [...path, 'branch']);
|
|
573
|
+
}
|
|
574
|
+
const parallel = asRecord(record.parallel);
|
|
575
|
+
if (parallel !== undefined) {
|
|
576
|
+
const arms = asArray(parallel.branches);
|
|
577
|
+
return {
|
|
578
|
+
kind: 'parallel',
|
|
579
|
+
branches: arms.map((arm, index) => buildStepList(arm, promptModels, context, [...path, 'parallel', 'branches', index])),
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
const map = asRecord(record.map);
|
|
583
|
+
if (map !== undefined) {
|
|
584
|
+
const body = asRecord(map.step) ?? {};
|
|
585
|
+
const mapStep = {
|
|
586
|
+
kind: 'map',
|
|
587
|
+
over: asString(map.over) ?? '',
|
|
588
|
+
as: asString(map.as) ?? '',
|
|
589
|
+
step: buildStep(body, promptModels, context, [...path, 'map', 'step']),
|
|
590
|
+
};
|
|
591
|
+
return mapStep;
|
|
592
|
+
}
|
|
593
|
+
const reduce = asRecord(record.reduce);
|
|
594
|
+
if (reduce !== undefined) {
|
|
595
|
+
const prompt = asString(reduce.prompt) ?? '';
|
|
596
|
+
return {
|
|
597
|
+
kind: 'reduce',
|
|
598
|
+
over: asString(reduce.over) ?? '',
|
|
599
|
+
prompt,
|
|
600
|
+
model: promptModels.get(prompt) ?? '',
|
|
601
|
+
as: asString(reduce.as) ?? '',
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
const rerank = asRecord(record.rerank);
|
|
605
|
+
if (rerank !== undefined) {
|
|
606
|
+
return {
|
|
607
|
+
kind: 'rerank',
|
|
608
|
+
over: asString(rerank.over) ?? '',
|
|
609
|
+
query: asString(rerank.query) ?? '',
|
|
610
|
+
model: asString(rerank.model) ?? '',
|
|
611
|
+
topN: asNumber(rerank.topN) ?? 0,
|
|
612
|
+
as: asString(rerank.as) ?? '',
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
const guard = asRecord(record.guard);
|
|
616
|
+
if (guard !== undefined) {
|
|
617
|
+
return {
|
|
618
|
+
kind: 'guard',
|
|
619
|
+
guardrail: asString(guard.guardrail) ?? '',
|
|
620
|
+
stage: (asString(guard.stage) ?? 'input'),
|
|
621
|
+
text: asString(guard.text) ?? '',
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
const agent = asRecord(record.agent);
|
|
625
|
+
if (agent !== undefined) {
|
|
626
|
+
return {
|
|
627
|
+
kind: 'agent',
|
|
628
|
+
agent: asString(agent.agent) ?? '',
|
|
629
|
+
text: asString(agent.text) ?? '',
|
|
630
|
+
as: asString(agent.as) ?? '',
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
const emit = asRecord(record.emit);
|
|
634
|
+
return { kind: 'emit', event: asString(emit?.event) ?? '' };
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* The spec layer already rejected a `when` the grammar cannot parse, so a parse failure
|
|
638
|
+
* here means the two layers disagree. It still has to yield a `BranchStep`, because
|
|
639
|
+
* `StepShape` has no "broken" variant: an unparsable predicate becomes the constant
|
|
640
|
+
* `false`, so the failure closes the arm rather than opening it.
|
|
641
|
+
*/
|
|
642
|
+
function buildBranchStep(branch, promptModels, context, path) {
|
|
643
|
+
const source = asString(branch.when) ?? '';
|
|
644
|
+
const parsed = parsePolicyExpr(source, { path: [...path, 'when'], file: context.file });
|
|
645
|
+
context.diagnostics.push(...parsed.diagnostics);
|
|
646
|
+
return {
|
|
647
|
+
kind: 'branch',
|
|
648
|
+
when: parsed.ast ?? { kind: 'boolean', value: false },
|
|
649
|
+
// biome-ignore lint/suspicious/noThenProperty: the branch shape names its taken arm `then`.
|
|
650
|
+
then: buildStepList(branch.then, promptModels, context, [...path, 'then']),
|
|
651
|
+
else: buildStepList(branch.else, promptModels, context, [...path, 'else']),
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
/** `query` stays as declared: the runtime interpolates it per request. */
|
|
655
|
+
export function buildRetrieveStep(retrieve) {
|
|
656
|
+
return {
|
|
657
|
+
kind: 'retrieve',
|
|
658
|
+
retriever: asString(retrieve.retriever) ?? '',
|
|
659
|
+
query: asString(retrieve.query) ?? '',
|
|
660
|
+
as: asString(retrieve.as) ?? '',
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
function buildStepInput(raw) {
|
|
664
|
+
const record = asRecord(raw) ?? {};
|
|
665
|
+
const input = {};
|
|
666
|
+
for (const key of sortedKeys(record)) {
|
|
667
|
+
input[key] = buildStepInputValue(record[key]);
|
|
668
|
+
}
|
|
669
|
+
return input;
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* `{{ name }}` alone is a reference; a string with several placeholders is kept
|
|
673
|
+
* as a template. The gate resolves the roots, so nothing is guessed here.
|
|
674
|
+
*/
|
|
675
|
+
function buildStepInputValue(raw) {
|
|
676
|
+
if (raw === null)
|
|
677
|
+
return { kind: 'literal', value: null };
|
|
678
|
+
if (typeof raw === 'number' || typeof raw === 'boolean')
|
|
679
|
+
return { kind: 'literal', value: raw };
|
|
680
|
+
if (typeof raw !== 'string')
|
|
681
|
+
return { kind: 'literal', value: String(raw) };
|
|
682
|
+
const sole = SOLE_PLACEHOLDER_PATTERN.exec(raw);
|
|
683
|
+
if (sole !== null)
|
|
684
|
+
return { kind: 'reference', path: sole[1].split('.') };
|
|
685
|
+
if (raw.includes('{{'))
|
|
686
|
+
return { kind: 'template', template: raw };
|
|
687
|
+
return { kind: 'literal', value: raw };
|
|
688
|
+
}
|
|
689
|
+
const MEMORY_KINDS = ['buffer', 'window', 'summary', 'semantic', 'episodic', 'vector'];
|
|
690
|
+
const MEMORY_SCOPES = ['thread', 'user', 'global'];
|
|
691
|
+
const GUARDRAIL_STAGES = ['input', 'output', 'retrieval', 'tool-call'];
|
|
692
|
+
const GUARDRAIL_ACTIONS = ['block', 'redact', 'flag', 'escalate'];
|
|
693
|
+
const DEPLOY_TARGETS = ['serverless', 'container', 'batch', 'edge'];
|
|
694
|
+
function buildMemory(raw) {
|
|
695
|
+
const record = asRecord(raw) ?? {};
|
|
696
|
+
return sortedKeys(record).map((id) => {
|
|
697
|
+
const entry = asRecord(record[id]) ?? {};
|
|
698
|
+
const memory = {
|
|
699
|
+
id,
|
|
700
|
+
kind: asMemoryKind(entry.kind),
|
|
701
|
+
scope: asMemoryScope(entry.scope),
|
|
702
|
+
tokenBudget: asNumber(entry.tokenBudget) ?? DEFAULT_MEMORY_TOKEN_BUDGET,
|
|
703
|
+
redact: asStringArray(entry.redact),
|
|
704
|
+
traits: [],
|
|
705
|
+
};
|
|
706
|
+
const summarizeModel = asString(entry.summarizeModel);
|
|
707
|
+
const store = asString(entry.store);
|
|
708
|
+
const backend = asString(entry.backend);
|
|
709
|
+
const ttl = asString(entry.ttl);
|
|
710
|
+
if (summarizeModel !== undefined)
|
|
711
|
+
memory.summarizeModel = summarizeModel;
|
|
712
|
+
if (store !== undefined)
|
|
713
|
+
memory.store = store;
|
|
714
|
+
if (backend !== undefined)
|
|
715
|
+
memory.backend = backend;
|
|
716
|
+
if (ttl !== undefined)
|
|
717
|
+
memory.ttl = ttl;
|
|
718
|
+
return memory;
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
/** Unknown kinds collapse to the cheapest tier rather than passing through. */
|
|
722
|
+
function asMemoryKind(raw) {
|
|
723
|
+
const value = asString(raw);
|
|
724
|
+
return MEMORY_KINDS.includes(value ?? '')
|
|
725
|
+
? value
|
|
726
|
+
: 'buffer';
|
|
727
|
+
}
|
|
728
|
+
function asMemoryScope(raw) {
|
|
729
|
+
const value = asString(raw);
|
|
730
|
+
return MEMORY_SCOPES.includes(value ?? '')
|
|
731
|
+
? value
|
|
732
|
+
: 'thread';
|
|
733
|
+
}
|
|
734
|
+
function buildGuardrails(raw) {
|
|
735
|
+
const record = asRecord(raw) ?? {};
|
|
736
|
+
return sortedKeys(record).map((id) => {
|
|
737
|
+
const entry = asRecord(record[id]) ?? {};
|
|
738
|
+
const onFailRecord = asRecord(entry.onFail) ?? {};
|
|
739
|
+
const onFail = {
|
|
740
|
+
message: asString(onFailRecord.message) ?? '',
|
|
741
|
+
log: asBoolean(onFailRecord.log) ?? false,
|
|
742
|
+
};
|
|
743
|
+
const fallbackPrompt = asString(onFailRecord.fallbackPrompt);
|
|
744
|
+
if (fallbackPrompt !== undefined)
|
|
745
|
+
onFail.fallbackPrompt = fallbackPrompt;
|
|
746
|
+
const stages = asStringArray(entry.stages).filter((stage) => GUARDRAIL_STAGES.includes(stage));
|
|
747
|
+
const action = asString(entry.action);
|
|
748
|
+
return {
|
|
749
|
+
id,
|
|
750
|
+
stages,
|
|
751
|
+
policies: asStringArray(entry.policies).filter(isGuardrailPolicyKind),
|
|
752
|
+
action: GUARDRAIL_ACTIONS.includes(action ?? '')
|
|
753
|
+
? action
|
|
754
|
+
: 'block',
|
|
755
|
+
redact: asStringArray(entry.redact),
|
|
756
|
+
tools: asStringArray(entry.tools),
|
|
757
|
+
onFail,
|
|
758
|
+
traits: [],
|
|
759
|
+
};
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
function buildAgents(raw, context) {
|
|
763
|
+
const record = asRecord(raw) ?? {};
|
|
764
|
+
return sortedKeys(record).map((id) => {
|
|
765
|
+
const entry = asRecord(record[id]) ?? {};
|
|
766
|
+
const agent = {
|
|
767
|
+
id,
|
|
768
|
+
name: asString(entry.name) ?? id,
|
|
769
|
+
instructions: asString(entry.instructions) ?? '',
|
|
770
|
+
model: asString(entry.model) ?? '',
|
|
771
|
+
tools: asStringArray(entry.tools),
|
|
772
|
+
retrievers: asStringArray(entry.retrievers),
|
|
773
|
+
memory: asStringArray(entry.memory),
|
|
774
|
+
guardrails: asStringArray(entry.guardrails),
|
|
775
|
+
maxSteps: asNumber(entry.maxSteps) ?? 1,
|
|
776
|
+
handoffs: asStringArray(entry.handoffs),
|
|
777
|
+
delegates: asStringArray(entry.delegates),
|
|
778
|
+
engine: asAgentEngine(entry.engine),
|
|
779
|
+
traits: [],
|
|
780
|
+
};
|
|
781
|
+
const structuredOutput = asString(entry.structuredOutput);
|
|
782
|
+
if (structuredOutput !== undefined) {
|
|
783
|
+
agent.structuredOutput = buildTypeRef(structuredOutput, context);
|
|
784
|
+
}
|
|
785
|
+
const maxTokensBudget = asNumber(entry.maxTokensBudget);
|
|
786
|
+
if (maxTokensBudget !== undefined)
|
|
787
|
+
agent.maxTokensBudget = maxTokensBudget;
|
|
788
|
+
const maxCostBudget = asNumber(entry.maxCostBudget);
|
|
789
|
+
if (maxCostBudget !== undefined)
|
|
790
|
+
agent.maxCostBudget = maxCostBudget;
|
|
791
|
+
const onBudgetExhausted = asAgentOnBudgetExhausted(entry.onBudgetExhausted);
|
|
792
|
+
if (onBudgetExhausted !== undefined)
|
|
793
|
+
agent.onBudgetExhausted = onBudgetExhausted;
|
|
794
|
+
const engineVersion = asString(entry.engineVersion);
|
|
795
|
+
if (engineVersion !== undefined)
|
|
796
|
+
agent.engineVersion = engineVersion;
|
|
797
|
+
const humanInTheLoop = asRecord(entry.humanInTheLoop);
|
|
798
|
+
if (humanInTheLoop !== undefined) {
|
|
799
|
+
const parsed = parsePolicyExpr(asString(humanInTheLoop.when) ?? '', {
|
|
800
|
+
path: ['agents', id, 'humanInTheLoop', 'when'],
|
|
801
|
+
file: context.file,
|
|
802
|
+
});
|
|
803
|
+
context.diagnostics.push(...parsed.diagnostics);
|
|
804
|
+
if (parsed.ast !== undefined) {
|
|
805
|
+
agent.humanInTheLoop = {
|
|
806
|
+
when: parsed.ast,
|
|
807
|
+
channel: asString(humanInTheLoop.channel) ?? '',
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
return agent;
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
function asAgentEngine(raw) {
|
|
815
|
+
const value = asString(raw);
|
|
816
|
+
return AGENT_ENGINES.includes(value ?? '')
|
|
817
|
+
? value
|
|
818
|
+
: 'native';
|
|
819
|
+
}
|
|
820
|
+
function asAgentOnBudgetExhausted(raw) {
|
|
821
|
+
const value = asString(raw);
|
|
822
|
+
return AGENT_ON_BUDGET_EXHAUSTED.includes(value ?? '')
|
|
823
|
+
? value
|
|
824
|
+
: undefined;
|
|
825
|
+
}
|
|
826
|
+
function buildDeployments(raw) {
|
|
827
|
+
const record = asRecord(raw) ?? {};
|
|
828
|
+
return sortedKeys(record).map((id) => {
|
|
829
|
+
const entry = asRecord(record[id]) ?? {};
|
|
830
|
+
const target = asString(entry.target);
|
|
831
|
+
const deployment = {
|
|
832
|
+
id,
|
|
833
|
+
target: DEPLOY_TARGETS.includes(target ?? '')
|
|
834
|
+
? target
|
|
835
|
+
: 'serverless',
|
|
836
|
+
streaming: asBoolean(entry.streaming) ?? false,
|
|
837
|
+
secrets: asStringArray(entry.secrets),
|
|
838
|
+
env: buildDeploymentEnv(entry.env),
|
|
839
|
+
traits: [],
|
|
840
|
+
};
|
|
841
|
+
const concurrency = asNumber(entry.concurrency);
|
|
842
|
+
if (concurrency !== undefined)
|
|
843
|
+
deployment.concurrency = concurrency;
|
|
844
|
+
const autoscale = asRecord(entry.autoscale);
|
|
845
|
+
if (autoscale !== undefined) {
|
|
846
|
+
deployment.autoscale = {
|
|
847
|
+
min: asNumber(autoscale.min) ?? 0,
|
|
848
|
+
max: asNumber(autoscale.max) ?? 1,
|
|
849
|
+
metric: asString(autoscale.metric) ?? '',
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
return deployment;
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
function buildDeploymentEnv(raw) {
|
|
856
|
+
const env = [];
|
|
857
|
+
for (const item of asArray(raw)) {
|
|
858
|
+
const record = asRecord(item);
|
|
859
|
+
const name = asString(record?.name);
|
|
860
|
+
if (record === undefined || name === undefined)
|
|
861
|
+
continue;
|
|
862
|
+
const entry = { name };
|
|
863
|
+
const value = asString(record.value);
|
|
864
|
+
const secret = asBoolean(record.secret);
|
|
865
|
+
if (value !== undefined)
|
|
866
|
+
entry.value = value;
|
|
867
|
+
if (secret !== undefined)
|
|
868
|
+
entry.secret = secret;
|
|
869
|
+
env.push(entry);
|
|
870
|
+
}
|
|
871
|
+
return env;
|
|
872
|
+
}
|
|
873
|
+
function buildEvals(raw, context) {
|
|
874
|
+
const record = asRecord(raw) ?? {};
|
|
875
|
+
return sortedKeys(record).map((id) => {
|
|
876
|
+
const entry = asRecord(record[id]) ?? {};
|
|
877
|
+
const metrics = asArray(entry.metrics)
|
|
878
|
+
.map((metric, index) => buildMetric(metric, context, ['evals', id, 'metrics', index]))
|
|
879
|
+
.filter((metric) => metric !== undefined);
|
|
880
|
+
const gate = asRecord(entry.gate) ?? {};
|
|
881
|
+
const dataset = asRecord(entry.dataset) ?? {};
|
|
882
|
+
const budgets = metrics
|
|
883
|
+
.filter((metric) => metric.kind === 'cost')
|
|
884
|
+
.map((metric) => metric.max);
|
|
885
|
+
const latencies = metrics
|
|
886
|
+
.filter((metric) => metric.kind === 'latency')
|
|
887
|
+
.map((metric) => metric.maxMs);
|
|
888
|
+
const evalShape = {
|
|
889
|
+
id,
|
|
890
|
+
description: asString(entry.description),
|
|
891
|
+
target: buildEvalTarget(entry.target),
|
|
892
|
+
dataset: { source: 'file', uri: asString(dataset.uri) ?? '' },
|
|
893
|
+
metrics,
|
|
894
|
+
gate: {
|
|
895
|
+
ci: asBoolean(gate.ci) ?? false,
|
|
896
|
+
maxRegression: asNumber(gate.maxRegression) ?? DEFAULT_EVAL_MAX_REGRESSION,
|
|
897
|
+
samples: asNumber(gate.samples) ?? DEFAULT_EVAL_SAMPLES,
|
|
898
|
+
},
|
|
899
|
+
traits: [],
|
|
900
|
+
};
|
|
901
|
+
// Written only when declared, so every eval that predates `minMean` serializes
|
|
902
|
+
// byte-identically.
|
|
903
|
+
const minMean = asNumber(gate.minMean);
|
|
904
|
+
if (minMean !== undefined)
|
|
905
|
+
evalShape.gate.minMean = minMean;
|
|
906
|
+
if (budgets.length > 0)
|
|
907
|
+
evalShape.costBudget = Math.min(...budgets);
|
|
908
|
+
if (latencies.length > 0)
|
|
909
|
+
evalShape.latencyBudgetP95Ms = Math.min(...latencies);
|
|
910
|
+
return evalShape;
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
/** Parses the qualified `kind:id` target form; `valid` is false when it is not one. */
|
|
914
|
+
export function buildEvalTarget(raw) {
|
|
915
|
+
const qualified = asString(raw) ?? '';
|
|
916
|
+
const match = EVAL_TARGET_PATTERN.exec(qualified);
|
|
917
|
+
if (match === null) {
|
|
918
|
+
return { kind: 'pipeline', id: qualified, qualified, valid: false };
|
|
919
|
+
}
|
|
920
|
+
const kind = match[1];
|
|
921
|
+
const id = match[2];
|
|
922
|
+
if (kind !== 'pipeline' && kind !== 'prompt') {
|
|
923
|
+
return { kind: 'pipeline', id, qualified, valid: false };
|
|
924
|
+
}
|
|
925
|
+
return { kind, id, qualified, valid: true };
|
|
926
|
+
}
|
|
927
|
+
function buildMetric(raw, context, path) {
|
|
928
|
+
const record = asRecord(raw) ?? {};
|
|
929
|
+
const kind = asString(record.kind);
|
|
930
|
+
const field = asString(record.field);
|
|
931
|
+
switch (kind) {
|
|
932
|
+
case 'exact':
|
|
933
|
+
return field === undefined ? undefined : { kind: 'exact', field };
|
|
934
|
+
case 'contains': {
|
|
935
|
+
if (field === undefined)
|
|
936
|
+
return undefined;
|
|
937
|
+
const value = asString(record.value);
|
|
938
|
+
return value === undefined ? { kind: 'contains', field } : { kind: 'contains', field, value };
|
|
939
|
+
}
|
|
940
|
+
case 'json-schema':
|
|
941
|
+
return { kind: 'json-schema', schema: buildTypeRef(record.schema, context) };
|
|
942
|
+
case 'tool-trace':
|
|
943
|
+
return {
|
|
944
|
+
kind: 'tool-trace',
|
|
945
|
+
expect: asStringArray(record.expect),
|
|
946
|
+
ordered: asBoolean(record.ordered) ?? false,
|
|
947
|
+
};
|
|
948
|
+
case 'cost': {
|
|
949
|
+
const max = asNumber(record.max);
|
|
950
|
+
return max === undefined ? undefined : { kind: 'cost', max };
|
|
951
|
+
}
|
|
952
|
+
case 'latency': {
|
|
953
|
+
const maxMs = asNumber(record.maxMs);
|
|
954
|
+
return maxMs === undefined ? undefined : { kind: 'latency', maxMs };
|
|
955
|
+
}
|
|
956
|
+
case 'retrieval':
|
|
957
|
+
return {
|
|
958
|
+
kind: 'retrieval',
|
|
959
|
+
expectDocs: asStringArray(record.expectDocs),
|
|
960
|
+
// The cut-off is what the metric scores against; 1 is the narrowest reading
|
|
961
|
+
// of a k the spec must declare anyway.
|
|
962
|
+
k: asNumber(record.k) ?? 1,
|
|
963
|
+
};
|
|
964
|
+
case 'llm-judge': {
|
|
965
|
+
const rubric = asString(record.rubric);
|
|
966
|
+
if (rubric === undefined)
|
|
967
|
+
return undefined;
|
|
968
|
+
const metric = { kind: 'llm-judge', rubric };
|
|
969
|
+
const model = asString(record.model);
|
|
970
|
+
const threshold = asNumber(record.threshold);
|
|
971
|
+
if (field !== undefined)
|
|
972
|
+
metric.field = field;
|
|
973
|
+
if (model !== undefined)
|
|
974
|
+
metric.model = model;
|
|
975
|
+
if (threshold !== undefined)
|
|
976
|
+
metric.threshold = threshold;
|
|
977
|
+
return metric;
|
|
978
|
+
}
|
|
979
|
+
case 'assert': {
|
|
980
|
+
const message = asString(record.message);
|
|
981
|
+
if (message === undefined)
|
|
982
|
+
return undefined;
|
|
983
|
+
const parsed = parsePredicateExpr(asString(record.expr) ?? '', {
|
|
984
|
+
path: [...path, 'expr'],
|
|
985
|
+
file: context.file,
|
|
986
|
+
});
|
|
987
|
+
context.diagnostics.push(...parsed.diagnostics);
|
|
988
|
+
// An unparsable predicate is already an error diagnostic; dropping the metric
|
|
989
|
+
// keeps the IR closed, and the caller treats the diagnostics as the failure.
|
|
990
|
+
if (parsed.ast.kind === 'invalid')
|
|
991
|
+
return undefined;
|
|
992
|
+
return { kind: 'assert', expr: parsed.ast, message };
|
|
993
|
+
}
|
|
994
|
+
default:
|
|
995
|
+
return undefined;
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
/** Rejects a binding name that is not a lower-case identifier. */
|
|
999
|
+
export function isBindingName(value) {
|
|
1000
|
+
return BINDING_PATTERN.test(value);
|
|
1001
|
+
}
|
|
1002
|
+
/**
|
|
1003
|
+
* Sensitivity is declared, or inferred from a name deny-list. Inference is
|
|
1004
|
+
* fail-closed on purpose: over-redacting a trace is recoverable, leaking one is
|
|
1005
|
+
* not, and spec v0.1 has no `sensitive` marker of its own.
|
|
1006
|
+
*/
|
|
1007
|
+
const SENSITIVE_NAME_PATTERN = /(pass(word|phrase)|secret|token|api_?key|private_?key|credential|ssn|social_security|credit_?card|card_?number|cvv|iban|email|phone|address|birth|dob)/i;
|
|
1008
|
+
export function isSensitiveName(name) {
|
|
1009
|
+
return SENSITIVE_NAME_PATTERN.test(name);
|
|
1010
|
+
}
|
|
1011
|
+
/** Sorted names of every field and variable the IR marks as sensitive. */
|
|
1012
|
+
export function sensitiveNames(ir) {
|
|
1013
|
+
const names = new Set();
|
|
1014
|
+
for (const type of ir.types) {
|
|
1015
|
+
for (const field of type.fields) {
|
|
1016
|
+
if (field.sensitive === true)
|
|
1017
|
+
names.add(field.name);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
for (const prompt of ir.prompts) {
|
|
1021
|
+
for (const variable of prompt.variables) {
|
|
1022
|
+
if (variable.sensitive === true)
|
|
1023
|
+
names.add(variable.name);
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
return [...names].sort();
|
|
1027
|
+
}
|
|
1028
|
+
const PLACEHOLDER_PATTERN = /\{\{\s*([A-Za-z_][A-Za-z0-9_.]*)\s*\}\}/g;
|
|
1029
|
+
/** Every `{{ path }}` placeholder a prompt template references, deduplicated. */
|
|
1030
|
+
export function templatePlaceholders(template) {
|
|
1031
|
+
const names = new Set();
|
|
1032
|
+
for (const match of template.matchAll(PLACEHOLDER_PATTERN)) {
|
|
1033
|
+
names.add(match[1].split('.')[0]);
|
|
1034
|
+
}
|
|
1035
|
+
return [...names].sort();
|
|
1036
|
+
}
|
|
1037
|
+
/**
|
|
1038
|
+
* A declared `observability` is honoured as written; a synthesized one defaults
|
|
1039
|
+
* `redact` to the sensitive set. An explicit `redact` therefore wins, which is
|
|
1040
|
+
* what lets the security gate catch a trace config that narrows it too far.
|
|
1041
|
+
*/
|
|
1042
|
+
function buildObservability(raw, sensitive, inherited) {
|
|
1043
|
+
const fallbackRedact = inherited === undefined ? [...sensitive] : [...inherited.redact];
|
|
1044
|
+
const record = asRecord(raw);
|
|
1045
|
+
if (record === undefined) {
|
|
1046
|
+
return { trace: inherited?.trace ?? DEFAULT_OBSERVABILITY.trace, redact: fallbackRedact };
|
|
1047
|
+
}
|
|
1048
|
+
return {
|
|
1049
|
+
trace: asBoolean(record.trace) ?? inherited?.trace ?? DEFAULT_OBSERVABILITY.trace,
|
|
1050
|
+
redact: Array.isArray(record.redact) ? asStringArray(record.redact) : fallbackRedact,
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
export function asRecord(value) {
|
|
1054
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
1055
|
+
? value
|
|
1056
|
+
: undefined;
|
|
1057
|
+
}
|
|
1058
|
+
export function asArray(value) {
|
|
1059
|
+
return Array.isArray(value) ? value : [];
|
|
1060
|
+
}
|
|
1061
|
+
export function asString(value) {
|
|
1062
|
+
return typeof value === 'string' ? value : undefined;
|
|
1063
|
+
}
|
|
1064
|
+
export function asNumber(value) {
|
|
1065
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
1066
|
+
}
|
|
1067
|
+
export function asBoolean(value) {
|
|
1068
|
+
return typeof value === 'boolean' ? value : undefined;
|
|
1069
|
+
}
|
|
1070
|
+
export function asStringArray(value) {
|
|
1071
|
+
return Array.isArray(value)
|
|
1072
|
+
? value.filter((item) => typeof item === 'string')
|
|
1073
|
+
: [];
|
|
1074
|
+
}
|
|
1075
|
+
/** Object keys in a deterministic order, so YAML key order never reaches the IR. */
|
|
1076
|
+
export function sortedKeys(record) {
|
|
1077
|
+
return Object.keys(record).sort();
|
|
1078
|
+
}
|
|
1079
|
+
//# sourceMappingURL=build.js.map
|