@full-self-browsing/lattice 0.0.0-bootstrap.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +2359 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3200 -0
- package/dist/index.js.map +1 -0
- package/dist/runtime-CfZ-sGGk.js +1359 -0
- package/dist/runtime-CfZ-sGGk.js.map +1 -0
- package/package.json +66 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3200 @@
|
|
|
1
|
+
import { C as createHookPipeline, S as BAND, T as toArtifactRef, _ as PAYLOAD_TYPE, a as formatToolsForProvider, b as decodeEnvelope, c as importMcpTools, d as validateOutputMap, f as createNoopSurvivabilityAdapter, g as createReceipt, h as createCheckpointHook, i as createNoopAgentHost, l as runTool, m as STEP_TRANSITION_EVENT_NAME, o as toolSchemaToJsonSchema, p as DEFAULT_CHECKPOINT_BAND, r as AgentDeniedError, s as defineTool, t as runAgent, u as toolArtifactRef, v as base64Encode, w as artifact, x as canonicalizeReceiptBody, y as buildPae } from "./runtime-CfZ-sGGk.js";
|
|
2
|
+
import canonicalize from "canonicalize";
|
|
3
|
+
import { mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
//#region src/contract/contract.ts
|
|
7
|
+
/**
|
|
8
|
+
* Factory for `CapabilityContract` values.
|
|
9
|
+
*
|
|
10
|
+
* Mirrors the `output()` and adapter factory style — exact-optional safe
|
|
11
|
+
* (does not emit `field: undefined` properties under `exactOptionalPropertyTypes`).
|
|
12
|
+
* Returns a frozen value with frozen nested objects so downstream code can
|
|
13
|
+
* rely on structural immutability when canonicalizing in Phase 9.
|
|
14
|
+
*/
|
|
15
|
+
function contract(input = {}) {
|
|
16
|
+
return Object.freeze({
|
|
17
|
+
kind: "capability-contract",
|
|
18
|
+
...input.budget !== void 0 ? { budget: Object.freeze({ ...input.budget }) } : {},
|
|
19
|
+
...input.invariants !== void 0 ? { invariants: Object.freeze(input.invariants.map((inv) => Object.freeze({ ...inv }))) } : {},
|
|
20
|
+
...input.qualityFloor !== void 0 ? { qualityFloor: Object.freeze({ ...input.qualityFloor }) } : {},
|
|
21
|
+
...input.requiredModalities !== void 0 ? { requiredModalities: Object.freeze([...input.requiredModalities]) } : {},
|
|
22
|
+
...input.requiredPrivacy !== void 0 ? { requiredPrivacy: input.requiredPrivacy } : {}
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
//#endregion
|
|
26
|
+
//#region src/contract/invariants.ts
|
|
27
|
+
let counter = 0;
|
|
28
|
+
function nextId(kind, options) {
|
|
29
|
+
counter += 1;
|
|
30
|
+
return options?.id ?? `${kind}-${counter}`;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Fluent builder for tripwire invariants.
|
|
34
|
+
*
|
|
35
|
+
* Each helper returns a frozen `InvariantDeclaration` with an auto-generated
|
|
36
|
+
* id of the form `${kind}-${counter}`. Callers may override the id via the
|
|
37
|
+
* second-positional `options.id` arg.
|
|
38
|
+
*
|
|
39
|
+
* The counter is monotonic across kinds — calling `inv.mustCite("a")` then
|
|
40
|
+
* `inv.fieldFromTable("x", ["y"])` yields ids `must-cite-1` then
|
|
41
|
+
* `field-from-table-2`. This keeps ids globally unique within a process.
|
|
42
|
+
*
|
|
43
|
+
* Note on `inv.matches`: the caller supplies the StandardSchema validator,
|
|
44
|
+
* and the tripwire evaluator trusts whatever `~standard.validate` returns.
|
|
45
|
+
* This is by design — `matches` is the caller-driven escape hatch (see
|
|
46
|
+
* T-08-05 in the 08-01-PLAN threat register).
|
|
47
|
+
*/
|
|
48
|
+
const inv = {
|
|
49
|
+
mustCite(artifactName, options) {
|
|
50
|
+
return Object.freeze({
|
|
51
|
+
id: nextId("must-cite", options),
|
|
52
|
+
kind: "must-cite",
|
|
53
|
+
artifactName
|
|
54
|
+
});
|
|
55
|
+
},
|
|
56
|
+
fieldFromTable(path, allowedValues, options) {
|
|
57
|
+
return Object.freeze({
|
|
58
|
+
id: nextId("field-from-table", options),
|
|
59
|
+
kind: "field-from-table",
|
|
60
|
+
path,
|
|
61
|
+
allowedValues: Object.freeze([...allowedValues])
|
|
62
|
+
});
|
|
63
|
+
},
|
|
64
|
+
noPII(path, options) {
|
|
65
|
+
return Object.freeze({
|
|
66
|
+
id: nextId("no-pii", options),
|
|
67
|
+
kind: "no-pii",
|
|
68
|
+
path
|
|
69
|
+
});
|
|
70
|
+
},
|
|
71
|
+
matches(path, schema, options) {
|
|
72
|
+
return Object.freeze({
|
|
73
|
+
id: nextId("matches", options),
|
|
74
|
+
kind: "matches",
|
|
75
|
+
path,
|
|
76
|
+
schema
|
|
77
|
+
});
|
|
78
|
+
},
|
|
79
|
+
__resetCounterForTests() {
|
|
80
|
+
counter = 0;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
//#endregion
|
|
84
|
+
//#region src/contract/pii-detectors.ts
|
|
85
|
+
/**
|
|
86
|
+
* Luhn check digit validator.
|
|
87
|
+
*
|
|
88
|
+
* Strips non-digit characters from `digits`, requires the resulting length
|
|
89
|
+
* to be 13-19 (ISO/IEC 7812 PAN range), then walks right-to-left doubling
|
|
90
|
+
* every second digit and summing. Returns true when the sum is a multiple
|
|
91
|
+
* of 10.
|
|
92
|
+
*/
|
|
93
|
+
function luhn(digits) {
|
|
94
|
+
const cleaned = digits.replace(/\D/g, "");
|
|
95
|
+
if (cleaned.length < 13 || cleaned.length > 19) return false;
|
|
96
|
+
let sum = 0;
|
|
97
|
+
let shouldDouble = false;
|
|
98
|
+
for (let i = cleaned.length - 1; i >= 0; i -= 1) {
|
|
99
|
+
const code = cleaned.charCodeAt(i);
|
|
100
|
+
if (code < 48 || code > 57) return false;
|
|
101
|
+
let digit = code - 48;
|
|
102
|
+
if (shouldDouble) {
|
|
103
|
+
digit *= 2;
|
|
104
|
+
if (digit > 9) digit -= 9;
|
|
105
|
+
}
|
|
106
|
+
sum += digit;
|
|
107
|
+
shouldDouble = !shouldDouble;
|
|
108
|
+
}
|
|
109
|
+
return sum % 10 === 0;
|
|
110
|
+
}
|
|
111
|
+
function execFirst(regex, input) {
|
|
112
|
+
const match = regex.exec(input);
|
|
113
|
+
return match ? match[0] : void 0;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Default PII detectors used by `evaluateTripwires` for `no-pii` invariants.
|
|
117
|
+
*
|
|
118
|
+
* Order is deterministic: email, us-ssn, credit-card, us-phone. Callers who
|
|
119
|
+
* need a different set can pass their own list to `evaluateTripwires`.
|
|
120
|
+
*/
|
|
121
|
+
const defaultPiiDetectors = Object.freeze([
|
|
122
|
+
{
|
|
123
|
+
name: "email",
|
|
124
|
+
detect(input) {
|
|
125
|
+
const substring = execFirst(/[\w.+-]+@[\w-]+\.[\w.-]+/, input);
|
|
126
|
+
return substring !== void 0 ? {
|
|
127
|
+
matched: true,
|
|
128
|
+
substring
|
|
129
|
+
} : { matched: false };
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
name: "us-ssn",
|
|
134
|
+
detect(input) {
|
|
135
|
+
const substring = execFirst(/\b\d{3}-\d{2}-\d{4}\b/, input);
|
|
136
|
+
return substring !== void 0 ? {
|
|
137
|
+
matched: true,
|
|
138
|
+
substring
|
|
139
|
+
} : { matched: false };
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
name: "credit-card",
|
|
144
|
+
detect(input) {
|
|
145
|
+
const candidate = execFirst(/\b(?:\d[ -]?){13,19}\b/, input);
|
|
146
|
+
if (candidate === void 0) return { matched: false };
|
|
147
|
+
const trimmed = candidate.replace(/[ -]+$/, "");
|
|
148
|
+
if (!luhn(trimmed)) return { matched: false };
|
|
149
|
+
return {
|
|
150
|
+
matched: true,
|
|
151
|
+
substring: trimmed
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
name: "us-phone",
|
|
157
|
+
detect(input) {
|
|
158
|
+
const substring = execFirst(/\b\d{3}-\d{3}-\d{4}\b|\(\d{3}\)\s?\d{3}-\d{4}/, input);
|
|
159
|
+
return substring !== void 0 ? {
|
|
160
|
+
matched: true,
|
|
161
|
+
substring
|
|
162
|
+
} : { matched: false };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
]);
|
|
166
|
+
//#endregion
|
|
167
|
+
//#region src/routing/catalog.ts
|
|
168
|
+
const DEFAULT_CATALOG_VERSION = "lattice:catalog:v1";
|
|
169
|
+
function createCapabilityCatalog(providers) {
|
|
170
|
+
return {
|
|
171
|
+
version: DEFAULT_CATALOG_VERSION,
|
|
172
|
+
models: providers.flatMap((provider) => {
|
|
173
|
+
if (provider.kind === "provider-adapter" && provider.capabilities !== void 0) return provider.capabilities;
|
|
174
|
+
return [defaultCapabilityForProvider(provider.id)];
|
|
175
|
+
})
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
function defaultCapabilityForProvider(providerId) {
|
|
179
|
+
return {
|
|
180
|
+
providerId,
|
|
181
|
+
modelId: `${providerId}:default`,
|
|
182
|
+
inputModalities: [
|
|
183
|
+
"text",
|
|
184
|
+
"json",
|
|
185
|
+
"image",
|
|
186
|
+
"audio",
|
|
187
|
+
"document",
|
|
188
|
+
"file",
|
|
189
|
+
"url",
|
|
190
|
+
"tool"
|
|
191
|
+
],
|
|
192
|
+
outputModalities: ["text", "json"],
|
|
193
|
+
fileTransport: [
|
|
194
|
+
"inline",
|
|
195
|
+
"json",
|
|
196
|
+
"url",
|
|
197
|
+
"base64",
|
|
198
|
+
"extracted-text",
|
|
199
|
+
"transcript"
|
|
200
|
+
],
|
|
201
|
+
contextWindow: 16e3,
|
|
202
|
+
structuredOutput: true,
|
|
203
|
+
toolUse: false,
|
|
204
|
+
streaming: false,
|
|
205
|
+
pricing: {
|
|
206
|
+
inputCostPer1M: 0,
|
|
207
|
+
outputCostPer1M: 0,
|
|
208
|
+
inputPer1kTokens: 0,
|
|
209
|
+
outputPer1kTokens: 0
|
|
210
|
+
},
|
|
211
|
+
latency: "interactive",
|
|
212
|
+
dataPolicy: {
|
|
213
|
+
privacy: ["standard", "sensitive"],
|
|
214
|
+
uploadRetention: "none",
|
|
215
|
+
supportsNoLogging: true,
|
|
216
|
+
supportsNoTraining: true
|
|
217
|
+
},
|
|
218
|
+
available: true
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Resolve the effective per-1k token pricing for a capability.
|
|
223
|
+
*
|
|
224
|
+
* Prefers the explicit `inputPer1kTokens` / `outputPer1kTokens` fields and
|
|
225
|
+
* falls back to dividing the legacy per-1M fields by 1000 when only those
|
|
226
|
+
* are present. Returns `undefined` per side when neither shape supplies a
|
|
227
|
+
* value, so callers can distinguish "free / zero" (`0`) from "unknown"
|
|
228
|
+
* (`undefined`) — Phase 7 cost normalization treats unknown pricing as
|
|
229
|
+
* `usage.costUsd === null`, not `0`.
|
|
230
|
+
*/
|
|
231
|
+
function effectivePer1kPricing(pricing) {
|
|
232
|
+
if (pricing === void 0) return {
|
|
233
|
+
inputPer1kTokens: void 0,
|
|
234
|
+
outputPer1kTokens: void 0
|
|
235
|
+
};
|
|
236
|
+
return {
|
|
237
|
+
inputPer1kTokens: pricing.inputPer1kTokens ?? (pricing.inputCostPer1M !== void 0 ? pricing.inputCostPer1M / 1e3 : void 0),
|
|
238
|
+
outputPer1kTokens: pricing.outputPer1kTokens ?? (pricing.outputCostPer1M !== void 0 ? pricing.outputCostPer1M / 1e3 : void 0)
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
//#endregion
|
|
242
|
+
//#region src/contract/preflight.ts
|
|
243
|
+
/**
|
|
244
|
+
* Pure cost estimator. Returns `null` when pricing is unknown (so downstream
|
|
245
|
+
* gates can distinguish "free / zero" from "unmeasured" per the Phase 7
|
|
246
|
+
* cost-normalization decision). Uses static catalog metadata only — no probes,
|
|
247
|
+
* no external pricing APIs.
|
|
248
|
+
*/
|
|
249
|
+
function estimateRouteCost(input) {
|
|
250
|
+
const { inputPer1kTokens, outputPer1kTokens } = effectivePer1kPricing(input.capability.pricing);
|
|
251
|
+
if (inputPer1kTokens === void 0 && outputPer1kTokens === void 0) return null;
|
|
252
|
+
return (inputPer1kTokens ?? 0) * input.estimatedInputTokens / 1e3 + (outputPer1kTokens ?? 0) * input.estimatedOutputTokens / 1e3;
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Pure pre-flight evaluator. Phase 9 receipts will reuse this for deterministic
|
|
256
|
+
* verdict reconstruction.
|
|
257
|
+
*
|
|
258
|
+
* Token estimation: Phase 7 does NOT define a separate token estimator. Output-
|
|
259
|
+
* token projection is the canonical responsibility of the router's existing
|
|
260
|
+
* `estimateRoute()` helper (in `routing/router.ts`), which already produces an
|
|
261
|
+
* `estimatedOutputTokens` value. The router passes that same value into this
|
|
262
|
+
* evaluator via `EvaluateContractInput.estimatedOutputTokens`, so preflight
|
|
263
|
+
* and the router always agree on the projected output size. Phase 9 receipts
|
|
264
|
+
* will pin the router's estimate as the deterministic input — intentionally
|
|
265
|
+
* one source of truth.
|
|
266
|
+
*
|
|
267
|
+
* Reject taxonomy (Phase 7 emits three of four codes):
|
|
268
|
+
* - `contract-budget-exceeded` (CONTRACT-04 + COST-03)
|
|
269
|
+
* - `contract-modality-missing` (CONTRACT-06)
|
|
270
|
+
* - `contract-privacy-mismatch` (CONTRACT-06)
|
|
271
|
+
* - `contract-quality-floor` (reserved for Phase 12 `lattice eval`; NEVER emitted here)
|
|
272
|
+
*/
|
|
273
|
+
function evaluateContractAgainstRoute(contract, input) {
|
|
274
|
+
if (contract === void 0) return {
|
|
275
|
+
ok: true,
|
|
276
|
+
reasons: []
|
|
277
|
+
};
|
|
278
|
+
const reasons = [];
|
|
279
|
+
if (contract.budget?.maxCostUsd !== void 0) {
|
|
280
|
+
const estimatedCost = estimateRouteCost({
|
|
281
|
+
capability: input.capability,
|
|
282
|
+
estimatedInputTokens: input.estimatedInputTokens,
|
|
283
|
+
estimatedOutputTokens: input.estimatedOutputTokens
|
|
284
|
+
});
|
|
285
|
+
if (estimatedCost === null) reasons.push({
|
|
286
|
+
code: "contract-budget-exceeded",
|
|
287
|
+
message: `${input.capability.modelId} pricing unknown; contract budget declared (maxCostUsd=${contract.budget.maxCostUsd}).`
|
|
288
|
+
});
|
|
289
|
+
else if (estimatedCost > contract.budget.maxCostUsd) reasons.push({
|
|
290
|
+
code: "contract-budget-exceeded",
|
|
291
|
+
message: `${input.capability.modelId} estimated ${estimatedCost.toFixed(6)} exceeds contract budget ${contract.budget.maxCostUsd}.`
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
if (contract.requiredModalities !== void 0) {
|
|
295
|
+
for (const modality of contract.requiredModalities) if (!input.capability.inputModalities.includes(modality) && !input.capability.outputModalities.includes(modality)) reasons.push({
|
|
296
|
+
code: "contract-modality-missing",
|
|
297
|
+
message: `${input.capability.modelId} does not support required modality ${modality}.`
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
if (contract.requiredPrivacy !== void 0) {
|
|
301
|
+
if (!input.capability.dataPolicy.privacy.includes(contract.requiredPrivacy)) reasons.push({
|
|
302
|
+
code: "contract-privacy-mismatch",
|
|
303
|
+
message: `${input.capability.modelId} does not satisfy contract privacy ${contract.requiredPrivacy}.`
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
return {
|
|
307
|
+
ok: reasons.length === 0,
|
|
308
|
+
reasons
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
//#endregion
|
|
312
|
+
//#region src/contract/tripwire.ts
|
|
313
|
+
/**
|
|
314
|
+
* Pure tripwire evaluator.
|
|
315
|
+
*
|
|
316
|
+
* No I/O, no Date.now, no random — same `(output, invariants)` always
|
|
317
|
+
* returns the same `TripwireResult`. Phase 9 receipts can reconstruct the
|
|
318
|
+
* verdict deterministically (T-08-04).
|
|
319
|
+
*
|
|
320
|
+
* Evaluates invariants in declaration order; the FIRST failing invariant
|
|
321
|
+
* aborts and returns its evidence. Subsequent invariants are not evaluated.
|
|
322
|
+
*
|
|
323
|
+
* @param output The provider output to inspect.
|
|
324
|
+
* @param invariants Invariants to evaluate, in declaration order.
|
|
325
|
+
* @param detectors PII detectors used for `no-pii` invariants. Defaults
|
|
326
|
+
* to `defaultPiiDetectors`. Callers can pass a custom
|
|
327
|
+
* list to override.
|
|
328
|
+
*/
|
|
329
|
+
async function evaluateTripwires(output, invariants, detectors = defaultPiiDetectors) {
|
|
330
|
+
for (const declaration of invariants) {
|
|
331
|
+
const result = await evaluateOne(output, declaration, detectors);
|
|
332
|
+
if (!result.ok) return result;
|
|
333
|
+
}
|
|
334
|
+
return { ok: true };
|
|
335
|
+
}
|
|
336
|
+
async function evaluateOne(output, declaration, detectors) {
|
|
337
|
+
switch (declaration.kind) {
|
|
338
|
+
case "must-cite": return evaluateMustCite(output, declaration);
|
|
339
|
+
case "field-from-table": return evaluateFieldFromTable(output, declaration);
|
|
340
|
+
case "no-pii": return evaluateNoPii(output, declaration, detectors);
|
|
341
|
+
case "matches": return evaluateMatches(output, declaration);
|
|
342
|
+
default: throw new Error(`Unknown invariant kind: ${JSON.stringify(declaration)}`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
function evaluateMustCite(output, decl) {
|
|
346
|
+
const located = locateCitations(output);
|
|
347
|
+
const cites = located?.value ?? [];
|
|
348
|
+
const path = located?.path ?? "citations";
|
|
349
|
+
if (cites.some((entry) => {
|
|
350
|
+
if (typeof entry === "string") return entry === decl.artifactName;
|
|
351
|
+
if (typeof entry === "object" && entry !== null && "source" in entry) return entry.source === decl.artifactName;
|
|
352
|
+
return false;
|
|
353
|
+
})) return { ok: true };
|
|
354
|
+
return {
|
|
355
|
+
ok: false,
|
|
356
|
+
evidence: {
|
|
357
|
+
invariantId: decl.id,
|
|
358
|
+
kind: "must-cite",
|
|
359
|
+
path,
|
|
360
|
+
observed: cites,
|
|
361
|
+
message: `must-cite: no citation found for "${decl.artifactName}".`
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Locate the citations payload in `output`. Searches top-level for a
|
|
367
|
+
* `citations` or `evidence` key holding an array. Per 08-CONTEXT.md:
|
|
368
|
+
* "Path defaults to evidence if the output has a citations field; the
|
|
369
|
+
* runtime locates the citations payload in the output."
|
|
370
|
+
*
|
|
371
|
+
* Returns `undefined` when neither field is an array.
|
|
372
|
+
*/
|
|
373
|
+
function locateCitations(output) {
|
|
374
|
+
if (typeof output !== "object" || output === null) return void 0;
|
|
375
|
+
const record = output;
|
|
376
|
+
for (const key of ["citations", "evidence"]) {
|
|
377
|
+
const value = record[key];
|
|
378
|
+
if (Array.isArray(value)) return {
|
|
379
|
+
value,
|
|
380
|
+
path: key
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
function evaluateFieldFromTable(output, decl) {
|
|
385
|
+
const value = resolvePath(output, decl.path);
|
|
386
|
+
if (typeof value === "string" && decl.allowedValues.includes(value)) return { ok: true };
|
|
387
|
+
return {
|
|
388
|
+
ok: false,
|
|
389
|
+
evidence: {
|
|
390
|
+
invariantId: decl.id,
|
|
391
|
+
kind: "field-from-table",
|
|
392
|
+
path: decl.path,
|
|
393
|
+
observed: value,
|
|
394
|
+
message: `field-from-table: value at "${decl.path}" not in allowedValues.`
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
function evaluateNoPii(output, decl, detectors) {
|
|
399
|
+
const value = resolvePath(output, decl.path);
|
|
400
|
+
if (typeof value !== "string") return { ok: true };
|
|
401
|
+
for (const detector of detectors) {
|
|
402
|
+
const result = detector.detect(value);
|
|
403
|
+
if (result.matched) return {
|
|
404
|
+
ok: false,
|
|
405
|
+
evidence: {
|
|
406
|
+
invariantId: decl.id,
|
|
407
|
+
kind: "no-pii",
|
|
408
|
+
path: decl.path,
|
|
409
|
+
observed: {
|
|
410
|
+
detector: detector.name,
|
|
411
|
+
substring: result.substring
|
|
412
|
+
},
|
|
413
|
+
message: `no-pii: detector "${detector.name}" flagged content at "${decl.path}".`
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
return { ok: true };
|
|
418
|
+
}
|
|
419
|
+
async function evaluateMatches(output, decl) {
|
|
420
|
+
const value = resolvePath(output, decl.path);
|
|
421
|
+
const validateResult = decl.schema["~standard"].validate(value);
|
|
422
|
+
const validation = validateResult instanceof Promise ? await validateResult : validateResult;
|
|
423
|
+
if ("issues" in validation && validation.issues !== void 0) {
|
|
424
|
+
const firstIssue = validation.issues[0];
|
|
425
|
+
return {
|
|
426
|
+
ok: false,
|
|
427
|
+
evidence: {
|
|
428
|
+
invariantId: decl.id,
|
|
429
|
+
kind: "matches",
|
|
430
|
+
path: decl.path,
|
|
431
|
+
observed: value,
|
|
432
|
+
message: firstIssue?.message ?? `matches: schema validation failed at "${decl.path}".`
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
return { ok: true };
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Resolve a dotted/bracketed path expression against a value.
|
|
440
|
+
*
|
|
441
|
+
* Supports three segment forms:
|
|
442
|
+
* - dotted key: `a.b.c`
|
|
443
|
+
* - bracket index: `a[0].b`
|
|
444
|
+
* - wildcard: `a[*].b` (materializes the array of resolutions)
|
|
445
|
+
*
|
|
446
|
+
* Returns `undefined` for missing paths (does not throw).
|
|
447
|
+
*
|
|
448
|
+
* NOTE (T-08-03): `[*]` materializes the array; deeply nested wildcard
|
|
449
|
+
* chains could allocate O(N^k). Accepted for v1.1 — provider responses
|
|
450
|
+
* are bounded by output token caps.
|
|
451
|
+
*/
|
|
452
|
+
function resolvePath(value, path) {
|
|
453
|
+
if (path === "") return value;
|
|
454
|
+
return walk(value, tokenize(path), 0);
|
|
455
|
+
}
|
|
456
|
+
function tokenize(path) {
|
|
457
|
+
const tokens = [];
|
|
458
|
+
let i = 0;
|
|
459
|
+
let buffer = "";
|
|
460
|
+
const flushKey = () => {
|
|
461
|
+
if (buffer.length > 0) {
|
|
462
|
+
tokens.push({
|
|
463
|
+
type: "key",
|
|
464
|
+
name: buffer
|
|
465
|
+
});
|
|
466
|
+
buffer = "";
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
while (i < path.length) {
|
|
470
|
+
const ch = path[i];
|
|
471
|
+
if (ch === ".") {
|
|
472
|
+
flushKey();
|
|
473
|
+
i += 1;
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
if (ch === "[") {
|
|
477
|
+
flushKey();
|
|
478
|
+
const end = path.indexOf("]", i + 1);
|
|
479
|
+
if (end === -1) {
|
|
480
|
+
buffer = path.slice(i);
|
|
481
|
+
i = path.length;
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
const inner = path.slice(i + 1, end);
|
|
485
|
+
if (inner === "*") tokens.push({ type: "wildcard" });
|
|
486
|
+
else {
|
|
487
|
+
const idx = Number(inner);
|
|
488
|
+
if (Number.isInteger(idx) && idx >= 0) tokens.push({
|
|
489
|
+
type: "index",
|
|
490
|
+
index: idx
|
|
491
|
+
});
|
|
492
|
+
else tokens.push({
|
|
493
|
+
type: "key",
|
|
494
|
+
name: inner
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
i = end + 1;
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
buffer += ch;
|
|
501
|
+
i += 1;
|
|
502
|
+
}
|
|
503
|
+
flushKey();
|
|
504
|
+
return tokens;
|
|
505
|
+
}
|
|
506
|
+
function walk(value, tokens, cursor) {
|
|
507
|
+
if (cursor >= tokens.length) return value;
|
|
508
|
+
if (value === void 0 || value === null) return void 0;
|
|
509
|
+
const token = tokens[cursor];
|
|
510
|
+
if (token.type === "key") {
|
|
511
|
+
if (typeof value !== "object") return void 0;
|
|
512
|
+
const next = value[token.name];
|
|
513
|
+
return walk(next, tokens, cursor + 1);
|
|
514
|
+
}
|
|
515
|
+
if (token.type === "index") {
|
|
516
|
+
if (!Array.isArray(value)) return void 0;
|
|
517
|
+
return walk(value[token.index], tokens, cursor + 1);
|
|
518
|
+
}
|
|
519
|
+
if (!Array.isArray(value)) return void 0;
|
|
520
|
+
return value.map((entry) => walk(entry, tokens, cursor + 1));
|
|
521
|
+
}
|
|
522
|
+
//#endregion
|
|
523
|
+
//#region src/outputs/contracts.ts
|
|
524
|
+
const output = {
|
|
525
|
+
citations() {
|
|
526
|
+
return { kind: "citations" };
|
|
527
|
+
},
|
|
528
|
+
artifacts(options = {}) {
|
|
529
|
+
return {
|
|
530
|
+
kind: "artifacts",
|
|
531
|
+
...options
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
//#endregion
|
|
536
|
+
//#region src/receipts/keyset.ts
|
|
537
|
+
/**
|
|
538
|
+
* In-memory KeySet factory.
|
|
539
|
+
*
|
|
540
|
+
* Verification flow (plan 09-03):
|
|
541
|
+
* - keySet.lookup(kid) returns undefined → VerifyError {kind: "key-not-found"}
|
|
542
|
+
* - entry.state === "revoked" → VerifyError {kind: "key-revoked"}
|
|
543
|
+
* - entry.state === "retired" → VerifyOk + keyState: "retired" (caller may warn)
|
|
544
|
+
* - entry.state === "active" → VerifyOk + keyState: "active"
|
|
545
|
+
*
|
|
546
|
+
* Duplicate kids: last write wins (deterministic — callers control entry order).
|
|
547
|
+
* Empty entries array is legal — every lookup returns undefined.
|
|
548
|
+
* Returned KeySet exposes only `lookup` — no enumeration.
|
|
549
|
+
*
|
|
550
|
+
* See 09-CONTEXT.md "Key Management (UNRETROFITTABLE)".
|
|
551
|
+
*/
|
|
552
|
+
function createMemoryKeySet(entries) {
|
|
553
|
+
const byKid = /* @__PURE__ */ new Map();
|
|
554
|
+
for (const entry of entries) byKid.set(entry.kid, entry);
|
|
555
|
+
return { lookup(kid) {
|
|
556
|
+
return byKid.get(kid);
|
|
557
|
+
} };
|
|
558
|
+
}
|
|
559
|
+
//#endregion
|
|
560
|
+
//#region src/receipts/sign.ts
|
|
561
|
+
const ALG = "Ed25519";
|
|
562
|
+
/**
|
|
563
|
+
* Copy a Uint8Array into a fresh ArrayBuffer. WebCrypto's BufferSource type
|
|
564
|
+
* (under exactOptionalPropertyTypes + strict TS) rejects `Uint8Array<ArrayBufferLike>`
|
|
565
|
+
* because the underlying buffer could be a SharedArrayBuffer. Matches the
|
|
566
|
+
* pattern used in storage/fingerprint.ts.
|
|
567
|
+
*/
|
|
568
|
+
function toArrayBuffer$1(bytes) {
|
|
569
|
+
const copy = new Uint8Array(bytes.byteLength);
|
|
570
|
+
copy.set(bytes);
|
|
571
|
+
return copy.buffer;
|
|
572
|
+
}
|
|
573
|
+
async function importEd25519PrivateKey(jwk) {
|
|
574
|
+
return crypto.subtle.importKey("jwk", jwk, ALG, true, ["sign"]);
|
|
575
|
+
}
|
|
576
|
+
async function importEd25519PublicKey(jwk) {
|
|
577
|
+
return crypto.subtle.importKey("jwk", jwk, ALG, true, ["verify"]);
|
|
578
|
+
}
|
|
579
|
+
async function generateEd25519KeyPairJwk() {
|
|
580
|
+
const pair = await crypto.subtle.generateKey(ALG, true, ["sign", "verify"]);
|
|
581
|
+
const [privateKeyJwk, publicKeyJwk] = await Promise.all([crypto.subtle.exportKey("jwk", pair.privateKey), crypto.subtle.exportKey("jwk", pair.publicKey)]);
|
|
582
|
+
return {
|
|
583
|
+
privateKeyJwk,
|
|
584
|
+
publicKeyJwk
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
async function verifyEd25519Signature(publicKeyJwk, message, signature) {
|
|
588
|
+
let key;
|
|
589
|
+
try {
|
|
590
|
+
key = await importEd25519PublicKey(publicKeyJwk);
|
|
591
|
+
} catch {
|
|
592
|
+
return false;
|
|
593
|
+
}
|
|
594
|
+
try {
|
|
595
|
+
return await crypto.subtle.verify(ALG, key, toArrayBuffer$1(signature), toArrayBuffer$1(message));
|
|
596
|
+
} catch {
|
|
597
|
+
return false;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
function createInMemorySigner(privateKeyJwk, options) {
|
|
601
|
+
let cachedKey;
|
|
602
|
+
const ensureKey = async () => {
|
|
603
|
+
if (cachedKey === void 0) cachedKey = await importEd25519PrivateKey(privateKeyJwk);
|
|
604
|
+
return cachedKey;
|
|
605
|
+
};
|
|
606
|
+
return {
|
|
607
|
+
kid: options.kid,
|
|
608
|
+
publicKeyJwk: options.publicKeyJwk,
|
|
609
|
+
async sign(bytes) {
|
|
610
|
+
const key = await ensureKey();
|
|
611
|
+
const sig = await crypto.subtle.sign(ALG, key, toArrayBuffer$1(bytes));
|
|
612
|
+
return new Uint8Array(sig);
|
|
613
|
+
}
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
//#endregion
|
|
617
|
+
//#region src/receipts/verify.ts
|
|
618
|
+
function fail$1(kind, message) {
|
|
619
|
+
return {
|
|
620
|
+
ok: false,
|
|
621
|
+
error: {
|
|
622
|
+
kind,
|
|
623
|
+
message
|
|
624
|
+
}
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
function bytesEqual(a, b) {
|
|
628
|
+
if (a.byteLength !== b.byteLength) return false;
|
|
629
|
+
for (let i = 0; i < a.byteLength; i += 1) if (a[i] !== b[i]) return false;
|
|
630
|
+
return true;
|
|
631
|
+
}
|
|
632
|
+
/**
|
|
633
|
+
* Receipt body shape check. We trust the JSON parse — but we re-validate
|
|
634
|
+
* that the required fields exist with the right primitive types before
|
|
635
|
+
* canonicalizing again. Anything off -> version-mismatch (the body is
|
|
636
|
+
* structurally NOT a v1 receipt, even if it parses as JSON).
|
|
637
|
+
*/
|
|
638
|
+
function asReceiptBody(value) {
|
|
639
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
640
|
+
const v = value;
|
|
641
|
+
if (v.version !== void 0 && v.version !== "lattice-receipt/v1" && v.version !== "lattice-receipt/v1.1") return;
|
|
642
|
+
if (typeof v.receiptId !== "string") return void 0;
|
|
643
|
+
if (typeof v.runId !== "string") return void 0;
|
|
644
|
+
if (typeof v.issuedAt !== "string") return void 0;
|
|
645
|
+
if (typeof v.kid !== "string") return void 0;
|
|
646
|
+
if (typeof v.model !== "object" || v.model === null) return void 0;
|
|
647
|
+
if (typeof v.route !== "object" || v.route === null) return void 0;
|
|
648
|
+
if (typeof v.usage !== "object" || v.usage === null) return void 0;
|
|
649
|
+
if (typeof v.contractVerdict !== "string") return void 0;
|
|
650
|
+
if (!Array.isArray(v.inputHashes)) return void 0;
|
|
651
|
+
if (typeof v.redactionPolicyId !== "string") return void 0;
|
|
652
|
+
if (!Array.isArray(v.redactions)) return void 0;
|
|
653
|
+
return v;
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Pure receipt verifier.
|
|
657
|
+
*
|
|
658
|
+
* Returns a typed VerifyResult — never throws across the verification
|
|
659
|
+
* boundary (PITFALLS.md security: "Verifier panics on malformed receipts
|
|
660
|
+
* -> DoS via crafted input"). All parsing failures become typed errors.
|
|
661
|
+
*
|
|
662
|
+
* Decision tree (first match wins):
|
|
663
|
+
* 1. decodeEnvelope throws OR signatures[] empty -> envelope-malformed
|
|
664
|
+
* 2. payload bytes are not valid JSON -> envelope-malformed
|
|
665
|
+
* 3. body shape check fails OR version unknown literal -> version-mismatch
|
|
666
|
+
* 4. body.version === undefined OR "lattice-receipt/v1"-> schema-version-too-low (CRYPTO-01)
|
|
667
|
+
* 5. keySet.lookup(keyid) === undefined -> key-not-found
|
|
668
|
+
* 6. entry.state === "revoked" -> key-revoked
|
|
669
|
+
* 7. re-canonicalized body != signed payloadBytes -> canonicalization-mismatch
|
|
670
|
+
* 8. Ed25519 verification of PAE fails -> signature-invalid
|
|
671
|
+
* 9. body.kid !== entry.kid (defense in depth) -> signature-invalid
|
|
672
|
+
* 10. otherwise -> ok + keyState
|
|
673
|
+
*/
|
|
674
|
+
async function verifyReceipt(envelope, keySet) {
|
|
675
|
+
let decoded;
|
|
676
|
+
try {
|
|
677
|
+
decoded = decodeEnvelope(envelope);
|
|
678
|
+
} catch (error) {
|
|
679
|
+
return fail$1("envelope-malformed", error instanceof Error ? error.message : String(error));
|
|
680
|
+
}
|
|
681
|
+
if (decoded.signatures.length === 0) return fail$1("envelope-malformed", "envelope has no signatures");
|
|
682
|
+
let parsed;
|
|
683
|
+
try {
|
|
684
|
+
parsed = JSON.parse(new TextDecoder().decode(decoded.payloadBytes));
|
|
685
|
+
} catch (error) {
|
|
686
|
+
return fail$1("envelope-malformed", `payload is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
687
|
+
}
|
|
688
|
+
const body = asReceiptBody(parsed);
|
|
689
|
+
if (body === void 0) return fail$1("version-mismatch", "receipt body is not a lattice-receipt/v1 shape");
|
|
690
|
+
if (body.version === void 0 || body.version === "lattice-receipt/v1") return fail$1("schema-version-too-low", "Receipt body.version must be 'lattice-receipt/v1.1' — v1 receipts are not accepted (CRYPTO-01).");
|
|
691
|
+
const firstSig = decoded.signatures[0];
|
|
692
|
+
const entry = keySet.lookup(firstSig.keyid);
|
|
693
|
+
if (entry === void 0) return fail$1("key-not-found", `keySet has no entry for kid "${firstSig.keyid}"`);
|
|
694
|
+
if (entry.state === "revoked") return fail$1("key-revoked", `key "${entry.kid}" is revoked`);
|
|
695
|
+
if (!bytesEqual(canonicalizeReceiptBody(body), decoded.payloadBytes)) return fail$1("canonicalization-mismatch", "re-canonicalized body does not match signed payload bytes");
|
|
696
|
+
const pae = buildPae(PAYLOAD_TYPE, base64Encode(decoded.payloadBytes));
|
|
697
|
+
if (!await verifyEd25519Signature(entry.publicKeyJwk, pae, firstSig.sig)) return fail$1("signature-invalid", "Ed25519 signature does not verify");
|
|
698
|
+
if (body.kid !== entry.kid) return fail$1("signature-invalid", `body.kid "${body.kid}" does not match envelope keyid "${entry.kid}"`);
|
|
699
|
+
return {
|
|
700
|
+
ok: true,
|
|
701
|
+
body,
|
|
702
|
+
keyState: entry.state
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
//#endregion
|
|
706
|
+
//#region src/results/errors.ts
|
|
707
|
+
/**
|
|
708
|
+
* Returns `true` for run errors that MUST NOT be retried by the fallback
|
|
709
|
+
* chain. Phase 8 covers two kinds:
|
|
710
|
+
*
|
|
711
|
+
* - `tripwire-violated` — the contract's invariants rejected the output;
|
|
712
|
+
* a different provider will not change the verdict, so retry burns
|
|
713
|
+
* budget for no gain (T-08-06 in 08-02-PLAN threat register).
|
|
714
|
+
* - `no-contract-match` — no route satisfies the contract at all; the
|
|
715
|
+
* run never executed and no retry will help.
|
|
716
|
+
*
|
|
717
|
+
* All other error kinds return `false` and remain eligible for fallback.
|
|
718
|
+
* The predicate is exported so Phase 12's eval gate and any user-side
|
|
719
|
+
* retry wrappers can share one source of truth.
|
|
720
|
+
*/
|
|
721
|
+
function isTerminal(error) {
|
|
722
|
+
return error.kind === "tripwire-violated" || error.kind === "no-contract-match";
|
|
723
|
+
}
|
|
724
|
+
//#endregion
|
|
725
|
+
//#region src/providers/adapters.ts
|
|
726
|
+
function createOpenAICompatibleProvider(options) {
|
|
727
|
+
const id = options.id ?? "openai-compatible";
|
|
728
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
729
|
+
return {
|
|
730
|
+
id,
|
|
731
|
+
kind: "provider-adapter",
|
|
732
|
+
capabilities: [{
|
|
733
|
+
...defaultCapabilityForProvider(id),
|
|
734
|
+
modelId: options.model,
|
|
735
|
+
fileTransport: [
|
|
736
|
+
"inline",
|
|
737
|
+
"json",
|
|
738
|
+
"url",
|
|
739
|
+
"base64",
|
|
740
|
+
"extracted-text",
|
|
741
|
+
"transcript"
|
|
742
|
+
]
|
|
743
|
+
}],
|
|
744
|
+
async execute(request) {
|
|
745
|
+
const init = {
|
|
746
|
+
method: "POST",
|
|
747
|
+
headers: {
|
|
748
|
+
"content-type": "application/json",
|
|
749
|
+
...options.apiKey !== void 0 ? { authorization: `Bearer ${options.apiKey}` } : {}
|
|
750
|
+
},
|
|
751
|
+
body: JSON.stringify({
|
|
752
|
+
model: options.model,
|
|
753
|
+
messages: [{
|
|
754
|
+
role: "user",
|
|
755
|
+
content: [
|
|
756
|
+
{
|
|
757
|
+
type: "text",
|
|
758
|
+
text: request.task
|
|
759
|
+
},
|
|
760
|
+
{
|
|
761
|
+
type: "text",
|
|
762
|
+
text: JSON.stringify({ contextPack: request.contextPack === void 0 ? void 0 : {
|
|
763
|
+
id: request.contextPack.id,
|
|
764
|
+
tokenBudget: request.contextPack.tokenBudget,
|
|
765
|
+
estimatedTokens: request.contextPack.estimatedTokens,
|
|
766
|
+
included: request.contextPack.included,
|
|
767
|
+
summarized: request.contextPack.summarized,
|
|
768
|
+
archived: request.contextPack.archived,
|
|
769
|
+
omitted: request.contextPack.omitted,
|
|
770
|
+
warnings: request.contextPack.warnings
|
|
771
|
+
} })
|
|
772
|
+
},
|
|
773
|
+
...request.artifacts.map((inputArtifact) => ({
|
|
774
|
+
type: "text",
|
|
775
|
+
text: JSON.stringify({
|
|
776
|
+
artifactId: inputArtifact.id,
|
|
777
|
+
kind: inputArtifact.kind,
|
|
778
|
+
mediaType: inputArtifact.mediaType,
|
|
779
|
+
privacy: inputArtifact.privacy,
|
|
780
|
+
transport: request.providerPackaging?.artifacts.find((item) => item.artifactId === inputArtifact.id)?.transport ?? request.plan?.providerPackaging?.artifacts.find((item) => item.artifactId === inputArtifact.id)?.transport,
|
|
781
|
+
value: typeof inputArtifact.value === "string" && inputArtifact.kind !== "url" ? inputArtifact.value : void 0,
|
|
782
|
+
url: inputArtifact.kind === "url" && typeof inputArtifact.value === "string" ? inputArtifact.value : void 0
|
|
783
|
+
})
|
|
784
|
+
}))
|
|
785
|
+
]
|
|
786
|
+
}]
|
|
787
|
+
}),
|
|
788
|
+
...request.signal !== void 0 ? { signal: request.signal } : {}
|
|
789
|
+
};
|
|
790
|
+
const response = await fetchImpl(`${options.baseUrl.replace(/\/$/u, "")}/chat/completions`, init);
|
|
791
|
+
if (!response.ok) throw new Error(`OpenAI-compatible provider failed with ${response.status}.`);
|
|
792
|
+
const body = await response.json();
|
|
793
|
+
const text = String(body.choices?.[0]?.message?.content ?? "");
|
|
794
|
+
const usage = normalizeUsage(body.usage);
|
|
795
|
+
const normalizedUsage = normalizeUsageToRunUsage(body.usage, options.pricing);
|
|
796
|
+
return {
|
|
797
|
+
rawOutputs: Object.fromEntries(request.outputs.map((name) => [name, text])),
|
|
798
|
+
...usage !== void 0 ? { usage } : {},
|
|
799
|
+
normalizedUsage,
|
|
800
|
+
rawResponse: body
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* Phase 7 normalization: maps raw provider usage payloads (OpenAI's
|
|
807
|
+
* `prompt_tokens`/`completion_tokens`, the Responses API's
|
|
808
|
+
* `input_tokens`/`output_tokens`, or camelCase variants) to the shared
|
|
809
|
+
* `Usage` shape. When `pricing` is supplied, `costUsd` is computed from
|
|
810
|
+
* the normalized token counts. Otherwise `costUsd` is `null` so consumers
|
|
811
|
+
* can distinguish "unmeasured" from "zero".
|
|
812
|
+
*/
|
|
813
|
+
function normalizeUsageToRunUsage(rawUsage, pricing) {
|
|
814
|
+
let promptTokens = 0;
|
|
815
|
+
let completionTokens = 0;
|
|
816
|
+
if (typeof rawUsage === "object" && rawUsage !== null) {
|
|
817
|
+
const record = rawUsage;
|
|
818
|
+
promptTokens = numberField$2(record, "prompt_tokens") ?? numberField$2(record, "input_tokens") ?? numberField$2(record, "inputTokens") ?? 0;
|
|
819
|
+
completionTokens = numberField$2(record, "completion_tokens") ?? numberField$2(record, "output_tokens") ?? numberField$2(record, "outputTokens") ?? 0;
|
|
820
|
+
}
|
|
821
|
+
let costUsd = null;
|
|
822
|
+
if (pricing !== void 0 && (pricing.inputPer1kTokens !== void 0 || pricing.outputPer1kTokens !== void 0)) costUsd = (pricing.inputPer1kTokens ?? 0) * promptTokens / 1e3 + (pricing.outputPer1kTokens ?? 0) * completionTokens / 1e3;
|
|
823
|
+
return {
|
|
824
|
+
promptTokens,
|
|
825
|
+
completionTokens,
|
|
826
|
+
costUsd
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
function normalizeUsage(usage) {
|
|
830
|
+
if (typeof usage !== "object" || usage === null) return;
|
|
831
|
+
const record = usage;
|
|
832
|
+
const inputTokens = numberField$2(record, "prompt_tokens") ?? numberField$2(record, "input_tokens");
|
|
833
|
+
const outputTokens = numberField$2(record, "completion_tokens") ?? numberField$2(record, "output_tokens");
|
|
834
|
+
const totalTokens = numberField$2(record, "total_tokens");
|
|
835
|
+
return {
|
|
836
|
+
...inputTokens !== void 0 ? { inputTokens } : {},
|
|
837
|
+
...outputTokens !== void 0 ? { outputTokens } : {},
|
|
838
|
+
...totalTokens !== void 0 ? { totalTokens } : {}
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
function numberField$2(record, key) {
|
|
842
|
+
const value = record[key];
|
|
843
|
+
return typeof value === "number" ? value : void 0;
|
|
844
|
+
}
|
|
845
|
+
function createOpenAIProvider(options) {
|
|
846
|
+
return createOpenAICompatibleProvider({
|
|
847
|
+
...options,
|
|
848
|
+
id: options.id ?? "openai",
|
|
849
|
+
baseUrl: options.baseUrl
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
function createAISdkProvider(options) {
|
|
853
|
+
const id = options.id ?? "ai-sdk";
|
|
854
|
+
return {
|
|
855
|
+
id,
|
|
856
|
+
kind: "provider-adapter",
|
|
857
|
+
capabilities: [{
|
|
858
|
+
...defaultCapabilityForProvider(id),
|
|
859
|
+
modelId: options.model,
|
|
860
|
+
toolUse: true,
|
|
861
|
+
streaming: true
|
|
862
|
+
}],
|
|
863
|
+
execute: async (request) => {
|
|
864
|
+
const response = await options.generate({
|
|
865
|
+
task: request.task,
|
|
866
|
+
outputNames: request.outputs
|
|
867
|
+
});
|
|
868
|
+
const normalizedUsage = {
|
|
869
|
+
promptTokens: response.usage?.inputTokens ?? 0,
|
|
870
|
+
completionTokens: response.usage?.outputTokens ?? 0,
|
|
871
|
+
costUsd: null
|
|
872
|
+
};
|
|
873
|
+
return {
|
|
874
|
+
...response,
|
|
875
|
+
normalizedUsage
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
//#endregion
|
|
881
|
+
//#region src/providers/anthropic.ts
|
|
882
|
+
const DEFAULT_BASE_URL$1 = "https://api.anthropic.com";
|
|
883
|
+
const DEFAULT_ANTHROPIC_VERSION = "2023-06-01";
|
|
884
|
+
const DEFAULT_MAX_TOKENS = 2e3;
|
|
885
|
+
function createAnthropicProvider(options) {
|
|
886
|
+
const id = options.id ?? "anthropic";
|
|
887
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
888
|
+
const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL$1).replace(/\/$/u, "");
|
|
889
|
+
const anthropicVersion = options.anthropicVersion ?? DEFAULT_ANTHROPIC_VERSION;
|
|
890
|
+
return {
|
|
891
|
+
id,
|
|
892
|
+
kind: "provider-adapter",
|
|
893
|
+
capabilities: [{
|
|
894
|
+
...defaultCapabilityForProvider(id),
|
|
895
|
+
modelId: options.model,
|
|
896
|
+
fileTransport: [
|
|
897
|
+
"inline",
|
|
898
|
+
"json",
|
|
899
|
+
"url",
|
|
900
|
+
"base64",
|
|
901
|
+
"extracted-text",
|
|
902
|
+
"transcript"
|
|
903
|
+
]
|
|
904
|
+
}],
|
|
905
|
+
async execute(request) {
|
|
906
|
+
const init = {
|
|
907
|
+
method: "POST",
|
|
908
|
+
headers: {
|
|
909
|
+
"content-type": "application/json",
|
|
910
|
+
"x-api-key": options.apiKey,
|
|
911
|
+
"anthropic-version": anthropicVersion
|
|
912
|
+
},
|
|
913
|
+
body: JSON.stringify({
|
|
914
|
+
model: options.model,
|
|
915
|
+
system: "",
|
|
916
|
+
messages: [{
|
|
917
|
+
role: "user",
|
|
918
|
+
content: request.task
|
|
919
|
+
}],
|
|
920
|
+
max_tokens: DEFAULT_MAX_TOKENS
|
|
921
|
+
}),
|
|
922
|
+
...request.signal !== void 0 ? { signal: request.signal } : {}
|
|
923
|
+
};
|
|
924
|
+
const response = await fetchImpl(`${baseUrl}/v1/messages`, init);
|
|
925
|
+
if (!response.ok) throw new Error(`Anthropic provider failed with ${response.status}.`);
|
|
926
|
+
const body = await response.json();
|
|
927
|
+
const text = String(body.content?.[0]?.text ?? "");
|
|
928
|
+
const usage = normalizeAnthropicUsage(body.usage);
|
|
929
|
+
const normalizedUsage = normalizeAnthropicUsageToRunUsage(body.usage, options.pricing);
|
|
930
|
+
return {
|
|
931
|
+
rawOutputs: Object.fromEntries(request.outputs.map((name) => [name, text])),
|
|
932
|
+
...usage !== void 0 ? { usage } : {},
|
|
933
|
+
normalizedUsage,
|
|
934
|
+
rawResponse: body
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
/**
|
|
940
|
+
* Anthropic uses `input_tokens` / `output_tokens` (not OpenAI's
|
|
941
|
+
* `prompt_tokens` / `completion_tokens`). This helper maps to Lattice's
|
|
942
|
+
* `Usage` shape and applies pricing when supplied (Phase 7 pattern).
|
|
943
|
+
*/
|
|
944
|
+
function normalizeAnthropicUsageToRunUsage(rawUsage, pricing) {
|
|
945
|
+
let promptTokens = 0;
|
|
946
|
+
let completionTokens = 0;
|
|
947
|
+
if (typeof rawUsage === "object" && rawUsage !== null) {
|
|
948
|
+
const record = rawUsage;
|
|
949
|
+
promptTokens = numberField$1(record, "input_tokens") ?? numberField$1(record, "inputTokens") ?? 0;
|
|
950
|
+
completionTokens = numberField$1(record, "output_tokens") ?? numberField$1(record, "outputTokens") ?? 0;
|
|
951
|
+
}
|
|
952
|
+
let costUsd = null;
|
|
953
|
+
if (pricing !== void 0 && (pricing.inputPer1kTokens !== void 0 || pricing.outputPer1kTokens !== void 0)) costUsd = (pricing.inputPer1kTokens ?? 0) * promptTokens / 1e3 + (pricing.outputPer1kTokens ?? 0) * completionTokens / 1e3;
|
|
954
|
+
return {
|
|
955
|
+
promptTokens,
|
|
956
|
+
completionTokens,
|
|
957
|
+
costUsd
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
function normalizeAnthropicUsage(usage) {
|
|
961
|
+
if (typeof usage !== "object" || usage === null) return;
|
|
962
|
+
const record = usage;
|
|
963
|
+
const inputTokens = numberField$1(record, "input_tokens");
|
|
964
|
+
const outputTokens = numberField$1(record, "output_tokens");
|
|
965
|
+
const totalTokens = inputTokens !== void 0 && outputTokens !== void 0 ? inputTokens + outputTokens : void 0;
|
|
966
|
+
return {
|
|
967
|
+
...inputTokens !== void 0 ? { inputTokens } : {},
|
|
968
|
+
...outputTokens !== void 0 ? { outputTokens } : {},
|
|
969
|
+
...totalTokens !== void 0 ? { totalTokens } : {}
|
|
970
|
+
};
|
|
971
|
+
}
|
|
972
|
+
function numberField$1(record, key) {
|
|
973
|
+
const value = record[key];
|
|
974
|
+
return typeof value === "number" ? value : void 0;
|
|
975
|
+
}
|
|
976
|
+
//#endregion
|
|
977
|
+
//#region src/providers/fake.ts
|
|
978
|
+
const DEFAULT_FAKE_USAGE = {
|
|
979
|
+
promptTokens: 0,
|
|
980
|
+
completionTokens: 0,
|
|
981
|
+
costUsd: null
|
|
982
|
+
};
|
|
983
|
+
function createFakeProvider(options = {}) {
|
|
984
|
+
const id = options.id ?? "fake";
|
|
985
|
+
const modelId = options.modelId ?? `${id}:deterministic`;
|
|
986
|
+
const defaultCapability = {
|
|
987
|
+
...defaultCapabilityForProvider(id),
|
|
988
|
+
modelId,
|
|
989
|
+
inputModalities: [
|
|
990
|
+
"text",
|
|
991
|
+
"json",
|
|
992
|
+
"image",
|
|
993
|
+
"audio",
|
|
994
|
+
"document",
|
|
995
|
+
"file",
|
|
996
|
+
"url",
|
|
997
|
+
"tool"
|
|
998
|
+
],
|
|
999
|
+
outputModalities: ["text", "json"],
|
|
1000
|
+
toolUse: true
|
|
1001
|
+
};
|
|
1002
|
+
return {
|
|
1003
|
+
id,
|
|
1004
|
+
kind: "provider-adapter",
|
|
1005
|
+
capabilities: options.capabilities ?? [defaultCapability],
|
|
1006
|
+
async execute(request) {
|
|
1007
|
+
const baseResponse = typeof options.response === "function" ? await options.response(request) : options.response;
|
|
1008
|
+
if (baseResponse !== void 0) return baseResponse.normalizedUsage !== void 0 ? baseResponse : {
|
|
1009
|
+
...baseResponse,
|
|
1010
|
+
normalizedUsage: { ...DEFAULT_FAKE_USAGE }
|
|
1011
|
+
};
|
|
1012
|
+
return {
|
|
1013
|
+
rawOutputs: Object.fromEntries(request.outputs.map((name) => [name, defaultOutputForName(name)])),
|
|
1014
|
+
...options.artifacts !== void 0 ? { artifactRefs: options.artifacts } : {},
|
|
1015
|
+
normalizedUsage: { ...DEFAULT_FAKE_USAGE }
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
function defaultOutputForName(name) {
|
|
1021
|
+
if (/action|json|data|decision/u.test(name)) return {
|
|
1022
|
+
kind: "clarify",
|
|
1023
|
+
reason: "fake provider default structured response"
|
|
1024
|
+
};
|
|
1025
|
+
if (/citations|evidence/u.test(name)) return [];
|
|
1026
|
+
if (/generated|artifacts/u.test(name)) return [];
|
|
1027
|
+
return `Fake response for ${name}.`;
|
|
1028
|
+
}
|
|
1029
|
+
//#endregion
|
|
1030
|
+
//#region src/providers/gemini.ts
|
|
1031
|
+
const DEFAULT_BASE_URL = "https://generativelanguage.googleapis.com";
|
|
1032
|
+
const DEFAULT_MAX_OUTPUT_TOKENS = 2e3;
|
|
1033
|
+
const DEFAULT_TEMPERATURE = .7;
|
|
1034
|
+
const DEFAULT_TOP_P = .9;
|
|
1035
|
+
/**
|
|
1036
|
+
* 4 HARM_CATEGORY entries at BLOCK_NONE (FSB convention mirrored from
|
|
1037
|
+
* `extension/ai/universal-provider.js:255-272`). If Google restricts
|
|
1038
|
+
* BLOCK_NONE in the future, that is a re-spec concern, not a Phase 4
|
|
1039
|
+
* design defect (CONTEXT.md Specific Ideas note).
|
|
1040
|
+
*/
|
|
1041
|
+
const SAFETY_SETTINGS = [
|
|
1042
|
+
{
|
|
1043
|
+
category: "HARM_CATEGORY_HARASSMENT",
|
|
1044
|
+
threshold: "BLOCK_NONE"
|
|
1045
|
+
},
|
|
1046
|
+
{
|
|
1047
|
+
category: "HARM_CATEGORY_HATE_SPEECH",
|
|
1048
|
+
threshold: "BLOCK_NONE"
|
|
1049
|
+
},
|
|
1050
|
+
{
|
|
1051
|
+
category: "HARM_CATEGORY_SEXUALLY_EXPLICIT",
|
|
1052
|
+
threshold: "BLOCK_NONE"
|
|
1053
|
+
},
|
|
1054
|
+
{
|
|
1055
|
+
category: "HARM_CATEGORY_DANGEROUS_CONTENT",
|
|
1056
|
+
threshold: "BLOCK_NONE"
|
|
1057
|
+
}
|
|
1058
|
+
];
|
|
1059
|
+
function createGeminiProvider(options) {
|
|
1060
|
+
const id = options.id ?? "gemini";
|
|
1061
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
1062
|
+
const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/u, "");
|
|
1063
|
+
return {
|
|
1064
|
+
id,
|
|
1065
|
+
kind: "provider-adapter",
|
|
1066
|
+
capabilities: [{
|
|
1067
|
+
...defaultCapabilityForProvider(id),
|
|
1068
|
+
modelId: options.model,
|
|
1069
|
+
fileTransport: [
|
|
1070
|
+
"inline",
|
|
1071
|
+
"json",
|
|
1072
|
+
"url",
|
|
1073
|
+
"base64",
|
|
1074
|
+
"extracted-text",
|
|
1075
|
+
"transcript"
|
|
1076
|
+
]
|
|
1077
|
+
}],
|
|
1078
|
+
async execute(request) {
|
|
1079
|
+
const init = {
|
|
1080
|
+
method: "POST",
|
|
1081
|
+
headers: { "content-type": "application/json" },
|
|
1082
|
+
body: JSON.stringify({
|
|
1083
|
+
contents: [{
|
|
1084
|
+
role: "user",
|
|
1085
|
+
parts: [{ text: request.task }]
|
|
1086
|
+
}],
|
|
1087
|
+
generationConfig: {
|
|
1088
|
+
temperature: DEFAULT_TEMPERATURE,
|
|
1089
|
+
topP: DEFAULT_TOP_P,
|
|
1090
|
+
maxOutputTokens: DEFAULT_MAX_OUTPUT_TOKENS
|
|
1091
|
+
},
|
|
1092
|
+
safetySettings: SAFETY_SETTINGS
|
|
1093
|
+
}),
|
|
1094
|
+
...request.signal !== void 0 ? { signal: request.signal } : {}
|
|
1095
|
+
};
|
|
1096
|
+
const response = await fetchImpl(`${baseUrl}/v1beta/models/${encodeURIComponent(options.model)}:generateContent?key=${encodeURIComponent(options.apiKey)}`, init);
|
|
1097
|
+
if (!response.ok) throw new Error(`Gemini provider failed with ${response.status}.`);
|
|
1098
|
+
const body = await response.json();
|
|
1099
|
+
if (!Array.isArray(body.candidates) || body.candidates.length === 0) throw new Error("Gemini provider returned no candidates.");
|
|
1100
|
+
const text = String(body.candidates[0]?.content?.parts?.[0]?.text ?? "");
|
|
1101
|
+
const usage = normalizeGeminiUsage(body.usageMetadata);
|
|
1102
|
+
const normalizedUsage = normalizeGeminiUsageToRunUsage(body.usageMetadata, options.pricing);
|
|
1103
|
+
return {
|
|
1104
|
+
rawOutputs: Object.fromEntries(request.outputs.map((name) => [name, text])),
|
|
1105
|
+
...usage !== void 0 ? { usage } : {},
|
|
1106
|
+
normalizedUsage,
|
|
1107
|
+
rawResponse: body
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
/**
|
|
1113
|
+
* Gemini uses `usageMetadata.promptTokenCount` / `candidatesTokenCount` /
|
|
1114
|
+
* `totalTokenCount` (NOT OpenAI's `prompt_tokens` / `completion_tokens`).
|
|
1115
|
+
* This helper maps to Lattice's `Usage` shape and applies pricing when supplied.
|
|
1116
|
+
*/
|
|
1117
|
+
function normalizeGeminiUsageToRunUsage(rawUsage, pricing) {
|
|
1118
|
+
let promptTokens = 0;
|
|
1119
|
+
let completionTokens = 0;
|
|
1120
|
+
if (typeof rawUsage === "object" && rawUsage !== null) {
|
|
1121
|
+
const record = rawUsage;
|
|
1122
|
+
promptTokens = numberField(record, "promptTokenCount") ?? 0;
|
|
1123
|
+
completionTokens = numberField(record, "candidatesTokenCount") ?? 0;
|
|
1124
|
+
}
|
|
1125
|
+
let costUsd = null;
|
|
1126
|
+
if (pricing !== void 0 && (pricing.inputPer1kTokens !== void 0 || pricing.outputPer1kTokens !== void 0)) costUsd = (pricing.inputPer1kTokens ?? 0) * promptTokens / 1e3 + (pricing.outputPer1kTokens ?? 0) * completionTokens / 1e3;
|
|
1127
|
+
return {
|
|
1128
|
+
promptTokens,
|
|
1129
|
+
completionTokens,
|
|
1130
|
+
costUsd
|
|
1131
|
+
};
|
|
1132
|
+
}
|
|
1133
|
+
function normalizeGeminiUsage(usage) {
|
|
1134
|
+
if (typeof usage !== "object" || usage === null) return;
|
|
1135
|
+
const record = usage;
|
|
1136
|
+
const inputTokens = numberField(record, "promptTokenCount");
|
|
1137
|
+
const outputTokens = numberField(record, "candidatesTokenCount");
|
|
1138
|
+
const totalTokens = numberField(record, "totalTokenCount");
|
|
1139
|
+
return {
|
|
1140
|
+
...inputTokens !== void 0 ? { inputTokens } : {},
|
|
1141
|
+
...outputTokens !== void 0 ? { outputTokens } : {},
|
|
1142
|
+
...totalTokens !== void 0 ? { totalTokens } : {}
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
function numberField(record, key) {
|
|
1146
|
+
const value = record[key];
|
|
1147
|
+
return typeof value === "number" ? value : void 0;
|
|
1148
|
+
}
|
|
1149
|
+
//#endregion
|
|
1150
|
+
//#region src/providers/lm-studio.ts
|
|
1151
|
+
const DEFAULT_LM_STUDIO_BASE_URL = "http://localhost:1234/v1";
|
|
1152
|
+
function createLmStudioProvider(options) {
|
|
1153
|
+
return createOpenAICompatibleProvider({
|
|
1154
|
+
...options,
|
|
1155
|
+
id: options.id ?? "lm-studio",
|
|
1156
|
+
baseUrl: options.baseUrl ?? DEFAULT_LM_STUDIO_BASE_URL
|
|
1157
|
+
});
|
|
1158
|
+
}
|
|
1159
|
+
//#endregion
|
|
1160
|
+
//#region src/providers/openrouter.ts
|
|
1161
|
+
const DEFAULT_OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1";
|
|
1162
|
+
function createOpenRouterProvider(options) {
|
|
1163
|
+
return createOpenAICompatibleProvider({
|
|
1164
|
+
...options,
|
|
1165
|
+
id: options.id ?? "openrouter",
|
|
1166
|
+
baseUrl: options.baseUrl ?? DEFAULT_OPENROUTER_BASE_URL
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
//#endregion
|
|
1170
|
+
//#region src/providers/xai.ts
|
|
1171
|
+
const DEFAULT_XAI_BASE_URL = "https://api.x.ai/v1";
|
|
1172
|
+
function createXaiProvider(options) {
|
|
1173
|
+
const inner = createOpenAICompatibleProvider({
|
|
1174
|
+
...options,
|
|
1175
|
+
id: options.id ?? "xai",
|
|
1176
|
+
baseUrl: options.baseUrl ?? DEFAULT_XAI_BASE_URL
|
|
1177
|
+
});
|
|
1178
|
+
const innerExecute = inner.execute;
|
|
1179
|
+
if (innerExecute === void 0) return inner;
|
|
1180
|
+
return {
|
|
1181
|
+
...inner,
|
|
1182
|
+
async execute(request) {
|
|
1183
|
+
const response = await innerExecute(request);
|
|
1184
|
+
const reasoningTokens = response.rawResponse?.usage?.completion_tokens_details?.reasoning_tokens;
|
|
1185
|
+
if (typeof reasoningTokens === "number" && response.usage !== void 0) {
|
|
1186
|
+
const inputTokens = response.usage.inputTokens ?? 0;
|
|
1187
|
+
const outputTokens = response.usage.outputTokens ?? 0;
|
|
1188
|
+
return {
|
|
1189
|
+
...response,
|
|
1190
|
+
usage: {
|
|
1191
|
+
...response.usage,
|
|
1192
|
+
totalTokens: inputTokens + outputTokens + reasoningTokens
|
|
1193
|
+
}
|
|
1194
|
+
};
|
|
1195
|
+
}
|
|
1196
|
+
return response;
|
|
1197
|
+
}
|
|
1198
|
+
};
|
|
1199
|
+
}
|
|
1200
|
+
//#endregion
|
|
1201
|
+
//#region src/plan/plan.ts
|
|
1202
|
+
function createExecutionPlan(input) {
|
|
1203
|
+
const selected = input.route.selected;
|
|
1204
|
+
const status = selected === void 0 ? "no-route" : "planned";
|
|
1205
|
+
const contextWarnings = input.context?.warnings ?? [];
|
|
1206
|
+
const packagingWarnings = input.providerPackaging?.warnings ?? [];
|
|
1207
|
+
const warnings = [
|
|
1208
|
+
...input.warnings ?? [],
|
|
1209
|
+
...contextWarnings,
|
|
1210
|
+
...packagingWarnings,
|
|
1211
|
+
...input.route.noRouteReasons.map((reason) => reason.message)
|
|
1212
|
+
];
|
|
1213
|
+
return {
|
|
1214
|
+
id: createPlanId(),
|
|
1215
|
+
kind: "execution-plan",
|
|
1216
|
+
version: 1,
|
|
1217
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1218
|
+
status,
|
|
1219
|
+
task: input.task,
|
|
1220
|
+
outputNames: Object.keys(input.outputs),
|
|
1221
|
+
artifactRefs: input.artifacts,
|
|
1222
|
+
route: input.route,
|
|
1223
|
+
stages: createDefaultStages(status, input.artifacts, warnings),
|
|
1224
|
+
...input.context !== void 0 ? { context: input.context } : {},
|
|
1225
|
+
...input.providerPackaging !== void 0 ? { providerPackaging: input.providerPackaging } : {},
|
|
1226
|
+
attempts: selected === void 0 ? [] : [{
|
|
1227
|
+
providerId: selected.providerId,
|
|
1228
|
+
modelId: selected.modelId,
|
|
1229
|
+
status: "pending"
|
|
1230
|
+
}],
|
|
1231
|
+
warnings,
|
|
1232
|
+
...input.metadata !== void 0 ? { metadata: input.metadata } : {}
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
1235
|
+
function withPlanStatus(plan, status, updates = {}) {
|
|
1236
|
+
return {
|
|
1237
|
+
...plan,
|
|
1238
|
+
status,
|
|
1239
|
+
...updates.stages !== void 0 ? { stages: updates.stages } : {},
|
|
1240
|
+
...updates.attempts !== void 0 ? { attempts: updates.attempts } : {},
|
|
1241
|
+
...updates.warnings !== void 0 ? { warnings: updates.warnings } : {}
|
|
1242
|
+
};
|
|
1243
|
+
}
|
|
1244
|
+
function markStage(stages, kind, status, metadata) {
|
|
1245
|
+
return stages.map((stage) => stage.kind === kind ? {
|
|
1246
|
+
...stage,
|
|
1247
|
+
status,
|
|
1248
|
+
...metadata !== void 0 ? { metadata: {
|
|
1249
|
+
...stage.metadata,
|
|
1250
|
+
...metadata
|
|
1251
|
+
} } : {}
|
|
1252
|
+
} : stage);
|
|
1253
|
+
}
|
|
1254
|
+
function createDefaultStages(status, artifacts, warnings) {
|
|
1255
|
+
const skipped = status === "no-route";
|
|
1256
|
+
const artifactIds = artifacts.map((artifact) => artifact.id);
|
|
1257
|
+
return [
|
|
1258
|
+
{
|
|
1259
|
+
id: "stage:analysis",
|
|
1260
|
+
kind: "analysis",
|
|
1261
|
+
status: "completed",
|
|
1262
|
+
inputArtifacts: artifactIds,
|
|
1263
|
+
warnings: []
|
|
1264
|
+
},
|
|
1265
|
+
{
|
|
1266
|
+
id: "stage:transforms",
|
|
1267
|
+
kind: "transforms",
|
|
1268
|
+
status: "pending",
|
|
1269
|
+
inputArtifacts: artifactIds,
|
|
1270
|
+
warnings: []
|
|
1271
|
+
},
|
|
1272
|
+
{
|
|
1273
|
+
id: "stage:context-packing",
|
|
1274
|
+
kind: "context-packing",
|
|
1275
|
+
status: "completed",
|
|
1276
|
+
inputArtifacts: artifactIds,
|
|
1277
|
+
warnings: []
|
|
1278
|
+
},
|
|
1279
|
+
{
|
|
1280
|
+
id: "stage:provider-packaging",
|
|
1281
|
+
kind: "provider-packaging",
|
|
1282
|
+
status: skipped ? "skipped" : "completed",
|
|
1283
|
+
inputArtifacts: artifactIds,
|
|
1284
|
+
warnings
|
|
1285
|
+
},
|
|
1286
|
+
{
|
|
1287
|
+
id: "stage:tool-execution",
|
|
1288
|
+
kind: "tool-execution",
|
|
1289
|
+
status: "pending",
|
|
1290
|
+
warnings: []
|
|
1291
|
+
},
|
|
1292
|
+
{
|
|
1293
|
+
id: "stage:execution",
|
|
1294
|
+
kind: "execution",
|
|
1295
|
+
status: skipped ? "skipped" : "pending",
|
|
1296
|
+
warnings: skipped ? warnings : []
|
|
1297
|
+
},
|
|
1298
|
+
{
|
|
1299
|
+
id: "stage:validation",
|
|
1300
|
+
kind: "validation",
|
|
1301
|
+
status: skipped ? "skipped" : "pending",
|
|
1302
|
+
warnings: []
|
|
1303
|
+
},
|
|
1304
|
+
{
|
|
1305
|
+
id: "stage:tripwire",
|
|
1306
|
+
kind: "tripwire",
|
|
1307
|
+
status: skipped ? "skipped" : "pending",
|
|
1308
|
+
warnings: []
|
|
1309
|
+
},
|
|
1310
|
+
{
|
|
1311
|
+
id: "stage:persistence",
|
|
1312
|
+
kind: "persistence",
|
|
1313
|
+
status: "pending",
|
|
1314
|
+
warnings: []
|
|
1315
|
+
}
|
|
1316
|
+
];
|
|
1317
|
+
}
|
|
1318
|
+
function createPlanId() {
|
|
1319
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return `plan:${crypto.randomUUID()}`;
|
|
1320
|
+
return `plan:${Date.now()}:${Math.random().toString(16).slice(2)}`;
|
|
1321
|
+
}
|
|
1322
|
+
//#endregion
|
|
1323
|
+
//#region src/version.ts
|
|
1324
|
+
const latticeVersion = "0.0.0";
|
|
1325
|
+
//#endregion
|
|
1326
|
+
//#region src/replay/materialize.ts
|
|
1327
|
+
function asMaterializationError(value) {
|
|
1328
|
+
return typeof value === "object" && value !== null && typeof value.kind === "string" && typeof value.message === "string";
|
|
1329
|
+
}
|
|
1330
|
+
/** Throwable shape — `instanceof Error` is not required for typed unions, so
|
|
1331
|
+
* the function just throws a plain object literal that matches the
|
|
1332
|
+
* `MaterializationError` shape. Callers pattern-match on `err.kind`. */
|
|
1333
|
+
function fail(kind, message) {
|
|
1334
|
+
return {
|
|
1335
|
+
kind,
|
|
1336
|
+
message
|
|
1337
|
+
};
|
|
1338
|
+
}
|
|
1339
|
+
/**
|
|
1340
|
+
* Pure async function that reconstructs a `ReplayEnvelope` from a receipt.
|
|
1341
|
+
*
|
|
1342
|
+
* Verify-FIRST ordering: `verifyReceipt` runs before `artifactLoader` is
|
|
1343
|
+
* touched. Tampered receipts MUST NOT cause loader side effects.
|
|
1344
|
+
*/
|
|
1345
|
+
async function materializeReplayEnvelope(receipt, options) {
|
|
1346
|
+
const verifyResult = await verifyReceipt(receipt, options.keySet);
|
|
1347
|
+
if (!verifyResult.ok) throw fail(verifyResult.error.kind === "envelope-malformed" ? "envelope-malformed" : "verify-failed", verifyResult.error.message);
|
|
1348
|
+
const body = verifyResult.body;
|
|
1349
|
+
const loadedInputs = [];
|
|
1350
|
+
for (const hash of body.inputHashes) {
|
|
1351
|
+
if (hash === "") continue;
|
|
1352
|
+
try {
|
|
1353
|
+
const input = await options.artifactLoader(hash);
|
|
1354
|
+
loadedInputs.push(input);
|
|
1355
|
+
} catch (error) {
|
|
1356
|
+
throw fail("artifact-load-failed", error instanceof Error ? error.message : asMaterializationError(error) ? error.message : String(error));
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
const artifactRefs = loadedInputs.map(toArtifactRef);
|
|
1360
|
+
const outputsMap = options.outputs !== void 0 ? Object.fromEntries(Object.keys(options.outputs).map((k) => [k, "text"])) : {};
|
|
1361
|
+
const plan = createExecutionPlan({
|
|
1362
|
+
task: options.task ?? "",
|
|
1363
|
+
artifacts: artifactRefs,
|
|
1364
|
+
outputs: outputsMap,
|
|
1365
|
+
route: {
|
|
1366
|
+
catalogVersion: "materialized",
|
|
1367
|
+
selected: {
|
|
1368
|
+
providerId: body.route.providerId,
|
|
1369
|
+
modelId: body.route.capabilityId,
|
|
1370
|
+
score: 0,
|
|
1371
|
+
estimates: {
|
|
1372
|
+
inputTokens: 0,
|
|
1373
|
+
outputTokens: 0
|
|
1374
|
+
},
|
|
1375
|
+
inputModalities: [],
|
|
1376
|
+
outputModalities: [],
|
|
1377
|
+
fileTransport: []
|
|
1378
|
+
},
|
|
1379
|
+
candidates: [],
|
|
1380
|
+
rejected: [],
|
|
1381
|
+
fallbackChain: [],
|
|
1382
|
+
noRouteReasons: []
|
|
1383
|
+
},
|
|
1384
|
+
warnings: [],
|
|
1385
|
+
metadata: {
|
|
1386
|
+
materialized: true,
|
|
1387
|
+
receiptId: body.receiptId,
|
|
1388
|
+
runId: body.runId,
|
|
1389
|
+
contractVerdict: body.contractVerdict,
|
|
1390
|
+
...options.policy !== void 0 ? { policy: { ...options.policy } } : {}
|
|
1391
|
+
}
|
|
1392
|
+
});
|
|
1393
|
+
const usage = {
|
|
1394
|
+
inputTokens: body.usage.promptTokens,
|
|
1395
|
+
outputTokens: body.usage.completionTokens,
|
|
1396
|
+
...body.usage.costUsd !== null ? { costUsd: Number(body.usage.costUsd) } : {}
|
|
1397
|
+
};
|
|
1398
|
+
return {
|
|
1399
|
+
kind: "replay-envelope",
|
|
1400
|
+
version: 1,
|
|
1401
|
+
runtimeVersion: latticeVersion,
|
|
1402
|
+
catalogVersion: "materialized",
|
|
1403
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1404
|
+
plan,
|
|
1405
|
+
artifacts: artifactRefs,
|
|
1406
|
+
...options.outputs !== void 0 ? { outputs: options.outputs } : {},
|
|
1407
|
+
warnings: [],
|
|
1408
|
+
errors: [],
|
|
1409
|
+
usage,
|
|
1410
|
+
events: [],
|
|
1411
|
+
receipt,
|
|
1412
|
+
...options.contract !== void 0 ? { contract: options.contract } : {}
|
|
1413
|
+
};
|
|
1414
|
+
}
|
|
1415
|
+
//#endregion
|
|
1416
|
+
//#region src/replay/replay.ts
|
|
1417
|
+
function createReplayEnvelope(result) {
|
|
1418
|
+
if (result.plan.kind !== "execution-plan") throw new Error("Replay envelopes require an execution plan.");
|
|
1419
|
+
const usage = result.plan.attempts.at(-1)?.usage;
|
|
1420
|
+
return {
|
|
1421
|
+
kind: "replay-envelope",
|
|
1422
|
+
version: 1,
|
|
1423
|
+
runtimeVersion: latticeVersion,
|
|
1424
|
+
catalogVersion: result.plan.route.catalogVersion,
|
|
1425
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1426
|
+
plan: redactPlan(result.plan),
|
|
1427
|
+
artifacts: result.ok ? result.artifacts : result.plan.artifactRefs,
|
|
1428
|
+
...result.ok ? { outputs: result.outputs } : {},
|
|
1429
|
+
warnings: result.plan.warnings,
|
|
1430
|
+
errors: result.ok ? [] : [result.error.message],
|
|
1431
|
+
...usage !== void 0 ? { usage } : {},
|
|
1432
|
+
events: result.events ?? []
|
|
1433
|
+
};
|
|
1434
|
+
}
|
|
1435
|
+
async function replayOffline(envelope) {
|
|
1436
|
+
const replayedUsage = envelopeUsage(envelope);
|
|
1437
|
+
if (envelope.outputs === void 0) return {
|
|
1438
|
+
ok: false,
|
|
1439
|
+
error: {
|
|
1440
|
+
kind: "execution_unavailable",
|
|
1441
|
+
message: "Replay envelope does not contain successful outputs."
|
|
1442
|
+
},
|
|
1443
|
+
usage: replayedUsage,
|
|
1444
|
+
plan: envelope.plan,
|
|
1445
|
+
events: envelope.events
|
|
1446
|
+
};
|
|
1447
|
+
return {
|
|
1448
|
+
ok: true,
|
|
1449
|
+
outputs: envelope.outputs,
|
|
1450
|
+
artifacts: envelope.artifacts,
|
|
1451
|
+
usage: replayedUsage,
|
|
1452
|
+
plan: envelope.plan,
|
|
1453
|
+
events: envelope.events
|
|
1454
|
+
};
|
|
1455
|
+
}
|
|
1456
|
+
function envelopeUsage(envelope) {
|
|
1457
|
+
if (envelope.usage === void 0) return {
|
|
1458
|
+
promptTokens: 0,
|
|
1459
|
+
completionTokens: 0,
|
|
1460
|
+
costUsd: null
|
|
1461
|
+
};
|
|
1462
|
+
return {
|
|
1463
|
+
promptTokens: envelope.usage.inputTokens ?? 0,
|
|
1464
|
+
completionTokens: envelope.usage.outputTokens ?? 0,
|
|
1465
|
+
costUsd: envelope.usage.costUsd ?? null
|
|
1466
|
+
};
|
|
1467
|
+
}
|
|
1468
|
+
async function rerunLive(ai, envelope, intent) {
|
|
1469
|
+
const result = await ai.run(intent);
|
|
1470
|
+
if (result.plan.kind === "execution-plan") return {
|
|
1471
|
+
...result,
|
|
1472
|
+
plan: {
|
|
1473
|
+
...result.plan,
|
|
1474
|
+
warnings: [...result.plan.warnings, `Live rerun of ${envelope.plan.id}: provider behavior, model versions, cost, and latency may differ.`]
|
|
1475
|
+
}
|
|
1476
|
+
};
|
|
1477
|
+
return result;
|
|
1478
|
+
}
|
|
1479
|
+
function redactReplayEnvelope(envelope) {
|
|
1480
|
+
return {
|
|
1481
|
+
...envelope,
|
|
1482
|
+
plan: redactPlan(envelope.plan),
|
|
1483
|
+
artifacts: envelope.artifacts.map(redactArtifactRef),
|
|
1484
|
+
events: envelope.events.map((event) => {
|
|
1485
|
+
const metadata = redactRecord(event.metadata);
|
|
1486
|
+
return {
|
|
1487
|
+
...event,
|
|
1488
|
+
...metadata !== void 0 ? { metadata } : {}
|
|
1489
|
+
};
|
|
1490
|
+
})
|
|
1491
|
+
};
|
|
1492
|
+
}
|
|
1493
|
+
function redactPlan(plan) {
|
|
1494
|
+
return {
|
|
1495
|
+
...plan,
|
|
1496
|
+
task: redactText(plan.task),
|
|
1497
|
+
artifactRefs: plan.artifactRefs.map(redactArtifactRef),
|
|
1498
|
+
...plan.providerPackaging !== void 0 ? { providerPackaging: {
|
|
1499
|
+
...plan.providerPackaging,
|
|
1500
|
+
artifacts: plan.providerPackaging.artifacts.map((item) => ({
|
|
1501
|
+
...item,
|
|
1502
|
+
warnings: item.warnings.map(redactText)
|
|
1503
|
+
})),
|
|
1504
|
+
warnings: plan.providerPackaging.warnings.map(redactText)
|
|
1505
|
+
} } : {},
|
|
1506
|
+
warnings: plan.warnings.map(redactText)
|
|
1507
|
+
};
|
|
1508
|
+
}
|
|
1509
|
+
function redactArtifactRef(ref) {
|
|
1510
|
+
const redactedMetadata = redactRecord(ref.metadata);
|
|
1511
|
+
return {
|
|
1512
|
+
...ref,
|
|
1513
|
+
...redactedMetadata !== void 0 ? { metadata: redactedMetadata } : {},
|
|
1514
|
+
...ref.source === "url" ? { metadata: {
|
|
1515
|
+
...redactedMetadata,
|
|
1516
|
+
redactedSource: "url"
|
|
1517
|
+
} } : {}
|
|
1518
|
+
};
|
|
1519
|
+
}
|
|
1520
|
+
function redactRecord(record) {
|
|
1521
|
+
if (record === void 0) return;
|
|
1522
|
+
return Object.fromEntries(Object.entries(record).map(([key, value]) => [key, shouldRedactKey(key) ? "[redacted]" : redactValue(value)]));
|
|
1523
|
+
}
|
|
1524
|
+
function redactValue(value) {
|
|
1525
|
+
if (typeof value === "string") return redactText(value);
|
|
1526
|
+
if (Array.isArray(value)) return value.map(redactValue);
|
|
1527
|
+
if (typeof value === "object" && value !== null) return redactRecord(value);
|
|
1528
|
+
return value;
|
|
1529
|
+
}
|
|
1530
|
+
function redactText(value) {
|
|
1531
|
+
return value.replace(/Bearer\s+[A-Za-z0-9._-]+/gu, "Bearer [redacted]").replace(/https?:\/\/[^\s)]+/gu, "[redacted-url]").replace(/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/gu, "[redacted-email]");
|
|
1532
|
+
}
|
|
1533
|
+
function shouldRedactKey(key) {
|
|
1534
|
+
return /api.?key|authorization|token|secret|password|credential|signed.?url|raw|body|transcript/iu.test(key);
|
|
1535
|
+
}
|
|
1536
|
+
//#endregion
|
|
1537
|
+
//#region src/agent/infra/cost-tracker.ts
|
|
1538
|
+
const WARNING_THRESHOLD = .8;
|
|
1539
|
+
function createCostTracker() {
|
|
1540
|
+
let promptTokens = 0;
|
|
1541
|
+
let completionTokens = 0;
|
|
1542
|
+
let costUsd = null;
|
|
1543
|
+
return {
|
|
1544
|
+
kind: "cost-tracker",
|
|
1545
|
+
recordIteration(usage) {
|
|
1546
|
+
promptTokens += usage.promptTokens;
|
|
1547
|
+
completionTokens += usage.completionTokens;
|
|
1548
|
+
if (usage.costUsd !== null) costUsd = (costUsd ?? 0) + usage.costUsd;
|
|
1549
|
+
},
|
|
1550
|
+
total() {
|
|
1551
|
+
return {
|
|
1552
|
+
promptTokens,
|
|
1553
|
+
completionTokens,
|
|
1554
|
+
costUsd
|
|
1555
|
+
};
|
|
1556
|
+
},
|
|
1557
|
+
budgetStatus(budget) {
|
|
1558
|
+
const max = budget?.maxCostUsd;
|
|
1559
|
+
if (max === void 0 || costUsd === null) return "ok";
|
|
1560
|
+
if (costUsd >= max) return "exceeded";
|
|
1561
|
+
if (costUsd >= max * WARNING_THRESHOLD) return "warning";
|
|
1562
|
+
return "ok";
|
|
1563
|
+
}
|
|
1564
|
+
};
|
|
1565
|
+
}
|
|
1566
|
+
//#endregion
|
|
1567
|
+
//#region src/agent/infra/transcript-store.ts
|
|
1568
|
+
const DEFAULT_TOKEN_ESTIMATOR = (text) => Math.ceil(text.length / 4);
|
|
1569
|
+
function createTranscriptStore() {
|
|
1570
|
+
const turns = [];
|
|
1571
|
+
function firstUserTurn() {
|
|
1572
|
+
for (const turn of turns) if (turn.role === "user") return turn;
|
|
1573
|
+
return null;
|
|
1574
|
+
}
|
|
1575
|
+
return {
|
|
1576
|
+
kind: "transcript-store",
|
|
1577
|
+
append(turn) {
|
|
1578
|
+
turns.push(turn);
|
|
1579
|
+
},
|
|
1580
|
+
all() {
|
|
1581
|
+
return Object.freeze([...turns]);
|
|
1582
|
+
},
|
|
1583
|
+
tail(limit) {
|
|
1584
|
+
if (limit <= 0) return Object.freeze([]);
|
|
1585
|
+
if (turns.length <= limit) return Object.freeze([...turns]);
|
|
1586
|
+
const start = turns.length - limit;
|
|
1587
|
+
const tail = turns.slice(start);
|
|
1588
|
+
const first = firstUserTurn();
|
|
1589
|
+
if (first === null || tail.includes(first)) return Object.freeze(tail);
|
|
1590
|
+
return Object.freeze([first, ...tail]);
|
|
1591
|
+
},
|
|
1592
|
+
tailByTokens(maxTokens, estimator = DEFAULT_TOKEN_ESTIMATOR) {
|
|
1593
|
+
if (maxTokens <= 0) return Object.freeze([]);
|
|
1594
|
+
const reversed = [...turns].reverse();
|
|
1595
|
+
const selected = [];
|
|
1596
|
+
let used = 0;
|
|
1597
|
+
for (const turn of reversed) {
|
|
1598
|
+
const cost = estimator(turn.content);
|
|
1599
|
+
if (used + cost > maxTokens) break;
|
|
1600
|
+
selected.unshift(turn);
|
|
1601
|
+
used += cost;
|
|
1602
|
+
}
|
|
1603
|
+
const first = firstUserTurn();
|
|
1604
|
+
if (first !== null && !selected.includes(first)) selected.unshift(first);
|
|
1605
|
+
return Object.freeze(selected);
|
|
1606
|
+
}
|
|
1607
|
+
};
|
|
1608
|
+
}
|
|
1609
|
+
//#endregion
|
|
1610
|
+
//#region src/agent/infra/goal-progress.ts
|
|
1611
|
+
function createGoalProgressTracker(options = {}) {
|
|
1612
|
+
const windowSize = options.windowSize ?? 3;
|
|
1613
|
+
const stallThreshold = options.stallThreshold ?? .02;
|
|
1614
|
+
const regressionThreshold = options.regressionThreshold ?? .1;
|
|
1615
|
+
const steps = [];
|
|
1616
|
+
return {
|
|
1617
|
+
kind: "goal-progress-tracker",
|
|
1618
|
+
recordStep(step) {
|
|
1619
|
+
steps.push(step);
|
|
1620
|
+
},
|
|
1621
|
+
status() {
|
|
1622
|
+
if (steps.length < windowSize) return "progressing";
|
|
1623
|
+
const window = steps.slice(-windowSize);
|
|
1624
|
+
const latest = window[window.length - 1];
|
|
1625
|
+
const earlierMax = steps.slice(0, -1).reduce((m, s) => s.goalSatisfaction > m ? s.goalSatisfaction : m, -Infinity);
|
|
1626
|
+
if (latest.goalSatisfaction < earlierMax - regressionThreshold) return "regressed";
|
|
1627
|
+
const min = window.reduce((m, s) => s.goalSatisfaction < m ? s.goalSatisfaction : m, Infinity);
|
|
1628
|
+
if (window.reduce((m, s) => s.goalSatisfaction > m ? s.goalSatisfaction : m, -Infinity) - min <= stallThreshold) return "stalled";
|
|
1629
|
+
return "progressing";
|
|
1630
|
+
}
|
|
1631
|
+
};
|
|
1632
|
+
}
|
|
1633
|
+
//#endregion
|
|
1634
|
+
//#region src/agent/infra/action-history.ts
|
|
1635
|
+
/**
|
|
1636
|
+
* ActionHistory — Phase 21 (v1.2).
|
|
1637
|
+
*
|
|
1638
|
+
* Detects stuck patterns in the agent loop's tool-call sequence:
|
|
1639
|
+
* - "consecutive-identical-tool-call" — same (toolName, argsHash) N+ times in a row
|
|
1640
|
+
* - "ping-pong" — last 4 records alternate between 2 distinct (toolName, argsHash) pairs
|
|
1641
|
+
* - "no-progress" — reserved for callers wiring goal-progress feedback here
|
|
1642
|
+
*
|
|
1643
|
+
* Standalone (no dependency on the agent runtime); callers register the
|
|
1644
|
+
* primitive externally and pump `recordAction` from their loop or hook.
|
|
1645
|
+
*/
|
|
1646
|
+
const STUCK_REASONS = [
|
|
1647
|
+
"consecutive-identical-tool-call",
|
|
1648
|
+
"no-progress",
|
|
1649
|
+
"ping-pong"
|
|
1650
|
+
];
|
|
1651
|
+
function actionKey(record) {
|
|
1652
|
+
return `${record.toolName}::${record.argsHash}`;
|
|
1653
|
+
}
|
|
1654
|
+
function createActionHistory(options = {}) {
|
|
1655
|
+
const consecutiveLimit = options.consecutiveLimit ?? 3;
|
|
1656
|
+
const records = [];
|
|
1657
|
+
return {
|
|
1658
|
+
kind: "action-history",
|
|
1659
|
+
recordAction(action) {
|
|
1660
|
+
records.push(action);
|
|
1661
|
+
if (records.length >= consecutiveLimit) {
|
|
1662
|
+
const tail = records.slice(-consecutiveLimit);
|
|
1663
|
+
const firstKey = actionKey(tail[0]);
|
|
1664
|
+
if (tail.every((r) => actionKey(r) === firstKey)) return "consecutive-identical-tool-call";
|
|
1665
|
+
}
|
|
1666
|
+
if (records.length >= 4) {
|
|
1667
|
+
const keys = records.slice(-4).map(actionKey);
|
|
1668
|
+
if (Array.from(new Set(keys)).length === 2 && keys[0] === keys[2] && keys[1] === keys[3] && keys[0] !== keys[1]) return "ping-pong";
|
|
1669
|
+
}
|
|
1670
|
+
return null;
|
|
1671
|
+
},
|
|
1672
|
+
history() {
|
|
1673
|
+
return Object.freeze([...records]);
|
|
1674
|
+
}
|
|
1675
|
+
};
|
|
1676
|
+
}
|
|
1677
|
+
//#endregion
|
|
1678
|
+
//#region src/agent/infra/permission-context.ts
|
|
1679
|
+
/**
|
|
1680
|
+
* PermissionContext — Phase 21 (v1.2).
|
|
1681
|
+
*
|
|
1682
|
+
* Gates tool execution per-tool / per-iteration / per-resource. Includes
|
|
1683
|
+
* a SAFETY-band hook helper that wires the context into the agent loop's
|
|
1684
|
+
* BEFORE_TOOL pipeline via the Phase 19 `controls.deny(reason)` veto.
|
|
1685
|
+
*/
|
|
1686
|
+
function matches(matcher, value) {
|
|
1687
|
+
if (matcher === void 0) return true;
|
|
1688
|
+
if (value === void 0) return false;
|
|
1689
|
+
if (typeof matcher === "string") return matcher === value;
|
|
1690
|
+
return matcher.test(value);
|
|
1691
|
+
}
|
|
1692
|
+
function createPermissionContext(rules) {
|
|
1693
|
+
return {
|
|
1694
|
+
kind: "permission-context",
|
|
1695
|
+
decide(input) {
|
|
1696
|
+
for (const rule of rules) {
|
|
1697
|
+
if (!matches(rule.toolName, input.toolName)) continue;
|
|
1698
|
+
if (rule.resource !== void 0 && !matches(rule.resource, input.resource)) continue;
|
|
1699
|
+
if (rule.verdict === "allow") return { allow: true };
|
|
1700
|
+
return {
|
|
1701
|
+
allow: false,
|
|
1702
|
+
reason: rule.reason ?? `denied by permission rule for ${input.toolName}`
|
|
1703
|
+
};
|
|
1704
|
+
}
|
|
1705
|
+
return { allow: true };
|
|
1706
|
+
}
|
|
1707
|
+
};
|
|
1708
|
+
}
|
|
1709
|
+
function createPermissionGuardHook(context) {
|
|
1710
|
+
return (ctx, controls) => {
|
|
1711
|
+
const verdict = context.decide({
|
|
1712
|
+
iterationIndex: ctx.iterationIndex,
|
|
1713
|
+
toolName: ctx.toolName,
|
|
1714
|
+
...ctx.resource !== void 0 ? { resource: ctx.resource } : {},
|
|
1715
|
+
...ctx.args !== void 0 ? { args: ctx.args } : {}
|
|
1716
|
+
});
|
|
1717
|
+
if (!verdict.allow) controls?.deny(verdict.reason);
|
|
1718
|
+
};
|
|
1719
|
+
}
|
|
1720
|
+
/**
|
|
1721
|
+
* Convenience: returns RegisterOptions for the SAFETY-band registration.
|
|
1722
|
+
* Callers do `pipeline.register("BEFORE_TOOL", hook, permissionGuardRegisterOptions())`.
|
|
1723
|
+
*/
|
|
1724
|
+
function permissionGuardRegisterOptions() {
|
|
1725
|
+
return { band: BAND.SAFETY };
|
|
1726
|
+
}
|
|
1727
|
+
//#endregion
|
|
1728
|
+
//#region src/agent/eval.ts
|
|
1729
|
+
function evalAgentRun(baseline, current, options = {}) {
|
|
1730
|
+
const iterLimit = options.iterationsToGoalRegressionLimit ?? 1;
|
|
1731
|
+
const costLimit = options.costUsdRegressionLimit ?? .1;
|
|
1732
|
+
const regressions = [];
|
|
1733
|
+
const iterDelta = current.iterationsToGoal - baseline.iterationsToGoal;
|
|
1734
|
+
if (iterDelta > iterLimit) regressions.push({
|
|
1735
|
+
kind: "iterations-to-goal",
|
|
1736
|
+
baseline: baseline.iterationsToGoal,
|
|
1737
|
+
current: current.iterationsToGoal,
|
|
1738
|
+
limit: iterLimit,
|
|
1739
|
+
message: `Iterations-to-goal ${current.iterationsToGoal} exceeds baseline ${baseline.iterationsToGoal} by ${iterDelta} (limit: ${iterLimit}).`
|
|
1740
|
+
});
|
|
1741
|
+
const bCost = baseline.usage.costUsd;
|
|
1742
|
+
const cCost = current.usage.costUsd;
|
|
1743
|
+
if (bCost === null && cCost === null) {} else if (bCost === null || cCost === null) regressions.push({
|
|
1744
|
+
kind: "mixed-cost-unknown",
|
|
1745
|
+
baseline: bCost,
|
|
1746
|
+
current: cCost,
|
|
1747
|
+
limit: costLimit,
|
|
1748
|
+
message: `Cost mixed: baseline=${bCost} current=${cCost}; cannot compare regression.`
|
|
1749
|
+
});
|
|
1750
|
+
else if (bCost > 0) {
|
|
1751
|
+
const ratio = (cCost - bCost) / bCost;
|
|
1752
|
+
if (ratio > costLimit) regressions.push({
|
|
1753
|
+
kind: "cost-regression",
|
|
1754
|
+
baseline: bCost,
|
|
1755
|
+
current: cCost,
|
|
1756
|
+
limit: costLimit,
|
|
1757
|
+
message: `Cost regression: $${cCost.toFixed(6)} vs baseline $${bCost.toFixed(6)} (+${(ratio * 100).toFixed(1)}%; limit ${(costLimit * 100).toFixed(1)}%).`
|
|
1758
|
+
});
|
|
1759
|
+
} else if (bCost === 0 && cCost > 0) regressions.push({
|
|
1760
|
+
kind: "cost-regression",
|
|
1761
|
+
baseline: bCost,
|
|
1762
|
+
current: cCost,
|
|
1763
|
+
limit: costLimit,
|
|
1764
|
+
message: `Cost regression: baseline was $0; current $${cCost.toFixed(6)}.`
|
|
1765
|
+
});
|
|
1766
|
+
return {
|
|
1767
|
+
ok: regressions.length === 0,
|
|
1768
|
+
regressions: Object.freeze(regressions)
|
|
1769
|
+
};
|
|
1770
|
+
}
|
|
1771
|
+
//#endregion
|
|
1772
|
+
//#region src/context/context-pack.ts
|
|
1773
|
+
function buildContextPack(input) {
|
|
1774
|
+
const routeBudget = input.route?.estimates.inputTokens ?? input.route?.inputModalities.length ?? 0;
|
|
1775
|
+
const tokenBudget = input.tokenBudget ?? Math.max(512, Math.min(input.route?.estimates.inputTokens ?? 4e3, 16e3));
|
|
1776
|
+
const remainingBudget = Math.max(512, tokenBudget - estimateTokens(input.task) - routeBudget);
|
|
1777
|
+
const included = [];
|
|
1778
|
+
const summarized = [];
|
|
1779
|
+
const archived = [];
|
|
1780
|
+
const omitted = [];
|
|
1781
|
+
const warnings = [];
|
|
1782
|
+
let usedTokens = 0;
|
|
1783
|
+
for (const artifact of input.artifacts) {
|
|
1784
|
+
const artifactTokens = estimateArtifactTokens(artifact);
|
|
1785
|
+
const item = {
|
|
1786
|
+
artifactId: artifact.id,
|
|
1787
|
+
reason: "Run artifact included for provider consideration.",
|
|
1788
|
+
estimatedTokens: artifactTokens,
|
|
1789
|
+
trust: trustForArtifact(artifact)
|
|
1790
|
+
};
|
|
1791
|
+
if (usedTokens + artifactTokens <= remainingBudget) {
|
|
1792
|
+
included.push(item);
|
|
1793
|
+
usedTokens += artifactTokens;
|
|
1794
|
+
continue;
|
|
1795
|
+
}
|
|
1796
|
+
if (artifact.kind === "text" || artifact.kind === "document" || artifact.kind === "json") {
|
|
1797
|
+
summarized.push({
|
|
1798
|
+
...item,
|
|
1799
|
+
reason: "Artifact exceeded live context budget and needs summary packaging."
|
|
1800
|
+
});
|
|
1801
|
+
usedTokens += Math.min(artifactTokens, 256);
|
|
1802
|
+
continue;
|
|
1803
|
+
}
|
|
1804
|
+
omitted.push({
|
|
1805
|
+
...item,
|
|
1806
|
+
reason: "Artifact exceeded context budget and cannot be summarized by the default packer."
|
|
1807
|
+
});
|
|
1808
|
+
warnings.push(`Artifact ${artifact.id} omitted from live context budget.`);
|
|
1809
|
+
}
|
|
1810
|
+
for (const turn of input.session?.turns ?? []) {
|
|
1811
|
+
const turnTokens = estimateTokens(turn.task);
|
|
1812
|
+
const item = {
|
|
1813
|
+
sessionTurnId: turn.id,
|
|
1814
|
+
reason: "Prior session turn retained for continuity.",
|
|
1815
|
+
estimatedTokens: turnTokens,
|
|
1816
|
+
trust: "user"
|
|
1817
|
+
};
|
|
1818
|
+
if (usedTokens + turnTokens <= remainingBudget) {
|
|
1819
|
+
included.push(item);
|
|
1820
|
+
usedTokens += turnTokens;
|
|
1821
|
+
} else archived.push({
|
|
1822
|
+
...item,
|
|
1823
|
+
reason: "Prior session turn archived because the run budget was exhausted."
|
|
1824
|
+
});
|
|
1825
|
+
}
|
|
1826
|
+
return {
|
|
1827
|
+
id: createContextPackId(),
|
|
1828
|
+
kind: "context-pack",
|
|
1829
|
+
tokenBudget,
|
|
1830
|
+
estimatedTokens: usedTokens,
|
|
1831
|
+
included,
|
|
1832
|
+
summarized,
|
|
1833
|
+
archived,
|
|
1834
|
+
omitted,
|
|
1835
|
+
warnings
|
|
1836
|
+
};
|
|
1837
|
+
}
|
|
1838
|
+
function estimateArtifactTokens(artifact) {
|
|
1839
|
+
if (artifact.size?.characters !== void 0) return estimateTokensFromCharacters(artifact.size.characters);
|
|
1840
|
+
if (artifact.size?.bytes !== void 0) return estimateTokensFromCharacters(Math.ceil(artifact.size.bytes / 2));
|
|
1841
|
+
if ("value" in artifact && typeof artifact.value === "string") return estimateTokens(artifact.value);
|
|
1842
|
+
if ("value" in artifact && artifact.value !== void 0) {
|
|
1843
|
+
const serialized = JSON.stringify(artifact.value);
|
|
1844
|
+
return serialized === void 0 ? 64 : estimateTokens(serialized);
|
|
1845
|
+
}
|
|
1846
|
+
return 64;
|
|
1847
|
+
}
|
|
1848
|
+
function estimateTokens(value) {
|
|
1849
|
+
return Math.max(1, estimateTokensFromCharacters(value.length));
|
|
1850
|
+
}
|
|
1851
|
+
function estimateTokensFromCharacters(characters) {
|
|
1852
|
+
return Math.ceil(characters / 4);
|
|
1853
|
+
}
|
|
1854
|
+
function trustForArtifact(artifact) {
|
|
1855
|
+
if (artifact.source === "tool") return "tool";
|
|
1856
|
+
if (artifact.source === "generated") return "model-summary";
|
|
1857
|
+
return "user";
|
|
1858
|
+
}
|
|
1859
|
+
function createContextPackId() {
|
|
1860
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return `context-pack:${crypto.randomUUID()}`;
|
|
1861
|
+
return `context-pack:${Date.now()}:${Math.random().toString(16).slice(2)}`;
|
|
1862
|
+
}
|
|
1863
|
+
//#endregion
|
|
1864
|
+
//#region src/policy/policy.ts
|
|
1865
|
+
function mergePolicy(defaultPolicy, runPolicy) {
|
|
1866
|
+
if (defaultPolicy === void 0 && runPolicy === void 0) return;
|
|
1867
|
+
return {
|
|
1868
|
+
...defaultPolicy,
|
|
1869
|
+
...runPolicy
|
|
1870
|
+
};
|
|
1871
|
+
}
|
|
1872
|
+
//#endregion
|
|
1873
|
+
//#region src/providers/packaging.ts
|
|
1874
|
+
function packageArtifactsForProvider(input) {
|
|
1875
|
+
const route = input.route;
|
|
1876
|
+
if (route === void 0) return {
|
|
1877
|
+
plan: {
|
|
1878
|
+
providerId: "none",
|
|
1879
|
+
modelId: "none",
|
|
1880
|
+
artifacts: [],
|
|
1881
|
+
warnings: ["No selected route; provider packaging skipped."]
|
|
1882
|
+
},
|
|
1883
|
+
packagedArtifacts: [],
|
|
1884
|
+
blocked: []
|
|
1885
|
+
};
|
|
1886
|
+
const packaged = [];
|
|
1887
|
+
const packagedArtifacts = [];
|
|
1888
|
+
const warnings = [];
|
|
1889
|
+
const blocked = [];
|
|
1890
|
+
for (const inputArtifact of input.artifacts) {
|
|
1891
|
+
const choice = chooseTransport(inputArtifact, route.fileTransport, input.policy);
|
|
1892
|
+
if (choice.blocked !== void 0) {
|
|
1893
|
+
blocked.push(choice.blocked);
|
|
1894
|
+
warnings.push(choice.blocked);
|
|
1895
|
+
continue;
|
|
1896
|
+
}
|
|
1897
|
+
packaged.push({
|
|
1898
|
+
artifactId: inputArtifact.id,
|
|
1899
|
+
transport: choice.transport,
|
|
1900
|
+
...inputArtifact.mediaType !== void 0 ? { mediaType: inputArtifact.mediaType } : {},
|
|
1901
|
+
lineageTransform: "provider-packaging",
|
|
1902
|
+
warnings: choice.warnings
|
|
1903
|
+
});
|
|
1904
|
+
packagedArtifacts.push(toArtifactRef(artifact.derive({
|
|
1905
|
+
id: `${inputArtifact.id}:packaged:${route.providerId}:${route.modelId}`,
|
|
1906
|
+
kind: inputArtifact.kind,
|
|
1907
|
+
source: choice.transport === "provider-upload" ? "provider-upload" : "generated",
|
|
1908
|
+
parents: [inputArtifact],
|
|
1909
|
+
transform: {
|
|
1910
|
+
kind: "provider-packaging",
|
|
1911
|
+
name: `${route.providerId}:${choice.transport}`,
|
|
1912
|
+
metadata: {
|
|
1913
|
+
providerId: route.providerId,
|
|
1914
|
+
modelId: route.modelId,
|
|
1915
|
+
transport: choice.transport
|
|
1916
|
+
}
|
|
1917
|
+
},
|
|
1918
|
+
metadata: {
|
|
1919
|
+
providerId: route.providerId,
|
|
1920
|
+
modelId: route.modelId,
|
|
1921
|
+
transport: choice.transport
|
|
1922
|
+
},
|
|
1923
|
+
...inputArtifact.mediaType !== void 0 ? { mediaType: inputArtifact.mediaType } : {},
|
|
1924
|
+
privacy: inputArtifact.privacy
|
|
1925
|
+
})));
|
|
1926
|
+
}
|
|
1927
|
+
return {
|
|
1928
|
+
plan: {
|
|
1929
|
+
providerId: route.providerId,
|
|
1930
|
+
modelId: route.modelId,
|
|
1931
|
+
artifacts: packaged,
|
|
1932
|
+
warnings
|
|
1933
|
+
},
|
|
1934
|
+
packagedArtifacts,
|
|
1935
|
+
blocked
|
|
1936
|
+
};
|
|
1937
|
+
}
|
|
1938
|
+
function chooseTransport(inputArtifact, supported, policy) {
|
|
1939
|
+
const warnings = [];
|
|
1940
|
+
const preferred = preferredTransports(inputArtifact);
|
|
1941
|
+
for (const transport of preferred) {
|
|
1942
|
+
if (!supported.includes(transport)) continue;
|
|
1943
|
+
if (policy?.noUpload === true && transport === "provider-upload") continue;
|
|
1944
|
+
if (policy?.noPublicUrl === true && transport === "url") continue;
|
|
1945
|
+
if (inputArtifact.privacy === "restricted" && (transport === "provider-upload" || transport === "url" || transport === "base64")) continue;
|
|
1946
|
+
if (transport === "base64") warnings.push(`Artifact ${inputArtifact.id} will be encoded as base64.`);
|
|
1947
|
+
return {
|
|
1948
|
+
transport,
|
|
1949
|
+
warnings
|
|
1950
|
+
};
|
|
1951
|
+
}
|
|
1952
|
+
return {
|
|
1953
|
+
transport: "inline",
|
|
1954
|
+
warnings,
|
|
1955
|
+
blocked: `No policy-safe transport for artifact ${inputArtifact.id}.`
|
|
1956
|
+
};
|
|
1957
|
+
}
|
|
1958
|
+
function preferredTransports(inputArtifact) {
|
|
1959
|
+
switch (inputArtifact.kind) {
|
|
1960
|
+
case "text": return ["inline", "extracted-text"];
|
|
1961
|
+
case "json":
|
|
1962
|
+
case "tool-result": return ["json", "inline"];
|
|
1963
|
+
case "url": return ["url", "inline"];
|
|
1964
|
+
case "document": return [
|
|
1965
|
+
"extracted-text",
|
|
1966
|
+
"provider-upload",
|
|
1967
|
+
"base64",
|
|
1968
|
+
"url"
|
|
1969
|
+
];
|
|
1970
|
+
case "audio": return [
|
|
1971
|
+
"transcript",
|
|
1972
|
+
"provider-upload",
|
|
1973
|
+
"base64",
|
|
1974
|
+
"url"
|
|
1975
|
+
];
|
|
1976
|
+
case "image":
|
|
1977
|
+
case "file":
|
|
1978
|
+
case "video": return [
|
|
1979
|
+
"provider-upload",
|
|
1980
|
+
"base64",
|
|
1981
|
+
"url"
|
|
1982
|
+
];
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
//#endregion
|
|
1986
|
+
//#region src/routing/router.ts
|
|
1987
|
+
function routeDeterministically(catalog, request) {
|
|
1988
|
+
const requiredInputs = requiredInputModalities(request.artifacts);
|
|
1989
|
+
const requiredOutputs = requiredOutputModalities(request.outputs);
|
|
1990
|
+
const requiresStructuredOutput = outputRequiresStructuredOutput(request.outputs);
|
|
1991
|
+
const estimatedInputTokens = estimateTokens(request.task) + request.artifacts.reduce((total, artifact) => total + estimateArtifactTokens(artifact), 0);
|
|
1992
|
+
const candidates = catalog.models.map((capability, index) => evaluateCapability(capability, {
|
|
1993
|
+
requiredInputs,
|
|
1994
|
+
requiredOutputs,
|
|
1995
|
+
requiresStructuredOutput,
|
|
1996
|
+
estimatedInputTokens,
|
|
1997
|
+
...request.policy !== void 0 ? { policy: request.policy } : {},
|
|
1998
|
+
...request.provider !== void 0 ? { provider: request.provider } : {},
|
|
1999
|
+
...request.model !== void 0 ? { model: request.model } : {},
|
|
2000
|
+
...request.contract !== void 0 ? { contract: request.contract } : {},
|
|
2001
|
+
index
|
|
2002
|
+
})).sort(compareCandidates);
|
|
2003
|
+
const accepted = candidates.filter((candidate) => candidate.accepted);
|
|
2004
|
+
const selected = accepted[0];
|
|
2005
|
+
return {
|
|
2006
|
+
catalogVersion: catalog.version,
|
|
2007
|
+
...selected !== void 0 ? { selected: {
|
|
2008
|
+
providerId: selected.providerId,
|
|
2009
|
+
modelId: selected.modelId,
|
|
2010
|
+
score: selected.score,
|
|
2011
|
+
estimates: selected.estimates,
|
|
2012
|
+
inputModalities: selected.capability.inputModalities,
|
|
2013
|
+
outputModalities: selected.capability.outputModalities,
|
|
2014
|
+
fileTransport: selected.capability.fileTransport
|
|
2015
|
+
} } : {},
|
|
2016
|
+
candidates,
|
|
2017
|
+
rejected: candidates.filter((candidate) => !candidate.accepted),
|
|
2018
|
+
fallbackChain: accepted.slice(1).map((candidate) => ({
|
|
2019
|
+
providerId: candidate.providerId,
|
|
2020
|
+
modelId: candidate.modelId,
|
|
2021
|
+
score: candidate.score,
|
|
2022
|
+
reason: "policy-preserving-fallback"
|
|
2023
|
+
})),
|
|
2024
|
+
noRouteReasons: selected === void 0 ? summarizeNoRouteReasons(candidates) : []
|
|
2025
|
+
};
|
|
2026
|
+
}
|
|
2027
|
+
function evaluateCapability(capability, input) {
|
|
2028
|
+
const reasons = [];
|
|
2029
|
+
if (capability.available === false) reasons.push({
|
|
2030
|
+
code: "provider-unavailable",
|
|
2031
|
+
message: `${capability.providerId}/${capability.modelId} is not available.`
|
|
2032
|
+
});
|
|
2033
|
+
if (input.provider !== void 0 && capability.providerId !== input.provider) reasons.push({
|
|
2034
|
+
code: "provider-forced-mismatch",
|
|
2035
|
+
message: `Provider override requires ${input.provider}.`
|
|
2036
|
+
});
|
|
2037
|
+
if (input.model !== void 0 && capability.modelId !== input.model) reasons.push({
|
|
2038
|
+
code: "model-forced-mismatch",
|
|
2039
|
+
message: `Model override requires ${input.model}.`
|
|
2040
|
+
});
|
|
2041
|
+
for (const modality of input.requiredInputs) if (!capability.inputModalities.includes(modality)) reasons.push({
|
|
2042
|
+
code: "input-modality-unsupported",
|
|
2043
|
+
message: `${capability.modelId} does not support ${modality} input.`
|
|
2044
|
+
});
|
|
2045
|
+
for (const modality of input.requiredOutputs) if (!capability.outputModalities.includes(modality)) reasons.push({
|
|
2046
|
+
code: "output-modality-unsupported",
|
|
2047
|
+
message: `${capability.modelId} does not support ${modality} output.`
|
|
2048
|
+
});
|
|
2049
|
+
if (input.requiresStructuredOutput && !capability.structuredOutput) reasons.push({
|
|
2050
|
+
code: "structured-output-unsupported",
|
|
2051
|
+
message: `${capability.modelId} does not support structured output contracts.`
|
|
2052
|
+
});
|
|
2053
|
+
if (input.estimatedInputTokens > capability.contextWindow) reasons.push({
|
|
2054
|
+
code: "context-window-exceeded",
|
|
2055
|
+
message: `Estimated input ${input.estimatedInputTokens} tokens exceeds ${capability.contextWindow}.`
|
|
2056
|
+
});
|
|
2057
|
+
const estimates = estimateRoute(capability, input.estimatedInputTokens);
|
|
2058
|
+
addPolicyRejectReasons(reasons, capability, estimates, input.policy);
|
|
2059
|
+
const contractResult = evaluateContractAgainstRoute(input.contract, {
|
|
2060
|
+
capability,
|
|
2061
|
+
estimatedInputTokens: input.estimatedInputTokens,
|
|
2062
|
+
estimatedOutputTokens: estimates.outputTokens
|
|
2063
|
+
});
|
|
2064
|
+
for (const reason of contractResult.reasons) reasons.push(reason);
|
|
2065
|
+
const score = scoreCapability(capability, estimates, input.index);
|
|
2066
|
+
return {
|
|
2067
|
+
providerId: capability.providerId,
|
|
2068
|
+
modelId: capability.modelId,
|
|
2069
|
+
capability,
|
|
2070
|
+
score,
|
|
2071
|
+
accepted: reasons.length === 0,
|
|
2072
|
+
reasons,
|
|
2073
|
+
estimates
|
|
2074
|
+
};
|
|
2075
|
+
}
|
|
2076
|
+
function addPolicyRejectReasons(reasons, capability, estimates, policy) {
|
|
2077
|
+
if (policy === void 0) return;
|
|
2078
|
+
if (policy.providerAllowList !== void 0 && !policy.providerAllowList.includes(capability.providerId)) reasons.push({
|
|
2079
|
+
code: "provider-not-allowed",
|
|
2080
|
+
message: `${capability.providerId} is not in the provider allow list.`
|
|
2081
|
+
});
|
|
2082
|
+
if (policy.providerDenyList?.includes(capability.providerId) === true) reasons.push({
|
|
2083
|
+
code: "provider-denied",
|
|
2084
|
+
message: `${capability.providerId} is in the provider deny list.`
|
|
2085
|
+
});
|
|
2086
|
+
if (policy.privacy !== void 0 && !capability.dataPolicy.privacy.includes(policy.privacy)) reasons.push({
|
|
2087
|
+
code: "privacy-unsupported",
|
|
2088
|
+
message: `${capability.modelId} does not satisfy ${policy.privacy} privacy.`
|
|
2089
|
+
});
|
|
2090
|
+
if (policy.noLogging === true && capability.dataPolicy.supportsNoLogging !== true) reasons.push({
|
|
2091
|
+
code: "no-logging-unsupported",
|
|
2092
|
+
message: `${capability.modelId} cannot satisfy noLogging.`
|
|
2093
|
+
});
|
|
2094
|
+
if (policy.noUpload === true && capability.fileTransport.length > 0 && capability.fileTransport.every((transport) => transport === "provider-upload")) reasons.push({
|
|
2095
|
+
code: "no-upload-violated",
|
|
2096
|
+
message: `${capability.modelId} requires an upload transport disallowed by policy.`
|
|
2097
|
+
});
|
|
2098
|
+
if (policy.latency !== void 0 && capability.latency !== policy.latency) reasons.push({
|
|
2099
|
+
code: "latency-class-mismatch",
|
|
2100
|
+
message: `${capability.modelId} latency class is ${capability.latency}, not ${policy.latency}.`
|
|
2101
|
+
});
|
|
2102
|
+
if (policy.maxCostUsd !== void 0 && estimates.costUsd !== void 0 && estimates.costUsd > policy.maxCostUsd) reasons.push({
|
|
2103
|
+
code: "budget-exceeded",
|
|
2104
|
+
message: `${capability.modelId} estimated cost ${estimates.costUsd} exceeds maxCostUsd ${policy.maxCostUsd}.`
|
|
2105
|
+
});
|
|
2106
|
+
}
|
|
2107
|
+
function estimateRoute(capability, inputTokens) {
|
|
2108
|
+
const outputTokens = 512;
|
|
2109
|
+
const inputCost = capability.pricing?.inputCostPer1M === void 0 ? void 0 : inputTokens / 1e6 * capability.pricing.inputCostPer1M;
|
|
2110
|
+
const outputCost = capability.pricing?.outputCostPer1M === void 0 ? void 0 : outputTokens / 1e6 * capability.pricing.outputCostPer1M;
|
|
2111
|
+
return {
|
|
2112
|
+
inputTokens,
|
|
2113
|
+
outputTokens,
|
|
2114
|
+
...inputCost !== void 0 || outputCost !== void 0 ? { costUsd: (inputCost ?? 0) + (outputCost ?? 0) } : {},
|
|
2115
|
+
latencyMs: capability.latency === "interactive" ? 1e3 : 1e4
|
|
2116
|
+
};
|
|
2117
|
+
}
|
|
2118
|
+
function scoreCapability(capability, estimates, index) {
|
|
2119
|
+
const costScore = Math.round((estimates.costUsd ?? 0) * 1e6);
|
|
2120
|
+
const latencyScore = capability.latency === "interactive" ? 0 : 1e5;
|
|
2121
|
+
const contextHeadroom = Math.max(0, capability.contextWindow - estimates.inputTokens);
|
|
2122
|
+
const contextScore = Math.max(0, 1e4 - Math.min(contextHeadroom, 1e4));
|
|
2123
|
+
return costScore + latencyScore + contextScore + index;
|
|
2124
|
+
}
|
|
2125
|
+
function compareCandidates(left, right) {
|
|
2126
|
+
if (left.accepted !== right.accepted) return left.accepted ? -1 : 1;
|
|
2127
|
+
if (left.score !== right.score) return left.score - right.score;
|
|
2128
|
+
const provider = left.providerId.localeCompare(right.providerId);
|
|
2129
|
+
return provider === 0 ? left.modelId.localeCompare(right.modelId) : provider;
|
|
2130
|
+
}
|
|
2131
|
+
function requiredInputModalities(artifacts) {
|
|
2132
|
+
const modalities = new Set(["text"]);
|
|
2133
|
+
for (const artifact of artifacts) switch (artifact.kind) {
|
|
2134
|
+
case "text":
|
|
2135
|
+
modalities.add("text");
|
|
2136
|
+
break;
|
|
2137
|
+
case "json":
|
|
2138
|
+
modalities.add("json");
|
|
2139
|
+
break;
|
|
2140
|
+
case "image":
|
|
2141
|
+
modalities.add("image");
|
|
2142
|
+
break;
|
|
2143
|
+
case "audio":
|
|
2144
|
+
modalities.add("audio");
|
|
2145
|
+
break;
|
|
2146
|
+
case "video":
|
|
2147
|
+
modalities.add("video");
|
|
2148
|
+
break;
|
|
2149
|
+
case "document":
|
|
2150
|
+
modalities.add("document");
|
|
2151
|
+
break;
|
|
2152
|
+
case "url":
|
|
2153
|
+
modalities.add("url");
|
|
2154
|
+
break;
|
|
2155
|
+
case "tool-result":
|
|
2156
|
+
modalities.add("tool");
|
|
2157
|
+
break;
|
|
2158
|
+
case "file":
|
|
2159
|
+
modalities.add("file");
|
|
2160
|
+
break;
|
|
2161
|
+
}
|
|
2162
|
+
return [...modalities];
|
|
2163
|
+
}
|
|
2164
|
+
function requiredOutputModalities(outputs) {
|
|
2165
|
+
const modalities = /* @__PURE__ */ new Set();
|
|
2166
|
+
for (const contract of Object.values(outputs)) {
|
|
2167
|
+
if (contract === "text") {
|
|
2168
|
+
modalities.add("text");
|
|
2169
|
+
continue;
|
|
2170
|
+
}
|
|
2171
|
+
if (isStructuredContract(contract)) {
|
|
2172
|
+
modalities.add("json");
|
|
2173
|
+
continue;
|
|
2174
|
+
}
|
|
2175
|
+
if (isReferenceContract(contract)) modalities.add("json");
|
|
2176
|
+
}
|
|
2177
|
+
return [...modalities];
|
|
2178
|
+
}
|
|
2179
|
+
function outputRequiresStructuredOutput(outputs) {
|
|
2180
|
+
return Object.values(outputs).some(isStructuredContract);
|
|
2181
|
+
}
|
|
2182
|
+
function isStructuredContract(contract) {
|
|
2183
|
+
return typeof contract === "object" && contract !== null && "~standard" in contract;
|
|
2184
|
+
}
|
|
2185
|
+
function isReferenceContract(contract) {
|
|
2186
|
+
return typeof contract === "object" && contract !== null && "kind" in contract && (contract.kind === "citations" || contract.kind === "artifacts");
|
|
2187
|
+
}
|
|
2188
|
+
function summarizeNoRouteReasons(candidates) {
|
|
2189
|
+
if (candidates.length === 0) return [{
|
|
2190
|
+
code: "catalog-empty",
|
|
2191
|
+
message: "No provider capabilities are configured."
|
|
2192
|
+
}];
|
|
2193
|
+
const unique = /* @__PURE__ */ new Map();
|
|
2194
|
+
for (const candidate of candidates) for (const reason of candidate.reasons) unique.set(reason.code, reason);
|
|
2195
|
+
return [...unique.values()];
|
|
2196
|
+
}
|
|
2197
|
+
//#endregion
|
|
2198
|
+
//#region src/storage/fingerprint.ts
|
|
2199
|
+
const textEncoder = new TextEncoder();
|
|
2200
|
+
async function fingerprintArtifactValue(value) {
|
|
2201
|
+
const bytes = await valueToBytes(value);
|
|
2202
|
+
if (bytes === void 0) return;
|
|
2203
|
+
const digest = await crypto.subtle.digest("SHA-256", toArrayBuffer(bytes));
|
|
2204
|
+
return {
|
|
2205
|
+
algorithm: "sha256",
|
|
2206
|
+
value: toHex(new Uint8Array(digest))
|
|
2207
|
+
};
|
|
2208
|
+
}
|
|
2209
|
+
async function valueToBytes(value) {
|
|
2210
|
+
if (typeof value === "string") return textEncoder.encode(value);
|
|
2211
|
+
if (value instanceof Uint8Array) return value;
|
|
2212
|
+
if (value instanceof ArrayBuffer) return new Uint8Array(value);
|
|
2213
|
+
if (isBlobLike$1(value)) return new Uint8Array(await value.arrayBuffer());
|
|
2214
|
+
const serialized = JSON.stringify(value);
|
|
2215
|
+
return serialized === void 0 ? void 0 : textEncoder.encode(serialized);
|
|
2216
|
+
}
|
|
2217
|
+
function isBlobLike$1(value) {
|
|
2218
|
+
return typeof Blob !== "undefined" && value instanceof Blob;
|
|
2219
|
+
}
|
|
2220
|
+
function toHex(bytes) {
|
|
2221
|
+
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
2222
|
+
}
|
|
2223
|
+
function toArrayBuffer(bytes) {
|
|
2224
|
+
const copy = new Uint8Array(bytes.byteLength);
|
|
2225
|
+
copy.set(bytes);
|
|
2226
|
+
return copy.buffer;
|
|
2227
|
+
}
|
|
2228
|
+
//#endregion
|
|
2229
|
+
//#region src/tracing/tracing.ts
|
|
2230
|
+
function createRunEvent(kind, input) {
|
|
2231
|
+
return {
|
|
2232
|
+
kind,
|
|
2233
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2234
|
+
...input
|
|
2235
|
+
};
|
|
2236
|
+
}
|
|
2237
|
+
//#endregion
|
|
2238
|
+
//#region src/runtime/config.ts
|
|
2239
|
+
function normalizeConfig(config = {}) {
|
|
2240
|
+
const normalized = {
|
|
2241
|
+
providers: normalizeProviders(config.providers),
|
|
2242
|
+
defaults: config.defaults ?? {},
|
|
2243
|
+
events: normalizeEventSinks(config.events)
|
|
2244
|
+
};
|
|
2245
|
+
if (config.storage !== void 0 && config.storage !== false) normalized.storage = config.storage;
|
|
2246
|
+
if (config.sessions !== void 0 && config.sessions !== false) normalized.sessions = config.sessions;
|
|
2247
|
+
if (config.tracing !== void 0 && config.tracing !== false) normalized.tracing = config.tracing;
|
|
2248
|
+
if (config.signer !== void 0) normalized.signer = config.signer;
|
|
2249
|
+
return normalized;
|
|
2250
|
+
}
|
|
2251
|
+
function normalizeEventSinks(events) {
|
|
2252
|
+
if (events === void 0) return [];
|
|
2253
|
+
return typeof events === "function" ? [events] : events;
|
|
2254
|
+
}
|
|
2255
|
+
function normalizeProviders(providers = []) {
|
|
2256
|
+
return providers.map((provider) => {
|
|
2257
|
+
if (typeof provider === "string") return {
|
|
2258
|
+
id: provider,
|
|
2259
|
+
kind: "provider-ref"
|
|
2260
|
+
};
|
|
2261
|
+
return provider;
|
|
2262
|
+
});
|
|
2263
|
+
}
|
|
2264
|
+
//#endregion
|
|
2265
|
+
//#region src/runtime/create-ai.ts
|
|
2266
|
+
const ZERO_USAGE = {
|
|
2267
|
+
promptTokens: 0,
|
|
2268
|
+
completionTokens: 0,
|
|
2269
|
+
costUsd: 0
|
|
2270
|
+
};
|
|
2271
|
+
const UNMEASURED_USAGE = {
|
|
2272
|
+
promptTokens: 0,
|
|
2273
|
+
completionTokens: 0,
|
|
2274
|
+
costUsd: null
|
|
2275
|
+
};
|
|
2276
|
+
function createAI(config = {}) {
|
|
2277
|
+
const normalized = normalizeConfig(config);
|
|
2278
|
+
return {
|
|
2279
|
+
session(id) {
|
|
2280
|
+
return {
|
|
2281
|
+
id,
|
|
2282
|
+
kind: "session-ref"
|
|
2283
|
+
};
|
|
2284
|
+
},
|
|
2285
|
+
async plan(intent) {
|
|
2286
|
+
return (await buildPlan(normalized, intent)).plan;
|
|
2287
|
+
},
|
|
2288
|
+
run(intent) {
|
|
2289
|
+
return runWithConfig(normalized, intent);
|
|
2290
|
+
},
|
|
2291
|
+
runAgent(intent) {
|
|
2292
|
+
return import("./runtime-CfZ-sGGk.js").then((n) => n.n).then((mod) => mod.runAgent(intent, config));
|
|
2293
|
+
}
|
|
2294
|
+
};
|
|
2295
|
+
}
|
|
2296
|
+
async function runWithConfig(normalized, intent) {
|
|
2297
|
+
if (intent.signal?.aborted === true) throw new DOMException("Run aborted before execution.", "AbortError");
|
|
2298
|
+
const runId = createRunId();
|
|
2299
|
+
const events = [];
|
|
2300
|
+
await emitEvent(normalized, events, createRunEvent("run.start", { runId }));
|
|
2301
|
+
const built = await buildPlan(normalized, intent, runId, events);
|
|
2302
|
+
let plan = built.plan;
|
|
2303
|
+
const selected = plan.route.selected;
|
|
2304
|
+
if (selected === void 0) {
|
|
2305
|
+
const isContractFailure = plan.route.noRouteReasons.filter((r) => r.code === "contract-budget-exceeded" || r.code === "contract-quality-floor" || r.code === "contract-modality-missing" || r.code === "contract-privacy-mismatch").length > 0;
|
|
2306
|
+
const receipt = await maybeIssueReceipt(normalized, {
|
|
2307
|
+
runId,
|
|
2308
|
+
...intent.contract !== void 0 ? { contract: intent.contract } : {},
|
|
2309
|
+
artifacts: intent.artifacts ?? [],
|
|
2310
|
+
contractVerdict: isContractFailure ? "no-contract-match" : "execution-failed",
|
|
2311
|
+
model: {
|
|
2312
|
+
requested: intent.overrides?.model ?? "",
|
|
2313
|
+
observed: null
|
|
2314
|
+
},
|
|
2315
|
+
route: {
|
|
2316
|
+
providerId: "",
|
|
2317
|
+
capabilityId: "",
|
|
2318
|
+
attemptNumber: 0
|
|
2319
|
+
},
|
|
2320
|
+
usage: ZERO_USAGE,
|
|
2321
|
+
...isContractFailure ? { noRouteReasons: plan.route.noRouteReasons } : {}
|
|
2322
|
+
});
|
|
2323
|
+
const failure = isContractFailure ? {
|
|
2324
|
+
ok: false,
|
|
2325
|
+
error: {
|
|
2326
|
+
kind: "no-contract-match",
|
|
2327
|
+
message: "No route satisfies the contract.",
|
|
2328
|
+
noRouteReasons: plan.route.noRouteReasons
|
|
2329
|
+
},
|
|
2330
|
+
usage: { ...ZERO_USAGE },
|
|
2331
|
+
plan,
|
|
2332
|
+
events,
|
|
2333
|
+
...receipt !== void 0 ? { receipt } : {}
|
|
2334
|
+
} : {
|
|
2335
|
+
ok: false,
|
|
2336
|
+
error: {
|
|
2337
|
+
kind: "no_route",
|
|
2338
|
+
message: "No route satisfied the run requirements.",
|
|
2339
|
+
reasons: plan.route.noRouteReasons.map((reason) => reason.message)
|
|
2340
|
+
},
|
|
2341
|
+
usage: { ...ZERO_USAGE },
|
|
2342
|
+
plan,
|
|
2343
|
+
events,
|
|
2344
|
+
...receipt !== void 0 ? { receipt } : {}
|
|
2345
|
+
};
|
|
2346
|
+
await emitEvent(normalized, events, createRunEvent("run.failed", {
|
|
2347
|
+
runId,
|
|
2348
|
+
planId: plan.id,
|
|
2349
|
+
metadata: { reason: isContractFailure ? "no-contract-match" : "no-route" }
|
|
2350
|
+
}));
|
|
2351
|
+
return failure;
|
|
2352
|
+
}
|
|
2353
|
+
const routes = [selected, ...plan.route.fallbackChain.map((fallback) => routeFromCandidate(plan, fallback.providerId, fallback.modelId) ?? {
|
|
2354
|
+
providerId: fallback.providerId,
|
|
2355
|
+
modelId: fallback.modelId,
|
|
2356
|
+
score: fallback.score,
|
|
2357
|
+
estimates: selected.estimates,
|
|
2358
|
+
inputModalities: selected.inputModalities,
|
|
2359
|
+
outputModalities: selected.outputModalities,
|
|
2360
|
+
fileTransport: selected.fileTransport
|
|
2361
|
+
})];
|
|
2362
|
+
const attempts = [];
|
|
2363
|
+
let lastError;
|
|
2364
|
+
let anyExecutableAdapter = false;
|
|
2365
|
+
for (const [index, route] of routes.entries()) {
|
|
2366
|
+
const adapter = findExecutableAdapter(normalized, route.providerId);
|
|
2367
|
+
if (adapter === void 0) {
|
|
2368
|
+
lastError = /* @__PURE__ */ new Error("No Phase 1 provider adapter with execute() is configured.");
|
|
2369
|
+
continue;
|
|
2370
|
+
}
|
|
2371
|
+
anyExecutableAdapter = true;
|
|
2372
|
+
if (index > 0) await emitEvent(normalized, events, createRunEvent("fallback.activated", {
|
|
2373
|
+
runId,
|
|
2374
|
+
planId: plan.id,
|
|
2375
|
+
providerId: route.providerId,
|
|
2376
|
+
modelId: route.modelId
|
|
2377
|
+
}));
|
|
2378
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2379
|
+
const attemptPackaging = packageArtifactsForProvider({
|
|
2380
|
+
artifacts: built.artifacts,
|
|
2381
|
+
route,
|
|
2382
|
+
...built.mergedPolicy !== void 0 ? { policy: built.mergedPolicy } : {}
|
|
2383
|
+
});
|
|
2384
|
+
if (attemptPackaging.blocked.length > 0) {
|
|
2385
|
+
const message = attemptPackaging.blocked.join("; ");
|
|
2386
|
+
attempts.push(attemptFailed(route.providerId, route.modelId, startedAt, (/* @__PURE__ */ new Date()).toISOString(), message));
|
|
2387
|
+
lastError = new Error(message);
|
|
2388
|
+
continue;
|
|
2389
|
+
}
|
|
2390
|
+
const request = {
|
|
2391
|
+
task: intent.task,
|
|
2392
|
+
artifacts: built.artifacts,
|
|
2393
|
+
outputs: Object.keys(intent.outputs),
|
|
2394
|
+
outputContracts: intent.outputs,
|
|
2395
|
+
...built.mergedPolicy !== void 0 ? { policy: built.mergedPolicy } : {},
|
|
2396
|
+
...intent.signal !== void 0 ? { signal: intent.signal } : {},
|
|
2397
|
+
plan,
|
|
2398
|
+
contextPack: built.contextPack,
|
|
2399
|
+
providerPackaging: attemptPackaging.plan,
|
|
2400
|
+
packagedArtifacts: attemptPackaging.packagedArtifacts
|
|
2401
|
+
};
|
|
2402
|
+
try {
|
|
2403
|
+
await emitEvent(normalized, events, createRunEvent("provider.attempt", {
|
|
2404
|
+
runId,
|
|
2405
|
+
planId: plan.id,
|
|
2406
|
+
providerId: route.providerId,
|
|
2407
|
+
modelId: route.modelId,
|
|
2408
|
+
metadata: {
|
|
2409
|
+
status: "started",
|
|
2410
|
+
fallback: index > 0
|
|
2411
|
+
}
|
|
2412
|
+
}));
|
|
2413
|
+
await intent.overrides?.hooks?.beforeProviderCall?.({
|
|
2414
|
+
plan,
|
|
2415
|
+
request
|
|
2416
|
+
});
|
|
2417
|
+
plan = withPlanStatus(plan, "running", {
|
|
2418
|
+
stages: markStage(plan.stages, "execution", "running"),
|
|
2419
|
+
attempts: [...attempts, {
|
|
2420
|
+
providerId: route.providerId,
|
|
2421
|
+
modelId: route.modelId,
|
|
2422
|
+
status: "running",
|
|
2423
|
+
startedAt
|
|
2424
|
+
}]
|
|
2425
|
+
});
|
|
2426
|
+
const response = await adapter.execute(request);
|
|
2427
|
+
await intent.overrides?.hooks?.afterProviderCall?.({
|
|
2428
|
+
plan,
|
|
2429
|
+
response
|
|
2430
|
+
});
|
|
2431
|
+
const completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2432
|
+
const validation = await validateOutputMap(intent.outputs, response.rawOutputs, plan);
|
|
2433
|
+
const succeededAttempt = attemptSucceeded(route.providerId, route.modelId, startedAt, completedAt, response.usage);
|
|
2434
|
+
if (!validation.ok) {
|
|
2435
|
+
attempts.push({
|
|
2436
|
+
...succeededAttempt,
|
|
2437
|
+
status: "failed",
|
|
2438
|
+
error: validation.error.message
|
|
2439
|
+
});
|
|
2440
|
+
const failedPlan = withPlanStatus(plan, "failed", {
|
|
2441
|
+
stages: markStage(plan.stages, "validation", "failed"),
|
|
2442
|
+
attempts
|
|
2443
|
+
});
|
|
2444
|
+
await emitEvent(normalized, events, createRunEvent("validation.failed", {
|
|
2445
|
+
runId,
|
|
2446
|
+
planId: plan.id,
|
|
2447
|
+
providerId: route.providerId,
|
|
2448
|
+
modelId: route.modelId,
|
|
2449
|
+
metadata: { error: validation.error.message }
|
|
2450
|
+
}));
|
|
2451
|
+
if (index === routes.length - 1) {
|
|
2452
|
+
const receipt = await maybeIssueReceipt(normalized, {
|
|
2453
|
+
runId,
|
|
2454
|
+
...intent.contract !== void 0 ? { contract: intent.contract } : {},
|
|
2455
|
+
artifacts: built.artifacts,
|
|
2456
|
+
contractVerdict: "validation-failed",
|
|
2457
|
+
model: {
|
|
2458
|
+
requested: route.modelId,
|
|
2459
|
+
observed: null
|
|
2460
|
+
},
|
|
2461
|
+
route: {
|
|
2462
|
+
providerId: route.providerId,
|
|
2463
|
+
capabilityId: route.modelId,
|
|
2464
|
+
attemptNumber: attempts.length
|
|
2465
|
+
},
|
|
2466
|
+
usage: normalizeAdapterUsage(response)
|
|
2467
|
+
});
|
|
2468
|
+
return {
|
|
2469
|
+
...validation,
|
|
2470
|
+
usage: normalizeAdapterUsage(response),
|
|
2471
|
+
plan: failedPlan,
|
|
2472
|
+
events,
|
|
2473
|
+
...receipt !== void 0 ? { receipt } : {}
|
|
2474
|
+
};
|
|
2475
|
+
}
|
|
2476
|
+
lastError = new Error(validation.error.message);
|
|
2477
|
+
continue;
|
|
2478
|
+
}
|
|
2479
|
+
const invariants = intent.contract?.invariants ?? [];
|
|
2480
|
+
if (invariants.length > 0) {
|
|
2481
|
+
const tripwireResult = await evaluateTripwires(validation.outputs, invariants);
|
|
2482
|
+
if (!tripwireResult.ok) {
|
|
2483
|
+
const tripwireFailedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2484
|
+
attempts.push({
|
|
2485
|
+
...succeededAttempt,
|
|
2486
|
+
status: "failed",
|
|
2487
|
+
error: tripwireResult.evidence.message,
|
|
2488
|
+
completedAt: tripwireFailedAt
|
|
2489
|
+
});
|
|
2490
|
+
const failedPlan = withPlanStatus(plan, "failed", {
|
|
2491
|
+
stages: markStage(markStage(markStage(plan.stages, "execution", "completed"), "validation", "completed"), "tripwire", "failed", { invariantId: tripwireResult.evidence.invariantId }),
|
|
2492
|
+
attempts
|
|
2493
|
+
});
|
|
2494
|
+
await emitEvent(normalized, events, createRunEvent("run.failed", {
|
|
2495
|
+
runId,
|
|
2496
|
+
planId: failedPlan.id,
|
|
2497
|
+
providerId: route.providerId,
|
|
2498
|
+
modelId: route.modelId,
|
|
2499
|
+
metadata: {
|
|
2500
|
+
reason: "tripwire-violated",
|
|
2501
|
+
invariantId: tripwireResult.evidence.invariantId
|
|
2502
|
+
}
|
|
2503
|
+
}));
|
|
2504
|
+
const receipt = await maybeIssueReceipt(normalized, {
|
|
2505
|
+
runId,
|
|
2506
|
+
...intent.contract !== void 0 ? { contract: intent.contract } : {},
|
|
2507
|
+
artifacts: built.artifacts,
|
|
2508
|
+
contractVerdict: "tripwire-violated",
|
|
2509
|
+
model: {
|
|
2510
|
+
requested: route.modelId,
|
|
2511
|
+
observed: null
|
|
2512
|
+
},
|
|
2513
|
+
route: {
|
|
2514
|
+
providerId: route.providerId,
|
|
2515
|
+
capabilityId: route.modelId,
|
|
2516
|
+
attemptNumber: attempts.length
|
|
2517
|
+
},
|
|
2518
|
+
usage: normalizeAdapterUsage(response),
|
|
2519
|
+
tripwireEvidence: tripwireResult.evidence
|
|
2520
|
+
});
|
|
2521
|
+
return {
|
|
2522
|
+
ok: false,
|
|
2523
|
+
error: {
|
|
2524
|
+
kind: "tripwire-violated",
|
|
2525
|
+
message: tripwireResult.evidence.message,
|
|
2526
|
+
invariantId: tripwireResult.evidence.invariantId,
|
|
2527
|
+
evidence: tripwireResult.evidence,
|
|
2528
|
+
terminal: true
|
|
2529
|
+
},
|
|
2530
|
+
usage: normalizeAdapterUsage(response),
|
|
2531
|
+
plan: failedPlan,
|
|
2532
|
+
events,
|
|
2533
|
+
...receipt !== void 0 ? { receipt } : {}
|
|
2534
|
+
};
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
attempts.push(succeededAttempt);
|
|
2538
|
+
const artifactRefs = response.artifactRefs !== void 0 ? response.artifactRefs.map(toArtifactRef) : [];
|
|
2539
|
+
const completedPlan = withPlanStatus(plan, "completed", {
|
|
2540
|
+
stages: markStage(markStage(markStage(markStage(markStage(plan.stages, "execution", "completed"), "validation", "completed"), "persistence", "completed"), "tool-execution", built.toolResults.length > 0 ? "completed" : "skipped"), "tripwire", invariants.length > 0 ? "completed" : "skipped"),
|
|
2541
|
+
attempts
|
|
2542
|
+
});
|
|
2543
|
+
if (built.sessionRecord !== void 0 && normalized.sessions !== void 0) await normalized.sessions.appendTurn({
|
|
2544
|
+
sessionId: built.sessionRecord.id,
|
|
2545
|
+
task: intent.task,
|
|
2546
|
+
artifactRefs: built.artifacts.map(toArtifactRef),
|
|
2547
|
+
outputArtifactRefs: artifactRefs,
|
|
2548
|
+
planId: completedPlan.id
|
|
2549
|
+
});
|
|
2550
|
+
await emitEvent(normalized, events, createRunEvent("validation.complete", {
|
|
2551
|
+
runId,
|
|
2552
|
+
planId: completedPlan.id,
|
|
2553
|
+
providerId: route.providerId,
|
|
2554
|
+
modelId: route.modelId
|
|
2555
|
+
}));
|
|
2556
|
+
await emitEvent(normalized, events, createRunEvent("run.complete", {
|
|
2557
|
+
runId,
|
|
2558
|
+
planId: completedPlan.id
|
|
2559
|
+
}));
|
|
2560
|
+
const successValidation = validation;
|
|
2561
|
+
const receipt = await maybeIssueReceipt(normalized, {
|
|
2562
|
+
runId,
|
|
2563
|
+
...intent.contract !== void 0 ? { contract: intent.contract } : {},
|
|
2564
|
+
artifacts: built.artifacts,
|
|
2565
|
+
contractVerdict: "success",
|
|
2566
|
+
model: {
|
|
2567
|
+
requested: route.modelId,
|
|
2568
|
+
observed: null
|
|
2569
|
+
},
|
|
2570
|
+
route: {
|
|
2571
|
+
providerId: route.providerId,
|
|
2572
|
+
capabilityId: route.modelId,
|
|
2573
|
+
attemptNumber: attempts.length
|
|
2574
|
+
},
|
|
2575
|
+
usage: normalizeAdapterUsage(response),
|
|
2576
|
+
outputs: JSON.stringify(successValidation.outputs)
|
|
2577
|
+
});
|
|
2578
|
+
return {
|
|
2579
|
+
...validation,
|
|
2580
|
+
artifacts: artifactRefs,
|
|
2581
|
+
usage: normalizeAdapterUsage(response),
|
|
2582
|
+
plan: completedPlan,
|
|
2583
|
+
events,
|
|
2584
|
+
...receipt !== void 0 ? { receipt } : {}
|
|
2585
|
+
};
|
|
2586
|
+
} catch (error) {
|
|
2587
|
+
const completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2588
|
+
const message = error instanceof Error ? error.message : "Provider adapter execution failed.";
|
|
2589
|
+
attempts.push(attemptFailed(route.providerId, route.modelId, startedAt, completedAt, message));
|
|
2590
|
+
lastError = error instanceof Error ? error : new Error(message);
|
|
2591
|
+
await emitEvent(normalized, events, createRunEvent("provider.attempt", {
|
|
2592
|
+
runId,
|
|
2593
|
+
planId: plan.id,
|
|
2594
|
+
providerId: route.providerId,
|
|
2595
|
+
modelId: route.modelId,
|
|
2596
|
+
metadata: {
|
|
2597
|
+
status: "failed",
|
|
2598
|
+
error: message
|
|
2599
|
+
}
|
|
2600
|
+
}));
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
if (!anyExecutableAdapter) {
|
|
2604
|
+
const receipt = await maybeIssueReceipt(normalized, {
|
|
2605
|
+
runId,
|
|
2606
|
+
...intent.contract !== void 0 ? { contract: intent.contract } : {},
|
|
2607
|
+
artifacts: built.artifacts,
|
|
2608
|
+
contractVerdict: "execution-failed",
|
|
2609
|
+
model: {
|
|
2610
|
+
requested: selected.modelId,
|
|
2611
|
+
observed: null
|
|
2612
|
+
},
|
|
2613
|
+
route: {
|
|
2614
|
+
providerId: selected.providerId,
|
|
2615
|
+
capabilityId: selected.modelId,
|
|
2616
|
+
attemptNumber: 0
|
|
2617
|
+
},
|
|
2618
|
+
usage: ZERO_USAGE
|
|
2619
|
+
});
|
|
2620
|
+
return {
|
|
2621
|
+
ok: false,
|
|
2622
|
+
error: {
|
|
2623
|
+
kind: "execution_unavailable",
|
|
2624
|
+
message: "No Phase 1 provider adapter with execute() is configured."
|
|
2625
|
+
},
|
|
2626
|
+
usage: { ...ZERO_USAGE },
|
|
2627
|
+
plan,
|
|
2628
|
+
events,
|
|
2629
|
+
...receipt !== void 0 ? { receipt } : {}
|
|
2630
|
+
};
|
|
2631
|
+
}
|
|
2632
|
+
const failedPlan = withPlanStatus(plan, "failed", {
|
|
2633
|
+
stages: markStage(plan.stages, "execution", "failed"),
|
|
2634
|
+
attempts
|
|
2635
|
+
});
|
|
2636
|
+
await emitEvent(normalized, events, createRunEvent("run.failed", {
|
|
2637
|
+
runId,
|
|
2638
|
+
planId: failedPlan.id,
|
|
2639
|
+
metadata: { error: lastError?.message ?? "Provider adapter execution failed." }
|
|
2640
|
+
}));
|
|
2641
|
+
const receipt = await maybeIssueReceipt(normalized, {
|
|
2642
|
+
runId,
|
|
2643
|
+
...intent.contract !== void 0 ? { contract: intent.contract } : {},
|
|
2644
|
+
artifacts: built.artifacts,
|
|
2645
|
+
contractVerdict: "execution-failed",
|
|
2646
|
+
model: {
|
|
2647
|
+
requested: selected.modelId,
|
|
2648
|
+
observed: null
|
|
2649
|
+
},
|
|
2650
|
+
route: {
|
|
2651
|
+
providerId: selected.providerId,
|
|
2652
|
+
capabilityId: selected.modelId,
|
|
2653
|
+
attemptNumber: attempts.length
|
|
2654
|
+
},
|
|
2655
|
+
usage: UNMEASURED_USAGE
|
|
2656
|
+
});
|
|
2657
|
+
return {
|
|
2658
|
+
ok: false,
|
|
2659
|
+
error: {
|
|
2660
|
+
kind: "provider_execution",
|
|
2661
|
+
message: lastError?.message ?? "Provider adapter execution failed.",
|
|
2662
|
+
providerId: selected.providerId,
|
|
2663
|
+
modelId: selected.modelId
|
|
2664
|
+
},
|
|
2665
|
+
usage: { ...UNMEASURED_USAGE },
|
|
2666
|
+
plan: failedPlan,
|
|
2667
|
+
events,
|
|
2668
|
+
...receipt !== void 0 ? { receipt } : {}
|
|
2669
|
+
};
|
|
2670
|
+
}
|
|
2671
|
+
async function buildPlan(normalized, intent, runId = createRunId(), events = []) {
|
|
2672
|
+
const prepared = await prepareArtifacts(intent);
|
|
2673
|
+
const artifacts = prepared.artifacts;
|
|
2674
|
+
const mergedPolicy = mergePolicy(mergePolicy(normalized.defaults.policy, intent.policy), intent.overrides?.routingPolicy);
|
|
2675
|
+
const sessionRecord = intent.session !== void 0 && normalized.sessions !== void 0 ? await loadOrCreateSession(normalized, intent.session) : void 0;
|
|
2676
|
+
const route = routeDeterministically(createCapabilityCatalog(normalized.providers), {
|
|
2677
|
+
task: intent.task,
|
|
2678
|
+
artifacts,
|
|
2679
|
+
outputs: intent.outputs,
|
|
2680
|
+
...mergedPolicy !== void 0 ? { policy: mergedPolicy } : {},
|
|
2681
|
+
...intent.overrides?.provider !== void 0 ? { provider: intent.overrides.provider } : {},
|
|
2682
|
+
...intent.overrides?.model !== void 0 ? { model: intent.overrides.model } : {},
|
|
2683
|
+
...intent.contract !== void 0 ? { contract: intent.contract } : {}
|
|
2684
|
+
});
|
|
2685
|
+
const contextPack = buildContextPack({
|
|
2686
|
+
task: intent.task,
|
|
2687
|
+
artifacts,
|
|
2688
|
+
...route.selected !== void 0 ? { route: route.selected } : {},
|
|
2689
|
+
...sessionRecord !== void 0 ? { session: sessionRecord } : {},
|
|
2690
|
+
...intent.overrides?.tokenBudget !== void 0 ? { tokenBudget: intent.overrides.tokenBudget } : {}
|
|
2691
|
+
});
|
|
2692
|
+
const summaryRefs = contextPack.summarized.length > 0 && intent.overrides?.summarizer !== void 0 ? await intent.overrides.summarizer.summarize({
|
|
2693
|
+
artifacts: artifacts.map(toArtifactRef),
|
|
2694
|
+
budgetTokens: contextPack.tokenBudget
|
|
2695
|
+
}) : [];
|
|
2696
|
+
const packaging = packageArtifactsForProvider({
|
|
2697
|
+
artifacts,
|
|
2698
|
+
...route.selected !== void 0 ? { route: route.selected } : {},
|
|
2699
|
+
...mergedPolicy !== void 0 ? { policy: mergedPolicy } : {}
|
|
2700
|
+
});
|
|
2701
|
+
let plan = createExecutionPlan({
|
|
2702
|
+
task: intent.task,
|
|
2703
|
+
artifacts: artifacts.map(toArtifactRef),
|
|
2704
|
+
outputs: intent.outputs,
|
|
2705
|
+
route,
|
|
2706
|
+
context: contextPack,
|
|
2707
|
+
providerPackaging: packaging.plan,
|
|
2708
|
+
warnings: packaging.blocked,
|
|
2709
|
+
metadata: {
|
|
2710
|
+
...intent.tools !== void 0 ? { tools: intent.tools.map((tool) => tool.name) } : {},
|
|
2711
|
+
...summaryRefs.length > 0 ? { summaryArtifactIds: summaryRefs.map((summary) => summary.id) } : {}
|
|
2712
|
+
}
|
|
2713
|
+
});
|
|
2714
|
+
plan = withPlanStatus(plan, plan.status, { stages: markStage(plan.stages, "tool-execution", prepared.toolResults.length > 0 ? "completed" : "skipped", prepared.toolResults.length > 0 ? { toolNames: prepared.toolResults.map((result) => result.toolName) } : void 0) });
|
|
2715
|
+
for (const result of prepared.toolResults) {
|
|
2716
|
+
await emitEvent(normalized, events, createRunEvent("tool.call", {
|
|
2717
|
+
runId,
|
|
2718
|
+
planId: plan.id,
|
|
2719
|
+
artifactId: result.artifact.id,
|
|
2720
|
+
metadata: {
|
|
2721
|
+
toolName: result.toolName,
|
|
2722
|
+
callId: result.callId
|
|
2723
|
+
}
|
|
2724
|
+
}));
|
|
2725
|
+
await emitEvent(normalized, events, createRunEvent("artifact.created", {
|
|
2726
|
+
runId,
|
|
2727
|
+
planId: plan.id,
|
|
2728
|
+
artifactId: result.artifact.id,
|
|
2729
|
+
metadata: { source: "tool" }
|
|
2730
|
+
}));
|
|
2731
|
+
}
|
|
2732
|
+
for (const artifactRef of artifacts.map(toArtifactRef)) await emitEvent(normalized, events, createRunEvent("artifact.ingested", {
|
|
2733
|
+
runId,
|
|
2734
|
+
planId: plan.id,
|
|
2735
|
+
artifactId: artifactRef.id
|
|
2736
|
+
}));
|
|
2737
|
+
await emitEvent(normalized, events, createRunEvent("context.packed", {
|
|
2738
|
+
runId,
|
|
2739
|
+
planId: plan.id,
|
|
2740
|
+
metadata: {
|
|
2741
|
+
estimatedTokens: contextPack.estimatedTokens,
|
|
2742
|
+
included: contextPack.included.length,
|
|
2743
|
+
summarized: contextPack.summarized.length,
|
|
2744
|
+
omitted: contextPack.omitted.length
|
|
2745
|
+
}
|
|
2746
|
+
}));
|
|
2747
|
+
await emitEvent(normalized, events, createRunEvent("router.candidates", {
|
|
2748
|
+
runId,
|
|
2749
|
+
planId: plan.id,
|
|
2750
|
+
metadata: {
|
|
2751
|
+
selected: route.selected?.modelId,
|
|
2752
|
+
rejected: route.rejected.length,
|
|
2753
|
+
fallbacks: route.fallbackChain.length
|
|
2754
|
+
}
|
|
2755
|
+
}));
|
|
2756
|
+
return {
|
|
2757
|
+
plan,
|
|
2758
|
+
artifacts,
|
|
2759
|
+
contextPack,
|
|
2760
|
+
packagedArtifacts: packaging.packagedArtifacts,
|
|
2761
|
+
blockedPackaging: packaging.blocked,
|
|
2762
|
+
toolResults: prepared.toolResults,
|
|
2763
|
+
...mergedPolicy !== void 0 ? { mergedPolicy } : {},
|
|
2764
|
+
...sessionRecord !== void 0 ? { sessionRecord } : {}
|
|
2765
|
+
};
|
|
2766
|
+
}
|
|
2767
|
+
async function prepareArtifacts(intent) {
|
|
2768
|
+
let artifacts = [...intent.artifacts ?? []];
|
|
2769
|
+
for (const transform of intent.overrides?.transforms ?? []) {
|
|
2770
|
+
const transformed = await transform.transform({
|
|
2771
|
+
task: intent.task,
|
|
2772
|
+
artifacts
|
|
2773
|
+
});
|
|
2774
|
+
artifacts = artifacts.concat(Array.isArray(transformed) ? transformed : [transformed]);
|
|
2775
|
+
}
|
|
2776
|
+
const toolResults = [];
|
|
2777
|
+
for (const tool of intent.tools ?? []) {
|
|
2778
|
+
const result = await runTool(tool, intent.toolInputs?.[tool.name] ?? {});
|
|
2779
|
+
toolResults.push(result);
|
|
2780
|
+
artifacts.push(result.artifact);
|
|
2781
|
+
}
|
|
2782
|
+
return {
|
|
2783
|
+
artifacts,
|
|
2784
|
+
toolResults
|
|
2785
|
+
};
|
|
2786
|
+
}
|
|
2787
|
+
async function loadOrCreateSession(normalized, session) {
|
|
2788
|
+
const existing = await normalized.sessions?.load(session.id);
|
|
2789
|
+
if (existing !== void 0) return existing;
|
|
2790
|
+
if (normalized.sessions === void 0) throw new Error("Session storage is not configured.");
|
|
2791
|
+
return normalized.sessions.create({ id: session.id });
|
|
2792
|
+
}
|
|
2793
|
+
function attemptSucceeded(providerId, modelId, startedAt, completedAt, usage) {
|
|
2794
|
+
return {
|
|
2795
|
+
providerId,
|
|
2796
|
+
modelId,
|
|
2797
|
+
status: "succeeded",
|
|
2798
|
+
startedAt,
|
|
2799
|
+
completedAt,
|
|
2800
|
+
...usage !== void 0 ? { usage } : {}
|
|
2801
|
+
};
|
|
2802
|
+
}
|
|
2803
|
+
function attemptFailed(providerId, modelId, startedAt, completedAt, error) {
|
|
2804
|
+
return {
|
|
2805
|
+
providerId,
|
|
2806
|
+
modelId,
|
|
2807
|
+
status: "failed",
|
|
2808
|
+
startedAt,
|
|
2809
|
+
completedAt,
|
|
2810
|
+
error
|
|
2811
|
+
};
|
|
2812
|
+
}
|
|
2813
|
+
function findExecutableAdapter(normalized, providerId) {
|
|
2814
|
+
return normalized.providers.find((provider) => provider.kind === "provider-adapter" && provider.id === providerId && typeof provider.execute === "function");
|
|
2815
|
+
}
|
|
2816
|
+
function routeFromCandidate(plan, providerId, modelId) {
|
|
2817
|
+
const candidate = plan.route.candidates.find((item) => item.providerId === providerId && item.modelId === modelId);
|
|
2818
|
+
if (candidate === void 0) return;
|
|
2819
|
+
return {
|
|
2820
|
+
providerId,
|
|
2821
|
+
modelId,
|
|
2822
|
+
score: candidate.score,
|
|
2823
|
+
estimates: candidate.estimates,
|
|
2824
|
+
inputModalities: candidate.capability.inputModalities,
|
|
2825
|
+
outputModalities: candidate.capability.outputModalities,
|
|
2826
|
+
fileTransport: candidate.capability.fileTransport
|
|
2827
|
+
};
|
|
2828
|
+
}
|
|
2829
|
+
async function emitEvent(normalized, events, event) {
|
|
2830
|
+
events.push(event);
|
|
2831
|
+
normalized.tracing?.event?.(event.kind, {
|
|
2832
|
+
...event.metadata,
|
|
2833
|
+
planId: event.planId,
|
|
2834
|
+
providerId: event.providerId,
|
|
2835
|
+
modelId: event.modelId,
|
|
2836
|
+
artifactId: event.artifactId
|
|
2837
|
+
});
|
|
2838
|
+
await Promise.all(normalized.events.map((sink) => sink(event)));
|
|
2839
|
+
}
|
|
2840
|
+
function createRunId() {
|
|
2841
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return `run:${crypto.randomUUID()}`;
|
|
2842
|
+
return `run:${Date.now()}:${Math.random().toString(16).slice(2)}`;
|
|
2843
|
+
}
|
|
2844
|
+
/**
|
|
2845
|
+
* Normalize an adapter response into the `RunResult.usage` shape.
|
|
2846
|
+
*
|
|
2847
|
+
* Prefers `ProviderRunResponse.normalizedUsage` (the Phase 7 shape emitted by
|
|
2848
|
+
* openai / openai-compat / ai-sdk / fake adapters). Falls back to mapping the
|
|
2849
|
+
* legacy `UsageRecord` (inputTokens / outputTokens) so v1.0 adapters that have
|
|
2850
|
+
* not yet been re-rolled still surface a usable Usage value.
|
|
2851
|
+
*/
|
|
2852
|
+
function normalizeAdapterUsage(response) {
|
|
2853
|
+
if (response.normalizedUsage !== void 0) return response.normalizedUsage;
|
|
2854
|
+
return {
|
|
2855
|
+
promptTokens: response.usage?.inputTokens ?? 0,
|
|
2856
|
+
completionTokens: response.usage?.outputTokens ?? 0,
|
|
2857
|
+
costUsd: response.usage?.costUsd ?? null
|
|
2858
|
+
};
|
|
2859
|
+
}
|
|
2860
|
+
/**
|
|
2861
|
+
* Phase 9 — hash each artifact's canonical value via SHA-256 and return the
|
|
2862
|
+
* hex digests in declaration order. Missing/undefined values produce an
|
|
2863
|
+
* empty string so the array length matches `artifacts.length` exactly.
|
|
2864
|
+
*/
|
|
2865
|
+
async function hashInputArtifacts(artifacts) {
|
|
2866
|
+
const out = [];
|
|
2867
|
+
for (const artifact of artifacts) {
|
|
2868
|
+
const fp = await fingerprintArtifactValue(artifact.value);
|
|
2869
|
+
out.push(fp?.value ?? "");
|
|
2870
|
+
}
|
|
2871
|
+
return out;
|
|
2872
|
+
}
|
|
2873
|
+
/**
|
|
2874
|
+
* Phase 9 — SHA-256 hex of `canonicalize(contract)` for the receipt's
|
|
2875
|
+
* contractHash field. Returns null when no contract is attached or when
|
|
2876
|
+
* canonicalize cannot serialize the input.
|
|
2877
|
+
*/
|
|
2878
|
+
async function sha256HexOfCanonicalContract(contract) {
|
|
2879
|
+
if (contract === void 0 || contract === null) return null;
|
|
2880
|
+
const canonical = canonicalize(contract);
|
|
2881
|
+
if (canonical === void 0) return null;
|
|
2882
|
+
const bytes = new TextEncoder().encode(canonical);
|
|
2883
|
+
const ab = new Uint8Array(bytes.byteLength);
|
|
2884
|
+
ab.set(bytes);
|
|
2885
|
+
const digest = await crypto.subtle.digest("SHA-256", ab.buffer);
|
|
2886
|
+
return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
2887
|
+
}
|
|
2888
|
+
/**
|
|
2889
|
+
* Phase 9 — issue a signed receipt at a terminal branch when a signer is
|
|
2890
|
+
* configured. Signer failures degrade gracefully to `undefined` so a faulty
|
|
2891
|
+
* signer never crashes `ai.run`.
|
|
2892
|
+
*/
|
|
2893
|
+
async function maybeIssueReceipt(normalized, input) {
|
|
2894
|
+
if (normalized.signer === void 0) return void 0;
|
|
2895
|
+
try {
|
|
2896
|
+
const inputHashes = await hashInputArtifacts(input.artifacts);
|
|
2897
|
+
const outputHash = input.outputs === void 0 ? null : (await fingerprintArtifactValue(input.outputs))?.value ?? null;
|
|
2898
|
+
const contractHash = await sha256HexOfCanonicalContract(input.contract);
|
|
2899
|
+
return await createReceipt({
|
|
2900
|
+
runId: input.runId,
|
|
2901
|
+
model: input.model,
|
|
2902
|
+
route: input.route,
|
|
2903
|
+
usage: input.usage,
|
|
2904
|
+
contractVerdict: input.contractVerdict,
|
|
2905
|
+
contractHash,
|
|
2906
|
+
inputHashes,
|
|
2907
|
+
outputHash,
|
|
2908
|
+
...input.noRouteReasons !== void 0 ? { noRouteReasons: input.noRouteReasons } : {},
|
|
2909
|
+
...input.tripwireEvidence !== void 0 ? { tripwireEvidence: input.tripwireEvidence } : {}
|
|
2910
|
+
}, normalized.signer);
|
|
2911
|
+
} catch {
|
|
2912
|
+
return;
|
|
2913
|
+
}
|
|
2914
|
+
}
|
|
2915
|
+
//#endregion
|
|
2916
|
+
//#region src/sessions/session.ts
|
|
2917
|
+
function createMemorySessionStore(options = {}) {
|
|
2918
|
+
const storeId = options.id ?? "memory-sessions";
|
|
2919
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
2920
|
+
return {
|
|
2921
|
+
kind: "session-store",
|
|
2922
|
+
id: storeId,
|
|
2923
|
+
async create(createOptions = {}) {
|
|
2924
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2925
|
+
const session = {
|
|
2926
|
+
id: createOptions.id ?? createSessionId(),
|
|
2927
|
+
kind: "session-ref",
|
|
2928
|
+
...createOptions.parentId !== void 0 ? { parentId: createOptions.parentId } : {},
|
|
2929
|
+
...createOptions.branchPointRunId !== void 0 ? { branchPointRunId: createOptions.branchPointRunId } : {},
|
|
2930
|
+
turns: [],
|
|
2931
|
+
summaries: [],
|
|
2932
|
+
artifactRefs: [],
|
|
2933
|
+
planIds: [],
|
|
2934
|
+
createdAt: now,
|
|
2935
|
+
updatedAt: now
|
|
2936
|
+
};
|
|
2937
|
+
sessions.set(session.id, clone(session));
|
|
2938
|
+
return clone(session);
|
|
2939
|
+
},
|
|
2940
|
+
async load(id) {
|
|
2941
|
+
const session = sessions.get(id);
|
|
2942
|
+
return session === void 0 ? void 0 : clone(session);
|
|
2943
|
+
},
|
|
2944
|
+
async save(session) {
|
|
2945
|
+
sessions.set(session.id, clone(session));
|
|
2946
|
+
return clone(session);
|
|
2947
|
+
},
|
|
2948
|
+
async branch(parentId, branchOptions = {}) {
|
|
2949
|
+
const parent = sessions.get(parentId);
|
|
2950
|
+
const branched = await this.create({
|
|
2951
|
+
...branchOptions,
|
|
2952
|
+
parentId
|
|
2953
|
+
});
|
|
2954
|
+
if (parent === void 0) return branched;
|
|
2955
|
+
const inherited = {
|
|
2956
|
+
...branched,
|
|
2957
|
+
turns: clone(parent.turns),
|
|
2958
|
+
summaries: clone(parent.summaries),
|
|
2959
|
+
artifactRefs: clone(parent.artifactRefs),
|
|
2960
|
+
planIds: clone(parent.planIds)
|
|
2961
|
+
};
|
|
2962
|
+
sessions.set(inherited.id, clone(inherited));
|
|
2963
|
+
return clone(inherited);
|
|
2964
|
+
},
|
|
2965
|
+
async appendTurn(input) {
|
|
2966
|
+
const existing = sessions.get(input.sessionId) ?? await this.create({ id: input.sessionId });
|
|
2967
|
+
const turn = {
|
|
2968
|
+
id: createTurnId(),
|
|
2969
|
+
task: input.task,
|
|
2970
|
+
artifactRefs: clone(input.artifactRefs),
|
|
2971
|
+
outputArtifactRefs: clone(input.outputArtifactRefs ?? []),
|
|
2972
|
+
...input.planId !== void 0 ? { planId: input.planId } : {},
|
|
2973
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2974
|
+
};
|
|
2975
|
+
const artifactRefs = mergeArtifactRefs(existing.artifactRefs, input.artifactRefs, input.outputArtifactRefs ?? []);
|
|
2976
|
+
const planIds = input.planId === void 0 ? existing.planIds : [...existing.planIds, input.planId];
|
|
2977
|
+
const next = {
|
|
2978
|
+
...existing,
|
|
2979
|
+
turns: [...existing.turns, turn],
|
|
2980
|
+
artifactRefs,
|
|
2981
|
+
planIds,
|
|
2982
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2983
|
+
};
|
|
2984
|
+
sessions.set(next.id, clone(next));
|
|
2985
|
+
return clone(next);
|
|
2986
|
+
}
|
|
2987
|
+
};
|
|
2988
|
+
}
|
|
2989
|
+
function mergeArtifactRefs(current, ...groups) {
|
|
2990
|
+
const byId = new Map(current.map((artifact) => [artifact.id, artifact]));
|
|
2991
|
+
for (const group of groups) for (const artifact of group) byId.set(artifact.id, artifact);
|
|
2992
|
+
return [...byId.values()];
|
|
2993
|
+
}
|
|
2994
|
+
function createSessionId() {
|
|
2995
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return `session:${crypto.randomUUID()}`;
|
|
2996
|
+
return `session:${Date.now()}:${Math.random().toString(16).slice(2)}`;
|
|
2997
|
+
}
|
|
2998
|
+
function createTurnId() {
|
|
2999
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return `turn:${crypto.randomUUID()}`;
|
|
3000
|
+
return `turn:${Date.now()}:${Math.random().toString(16).slice(2)}`;
|
|
3001
|
+
}
|
|
3002
|
+
function clone(value) {
|
|
3003
|
+
try {
|
|
3004
|
+
return structuredClone(value);
|
|
3005
|
+
} catch {
|
|
3006
|
+
return value;
|
|
3007
|
+
}
|
|
3008
|
+
}
|
|
3009
|
+
//#endregion
|
|
3010
|
+
//#region src/storage/local.ts
|
|
3011
|
+
function createLocalArtifactStore(rootDir, options = {}) {
|
|
3012
|
+
const rootPath = rootDir instanceof URL ? fileURLToPath(rootDir) : rootDir;
|
|
3013
|
+
const storeId = options.id ?? "local";
|
|
3014
|
+
return {
|
|
3015
|
+
kind: "artifact-store",
|
|
3016
|
+
id: storeId,
|
|
3017
|
+
async put(artifact) {
|
|
3018
|
+
const artifactDir = artifactDirectory(rootPath, artifact.id);
|
|
3019
|
+
const fingerprint = artifact.fingerprint ?? await fingerprintArtifactValue(artifact.value);
|
|
3020
|
+
const ref = toArtifactRef({
|
|
3021
|
+
...artifact,
|
|
3022
|
+
storage: {
|
|
3023
|
+
storeId,
|
|
3024
|
+
key: artifact.id
|
|
3025
|
+
},
|
|
3026
|
+
...fingerprint !== void 0 ? { fingerprint } : {}
|
|
3027
|
+
});
|
|
3028
|
+
await rm(artifactDir, {
|
|
3029
|
+
recursive: true,
|
|
3030
|
+
force: true
|
|
3031
|
+
});
|
|
3032
|
+
await mkdir(artifactDir, { recursive: true });
|
|
3033
|
+
const payload = await writePayload(artifactDir, artifact.value);
|
|
3034
|
+
const envelope = {
|
|
3035
|
+
version: 1,
|
|
3036
|
+
ref,
|
|
3037
|
+
...payload !== void 0 ? { payload } : {}
|
|
3038
|
+
};
|
|
3039
|
+
await writeFile(metadataPath(rootPath, artifact.id), `${JSON.stringify(envelope, null, 2)}\n`, "utf8");
|
|
3040
|
+
return ref;
|
|
3041
|
+
},
|
|
3042
|
+
async get(id) {
|
|
3043
|
+
return (await readEnvelope(rootPath, id))?.ref;
|
|
3044
|
+
},
|
|
3045
|
+
async load(id) {
|
|
3046
|
+
const envelope = await readEnvelope(rootPath, id);
|
|
3047
|
+
if (envelope === void 0) return;
|
|
3048
|
+
if (envelope.payload === void 0) return envelope.ref;
|
|
3049
|
+
const value = await readPayload(artifactDirectory(rootPath, id), envelope.payload);
|
|
3050
|
+
return {
|
|
3051
|
+
...envelope.ref,
|
|
3052
|
+
value
|
|
3053
|
+
};
|
|
3054
|
+
},
|
|
3055
|
+
async has(id) {
|
|
3056
|
+
try {
|
|
3057
|
+
await stat(metadataPath(rootPath, id));
|
|
3058
|
+
return true;
|
|
3059
|
+
} catch (error) {
|
|
3060
|
+
if (isNotFoundError(error)) return false;
|
|
3061
|
+
throw error;
|
|
3062
|
+
}
|
|
3063
|
+
},
|
|
3064
|
+
async delete(id) {
|
|
3065
|
+
if (!await this.has(id)) return false;
|
|
3066
|
+
await rm(artifactDirectory(rootPath, id), {
|
|
3067
|
+
recursive: true,
|
|
3068
|
+
force: true
|
|
3069
|
+
});
|
|
3070
|
+
return true;
|
|
3071
|
+
},
|
|
3072
|
+
async list() {
|
|
3073
|
+
const artifactsPath = join(rootPath, "artifacts");
|
|
3074
|
+
let entries;
|
|
3075
|
+
try {
|
|
3076
|
+
entries = await readdir(artifactsPath, { withFileTypes: true });
|
|
3077
|
+
} catch (error) {
|
|
3078
|
+
if (isNotFoundError(error)) return [];
|
|
3079
|
+
throw error;
|
|
3080
|
+
}
|
|
3081
|
+
return (await Promise.all(entries.filter((entry) => entry.isDirectory()).map(async (entry) => {
|
|
3082
|
+
return (await readEnvelopeByDirectory(join(artifactsPath, entry.name))).ref;
|
|
3083
|
+
}))).sort((left, right) => left.id.localeCompare(right.id));
|
|
3084
|
+
}
|
|
3085
|
+
};
|
|
3086
|
+
}
|
|
3087
|
+
async function writePayload(artifactDir, value) {
|
|
3088
|
+
if (value === void 0) return;
|
|
3089
|
+
if (value instanceof Uint8Array || value instanceof ArrayBuffer || isBlobLike(value)) {
|
|
3090
|
+
await writeFile(join(artifactDir, "payload.bin"), await toBinaryPayload(value));
|
|
3091
|
+
return {
|
|
3092
|
+
kind: "binary",
|
|
3093
|
+
path: "payload.bin"
|
|
3094
|
+
};
|
|
3095
|
+
}
|
|
3096
|
+
const serialized = JSON.stringify(value, null, 2);
|
|
3097
|
+
if (serialized === void 0) return;
|
|
3098
|
+
await writeFile(join(artifactDir, "payload.json"), `${serialized}\n`, "utf8");
|
|
3099
|
+
return {
|
|
3100
|
+
kind: "json",
|
|
3101
|
+
path: "payload.json"
|
|
3102
|
+
};
|
|
3103
|
+
}
|
|
3104
|
+
async function readPayload(artifactDir, payload) {
|
|
3105
|
+
const payloadPath = join(artifactDir, payload.path);
|
|
3106
|
+
if (payload.kind === "binary") {
|
|
3107
|
+
const bytes = await readFile(payloadPath);
|
|
3108
|
+
return new Uint8Array(bytes);
|
|
3109
|
+
}
|
|
3110
|
+
return JSON.parse(await readFile(payloadPath, "utf8"));
|
|
3111
|
+
}
|
|
3112
|
+
async function readEnvelope(rootPath, id) {
|
|
3113
|
+
try {
|
|
3114
|
+
return JSON.parse(await readFile(metadataPath(rootPath, id), "utf8"));
|
|
3115
|
+
} catch (error) {
|
|
3116
|
+
if (isNotFoundError(error)) return;
|
|
3117
|
+
throw error;
|
|
3118
|
+
}
|
|
3119
|
+
}
|
|
3120
|
+
async function readEnvelopeByDirectory(artifactDir) {
|
|
3121
|
+
return JSON.parse(await readFile(join(artifactDir, "metadata.json"), "utf8"));
|
|
3122
|
+
}
|
|
3123
|
+
async function toBinaryPayload(value) {
|
|
3124
|
+
if (value instanceof Uint8Array) return value;
|
|
3125
|
+
if (value instanceof ArrayBuffer) return new Uint8Array(value);
|
|
3126
|
+
return new Uint8Array(await value.arrayBuffer());
|
|
3127
|
+
}
|
|
3128
|
+
function artifactDirectory(rootPath, id) {
|
|
3129
|
+
return join(rootPath, "artifacts", encodeURIComponent(id));
|
|
3130
|
+
}
|
|
3131
|
+
function metadataPath(rootPath, id) {
|
|
3132
|
+
return join(artifactDirectory(rootPath, id), "metadata.json");
|
|
3133
|
+
}
|
|
3134
|
+
function isBlobLike(value) {
|
|
3135
|
+
return typeof Blob !== "undefined" && value instanceof Blob;
|
|
3136
|
+
}
|
|
3137
|
+
function isNotFoundError(error) {
|
|
3138
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
3139
|
+
}
|
|
3140
|
+
//#endregion
|
|
3141
|
+
//#region src/storage/memory.ts
|
|
3142
|
+
function createMemoryArtifactStore(options = {}) {
|
|
3143
|
+
const storeId = options.id ?? "memory";
|
|
3144
|
+
const artifacts = /* @__PURE__ */ new Map();
|
|
3145
|
+
return {
|
|
3146
|
+
kind: "artifact-store",
|
|
3147
|
+
id: storeId,
|
|
3148
|
+
async put(artifact) {
|
|
3149
|
+
const fingerprint = artifact.fingerprint ?? await fingerprintArtifactValue(artifact.value);
|
|
3150
|
+
const storedArtifact = {
|
|
3151
|
+
...cloneArtifactInput(artifact),
|
|
3152
|
+
storage: {
|
|
3153
|
+
storeId,
|
|
3154
|
+
key: artifact.id
|
|
3155
|
+
},
|
|
3156
|
+
...fingerprint !== void 0 ? { fingerprint } : {}
|
|
3157
|
+
};
|
|
3158
|
+
const ref = toArtifactRef(storedArtifact);
|
|
3159
|
+
artifacts.set(artifact.id, {
|
|
3160
|
+
ref: cloneArtifactRef(ref),
|
|
3161
|
+
artifact: cloneArtifactInput(storedArtifact)
|
|
3162
|
+
});
|
|
3163
|
+
return cloneArtifactRef(ref);
|
|
3164
|
+
},
|
|
3165
|
+
async get(id) {
|
|
3166
|
+
const record = artifacts.get(id);
|
|
3167
|
+
return record === void 0 ? void 0 : cloneArtifactRef(record.ref);
|
|
3168
|
+
},
|
|
3169
|
+
async load(id) {
|
|
3170
|
+
const record = artifacts.get(id);
|
|
3171
|
+
return record === void 0 ? void 0 : cloneArtifactInput(record.artifact);
|
|
3172
|
+
},
|
|
3173
|
+
async has(id) {
|
|
3174
|
+
return artifacts.has(id);
|
|
3175
|
+
},
|
|
3176
|
+
async delete(id) {
|
|
3177
|
+
return artifacts.delete(id);
|
|
3178
|
+
},
|
|
3179
|
+
async list() {
|
|
3180
|
+
return Array.from(artifacts.values(), (record) => cloneArtifactRef(record.ref));
|
|
3181
|
+
}
|
|
3182
|
+
};
|
|
3183
|
+
}
|
|
3184
|
+
function cloneArtifactInput(artifact) {
|
|
3185
|
+
return cloneValue(artifact);
|
|
3186
|
+
}
|
|
3187
|
+
function cloneArtifactRef(ref) {
|
|
3188
|
+
return cloneValue(ref);
|
|
3189
|
+
}
|
|
3190
|
+
function cloneValue(value) {
|
|
3191
|
+
try {
|
|
3192
|
+
return structuredClone(value);
|
|
3193
|
+
} catch {
|
|
3194
|
+
return value;
|
|
3195
|
+
}
|
|
3196
|
+
}
|
|
3197
|
+
//#endregion
|
|
3198
|
+
export { AgentDeniedError, BAND, DEFAULT_CHECKPOINT_BAND, STEP_TRANSITION_EVENT_NAME, STUCK_REASONS, artifact, contract, createAI, createAISdkProvider, createActionHistory, createAnthropicProvider, createCheckpointHook, createCostTracker, createFakeProvider, createGeminiProvider, createGoalProgressTracker, createHookPipeline, createInMemorySigner, createLmStudioProvider, createLocalArtifactStore, createMemoryArtifactStore, createMemoryKeySet, createMemorySessionStore, createNoopAgentHost, createNoopSurvivabilityAdapter, createOpenAICompatibleProvider, createOpenAIProvider, createOpenRouterProvider, createPermissionContext, createPermissionGuardHook, createReceipt, createReplayEnvelope, createTranscriptStore, createXaiProvider, defaultPiiDetectors, defineTool, estimateRouteCost, evalAgentRun, evaluateContractAgainstRoute, evaluateTripwires, formatToolsForProvider, generateEd25519KeyPairJwk, importMcpTools, inv, isTerminal, latticeVersion, materializeReplayEnvelope, output, permissionGuardRegisterOptions, redactArtifactRef, redactPlan, redactReplayEnvelope, replayOffline, rerunLive, runAgent, runTool, toolArtifactRef, toolSchemaToJsonSchema, verifyReceipt };
|
|
3199
|
+
|
|
3200
|
+
//# sourceMappingURL=index.js.map
|