@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
|
@@ -0,0 +1,1359 @@
|
|
|
1
|
+
import mime from "mime";
|
|
2
|
+
import canonicalize from "canonicalize";
|
|
3
|
+
//#region \0rolldown/runtime.js
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __exportAll = (all, no_symbols) => {
|
|
6
|
+
let target = {};
|
|
7
|
+
for (var name in all) __defProp(target, name, {
|
|
8
|
+
get: all[name],
|
|
9
|
+
enumerable: true
|
|
10
|
+
});
|
|
11
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
12
|
+
return target;
|
|
13
|
+
};
|
|
14
|
+
//#endregion
|
|
15
|
+
//#region src/artifacts/metadata.ts
|
|
16
|
+
const textEncoder$1 = new TextEncoder();
|
|
17
|
+
function inferMediaType(value, options) {
|
|
18
|
+
if (options.mediaType !== void 0) return options.mediaType;
|
|
19
|
+
if (isBlobLike(value) && value.type !== "") return value.type;
|
|
20
|
+
if (typeof value === "string") return mime.getType(value) ?? options.defaultMediaType;
|
|
21
|
+
return options.defaultMediaType;
|
|
22
|
+
}
|
|
23
|
+
function measureArtifactValue(value, kind) {
|
|
24
|
+
if (kind === "text" && typeof value === "string") return measureString(value);
|
|
25
|
+
if (kind === "json") {
|
|
26
|
+
const serialized = JSON.stringify(value);
|
|
27
|
+
return serialized === void 0 ? void 0 : measureString(serialized);
|
|
28
|
+
}
|
|
29
|
+
if (isBlobLike(value)) return { bytes: value.size };
|
|
30
|
+
}
|
|
31
|
+
function measureString(value) {
|
|
32
|
+
return {
|
|
33
|
+
characters: value.length,
|
|
34
|
+
bytes: textEncoder$1.encode(value).byteLength
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function isBlobLike(value) {
|
|
38
|
+
return typeof Blob !== "undefined" && value instanceof Blob;
|
|
39
|
+
}
|
|
40
|
+
//#endregion
|
|
41
|
+
//#region src/artifacts/artifact.ts
|
|
42
|
+
const artifact = {
|
|
43
|
+
text(value, options = {}) {
|
|
44
|
+
return createArtifact("text", "inline", value, options, "text/plain");
|
|
45
|
+
},
|
|
46
|
+
json(value, options = {}) {
|
|
47
|
+
return createArtifact("json", "inline", value, options, "application/json");
|
|
48
|
+
},
|
|
49
|
+
file(value, options = {}) {
|
|
50
|
+
return createArtifact("file", "file", value, options);
|
|
51
|
+
},
|
|
52
|
+
image(value, options = {}) {
|
|
53
|
+
return createArtifact("image", "file", value, options);
|
|
54
|
+
},
|
|
55
|
+
audio(value, options = {}) {
|
|
56
|
+
return createArtifact("audio", "file", value, options);
|
|
57
|
+
},
|
|
58
|
+
document(value, options = {}) {
|
|
59
|
+
return createArtifact("document", "file", value, options);
|
|
60
|
+
},
|
|
61
|
+
url(value, options = {}) {
|
|
62
|
+
return createArtifact("url", "url", value.toString(), options);
|
|
63
|
+
},
|
|
64
|
+
toolResult(value, options) {
|
|
65
|
+
return createArtifact("tool-result", "tool", value, {
|
|
66
|
+
...options,
|
|
67
|
+
metadata: {
|
|
68
|
+
...options.metadata,
|
|
69
|
+
toolName: options.toolName,
|
|
70
|
+
...options.callId !== void 0 ? { callId: options.callId } : {}
|
|
71
|
+
}
|
|
72
|
+
}, "application/json");
|
|
73
|
+
},
|
|
74
|
+
derive(input) {
|
|
75
|
+
const { kind, source = "generated", value, parents, transform, ...options } = input;
|
|
76
|
+
return createArtifact(kind, source, value, {
|
|
77
|
+
...options,
|
|
78
|
+
lineage: {
|
|
79
|
+
parents: parents.map(toArtifactRef),
|
|
80
|
+
transform
|
|
81
|
+
}
|
|
82
|
+
}, defaultMediaTypeForKind(kind));
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
function toArtifactRef(input) {
|
|
86
|
+
return {
|
|
87
|
+
id: input.id,
|
|
88
|
+
kind: input.kind,
|
|
89
|
+
source: input.source,
|
|
90
|
+
privacy: input.privacy,
|
|
91
|
+
...input.mediaType !== void 0 ? { mediaType: input.mediaType } : {},
|
|
92
|
+
...input.label !== void 0 ? { label: input.label } : {},
|
|
93
|
+
...input.metadata !== void 0 ? { metadata: input.metadata } : {},
|
|
94
|
+
...input.size !== void 0 ? { size: input.size } : {},
|
|
95
|
+
...input.fingerprint !== void 0 ? { fingerprint: input.fingerprint } : {},
|
|
96
|
+
...input.storage !== void 0 ? { storage: input.storage } : {},
|
|
97
|
+
...input.lineage !== void 0 ? { lineage: input.lineage } : {}
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function isArtifactRef(value) {
|
|
101
|
+
if (!isRecord(value)) return false;
|
|
102
|
+
return typeof value.id === "string" && isArtifactKind(value.kind) && isArtifactSource(value.source) && isArtifactPrivacy(value.privacy);
|
|
103
|
+
}
|
|
104
|
+
function createArtifact(kind, source, value, options, defaultMediaType) {
|
|
105
|
+
const mediaType = inferMediaType(value, {
|
|
106
|
+
kind,
|
|
107
|
+
...options.mediaType !== void 0 ? { mediaType: options.mediaType } : {},
|
|
108
|
+
...defaultMediaType !== void 0 ? { defaultMediaType } : {}
|
|
109
|
+
});
|
|
110
|
+
const size = options.size ?? measureArtifactValue(value, kind);
|
|
111
|
+
return {
|
|
112
|
+
id: options.id ?? createArtifactId(kind),
|
|
113
|
+
kind,
|
|
114
|
+
source,
|
|
115
|
+
value,
|
|
116
|
+
privacy: options.privacy ?? "standard",
|
|
117
|
+
...mediaType !== void 0 ? { mediaType } : {},
|
|
118
|
+
...options.label !== void 0 ? { label: options.label } : {},
|
|
119
|
+
...options.metadata !== void 0 ? { metadata: options.metadata } : {},
|
|
120
|
+
...size !== void 0 ? { size } : {},
|
|
121
|
+
...options.fingerprint !== void 0 ? { fingerprint: options.fingerprint } : {},
|
|
122
|
+
...options.storage !== void 0 ? { storage: options.storage } : {},
|
|
123
|
+
...options.lineage !== void 0 ? { lineage: options.lineage } : {}
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function defaultMediaTypeForKind(kind) {
|
|
127
|
+
switch (kind) {
|
|
128
|
+
case "text": return "text/plain";
|
|
129
|
+
case "json":
|
|
130
|
+
case "tool-result": return "application/json";
|
|
131
|
+
default: return;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function isArtifactKind(value) {
|
|
135
|
+
return value === "text" || value === "json" || value === "file" || value === "image" || value === "audio" || value === "video" || value === "document" || value === "url" || value === "tool-result";
|
|
136
|
+
}
|
|
137
|
+
function isArtifactSource(value) {
|
|
138
|
+
return value === "inline" || value === "file" || value === "url" || value === "generated" || value === `provider-upload` || value === "tool";
|
|
139
|
+
}
|
|
140
|
+
function isArtifactPrivacy(value) {
|
|
141
|
+
return value === "standard" || value === "sensitive" || value === "restricted";
|
|
142
|
+
}
|
|
143
|
+
function isRecord(value) {
|
|
144
|
+
return typeof value === "object" && value !== null;
|
|
145
|
+
}
|
|
146
|
+
function createArtifactId(kind) {
|
|
147
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return `artifact:${kind}:${crypto.randomUUID()}`;
|
|
148
|
+
return `artifact:${kind}:${Date.now()}:${Math.random().toString(16).slice(2)}`;
|
|
149
|
+
}
|
|
150
|
+
//#endregion
|
|
151
|
+
//#region src/contract/bands.ts
|
|
152
|
+
/**
|
|
153
|
+
* Priority bands. Lower number = higher priority (runs first).
|
|
154
|
+
*
|
|
155
|
+
* SAFETY (0) -- safety / breaker hooks; cannot be overridden by lower bands
|
|
156
|
+
* OBSERVABILITY (1) -- logging, metrics, audit; runs after safety, before extension
|
|
157
|
+
* EXTENSION (2) -- user-supplied hooks; runs last
|
|
158
|
+
*
|
|
159
|
+
* Within a band, handlers run in registration order.
|
|
160
|
+
*/
|
|
161
|
+
const BAND = {
|
|
162
|
+
SAFETY: 0,
|
|
163
|
+
OBSERVABILITY: 1,
|
|
164
|
+
EXTENSION: 2
|
|
165
|
+
};
|
|
166
|
+
const BAND_ORDER = [
|
|
167
|
+
BAND.SAFETY,
|
|
168
|
+
BAND.OBSERVABILITY,
|
|
169
|
+
BAND.EXTENSION
|
|
170
|
+
];
|
|
171
|
+
const PIPELINE_FROZEN_ERROR_NAME = "PIPELINE_FROZEN";
|
|
172
|
+
const HOOK_TIMEOUT_EVENT_NAME = "HOOK_TIMEOUT";
|
|
173
|
+
function freezeContext(ctx) {
|
|
174
|
+
let cloned;
|
|
175
|
+
try {
|
|
176
|
+
cloned = structuredClone(ctx);
|
|
177
|
+
} catch {
|
|
178
|
+
cloned = ctx;
|
|
179
|
+
}
|
|
180
|
+
if (typeof cloned === "object" && cloned !== null) Object.freeze(cloned);
|
|
181
|
+
return cloned;
|
|
182
|
+
}
|
|
183
|
+
async function runHandlerWithBudget(record, ctx, controls, emit, event, sessionId) {
|
|
184
|
+
const startedAt = performance.now();
|
|
185
|
+
let timeoutFired = false;
|
|
186
|
+
const budgetMs = record.budgetMs;
|
|
187
|
+
const budgetPromise = new Promise((resolve) => {
|
|
188
|
+
setTimeout(() => {
|
|
189
|
+
timeoutFired = true;
|
|
190
|
+
resolve("__timeout__");
|
|
191
|
+
}, budgetMs);
|
|
192
|
+
});
|
|
193
|
+
const handlerPromise = (async () => {
|
|
194
|
+
try {
|
|
195
|
+
await record.handler(ctx, controls);
|
|
196
|
+
} catch {}
|
|
197
|
+
return "__done__";
|
|
198
|
+
})();
|
|
199
|
+
if (await Promise.race([handlerPromise, budgetPromise]) === "__timeout__" && timeoutFired) {
|
|
200
|
+
const elapsedMs = Math.round(performance.now() - startedAt);
|
|
201
|
+
if (emit !== void 0) emit(HOOK_TIMEOUT_EVENT_NAME, {
|
|
202
|
+
event,
|
|
203
|
+
band: record.band,
|
|
204
|
+
budgetMs,
|
|
205
|
+
...sessionId !== void 0 ? { sessionId } : {},
|
|
206
|
+
handlerIndex: record.registrationIndex,
|
|
207
|
+
elapsedMs
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Factory: build a fresh hook pipeline.
|
|
213
|
+
*/
|
|
214
|
+
function createHookPipeline(options) {
|
|
215
|
+
const tracer = options?.tracer;
|
|
216
|
+
const sessionId = options?.sessionId;
|
|
217
|
+
const defaultBudgetMs = options?.defaultBudgetMs ?? 100;
|
|
218
|
+
const registry = /* @__PURE__ */ new Map();
|
|
219
|
+
let frozen = false;
|
|
220
|
+
let globalRegistrationCounter = 0;
|
|
221
|
+
let currentDenialReason = null;
|
|
222
|
+
const emit = tracer !== void 0 ? (kind, payload) => {
|
|
223
|
+
tracer.event?.(kind, payload);
|
|
224
|
+
} : void 0;
|
|
225
|
+
function register(event, handler, opts) {
|
|
226
|
+
if (frozen) {
|
|
227
|
+
const err = /* @__PURE__ */ new Error("HookPipeline.register() called after freeze()");
|
|
228
|
+
err.name = PIPELINE_FROZEN_ERROR_NAME;
|
|
229
|
+
throw err;
|
|
230
|
+
}
|
|
231
|
+
let perEventBands = registry.get(event);
|
|
232
|
+
if (perEventBands === void 0) {
|
|
233
|
+
perEventBands = /* @__PURE__ */ new Map();
|
|
234
|
+
registry.set(event, perEventBands);
|
|
235
|
+
}
|
|
236
|
+
let arr = perEventBands.get(opts.band);
|
|
237
|
+
if (arr === void 0) {
|
|
238
|
+
arr = [];
|
|
239
|
+
perEventBands.set(opts.band, arr);
|
|
240
|
+
}
|
|
241
|
+
const record = {
|
|
242
|
+
handler,
|
|
243
|
+
...opts.matcher !== void 0 ? { matcher: opts.matcher } : {},
|
|
244
|
+
budgetMs: opts.budgetMs ?? defaultBudgetMs,
|
|
245
|
+
band: opts.band,
|
|
246
|
+
registrationIndex: globalRegistrationCounter
|
|
247
|
+
};
|
|
248
|
+
globalRegistrationCounter += 1;
|
|
249
|
+
arr.push(record);
|
|
250
|
+
}
|
|
251
|
+
function freezePipeline() {
|
|
252
|
+
frozen = true;
|
|
253
|
+
}
|
|
254
|
+
function isFrozen() {
|
|
255
|
+
return frozen;
|
|
256
|
+
}
|
|
257
|
+
async function run(event, context) {
|
|
258
|
+
currentDenialReason = null;
|
|
259
|
+
const perEventBands = registry.get(event);
|
|
260
|
+
if (perEventBands === void 0) return;
|
|
261
|
+
const controls = { deny: (reason) => {
|
|
262
|
+
currentDenialReason = reason;
|
|
263
|
+
} };
|
|
264
|
+
for (const band of BAND_ORDER) {
|
|
265
|
+
const arr = perEventBands.get(band);
|
|
266
|
+
if (arr === void 0 || arr.length === 0) continue;
|
|
267
|
+
for (const record of arr) {
|
|
268
|
+
if (record.matcher !== void 0 && !record.matcher.test(event)) continue;
|
|
269
|
+
await runHandlerWithBudget(record, freezeContext(context), controls, emit, event, sessionId);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
function lastDenialReason() {
|
|
274
|
+
return currentDenialReason;
|
|
275
|
+
}
|
|
276
|
+
return {
|
|
277
|
+
kind: "hook-pipeline",
|
|
278
|
+
register,
|
|
279
|
+
freeze: freezePipeline,
|
|
280
|
+
isFrozen,
|
|
281
|
+
run,
|
|
282
|
+
lastDenialReason
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
//#endregion
|
|
286
|
+
//#region src/receipts/canonical.ts
|
|
287
|
+
const encoder = new TextEncoder();
|
|
288
|
+
/**
|
|
289
|
+
* Convert costUsd (number | null) to its canonical string form.
|
|
290
|
+
* RFC 8785 requires deterministic float-to-string; using JS Number→string
|
|
291
|
+
* directly is unsafe across V8 versions (Grisu3 vs Dragonbox). We pin the
|
|
292
|
+
* format by routing through Number.prototype.toString() for FINITE numbers
|
|
293
|
+
* only, and treat NaN/Infinity as null. This matches "I-JSON only" from
|
|
294
|
+
* 09-CONTEXT.md — receipts NEVER carry non-finite floats.
|
|
295
|
+
*/
|
|
296
|
+
function stringifyCostUsd(costUsd) {
|
|
297
|
+
if (costUsd === null) return null;
|
|
298
|
+
if (!Number.isFinite(costUsd)) return null;
|
|
299
|
+
return costUsd.toString();
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Convert a runtime Usage (number costUsd) to its canonical receipt form
|
|
303
|
+
* (string costUsd). This is the single conversion site — canonical bytes
|
|
304
|
+
* NEVER see a raw float in the cost field.
|
|
305
|
+
*/
|
|
306
|
+
function usageToCanonical(usage) {
|
|
307
|
+
return {
|
|
308
|
+
promptTokens: usage.promptTokens,
|
|
309
|
+
completionTokens: usage.completionTokens,
|
|
310
|
+
costUsd: stringifyCostUsd(usage.costUsd)
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Canonicalize a receipt body to JCS bytes (RFC 8785).
|
|
315
|
+
*
|
|
316
|
+
* INVARIANT: callers MUST pass an already-redacted body. The redactor in
|
|
317
|
+
* redact.ts produces the input to this function — never the cleartext.
|
|
318
|
+
* See 09-CONTEXT.md "Redact-Then-Sign Ordering (UNRETROFITTABLE)".
|
|
319
|
+
*
|
|
320
|
+
* Throws if canonicalize returns undefined (impossible for valid bodies
|
|
321
|
+
* — surfaces a programmer error rather than silently producing zero
|
|
322
|
+
* bytes that would later fail signature verification).
|
|
323
|
+
*/
|
|
324
|
+
function canonicalizeReceiptBody(body) {
|
|
325
|
+
const json = canonicalize(body);
|
|
326
|
+
if (json === void 0) throw new Error("canonicalizeReceiptBody: canonicalize returned undefined; receipt body contained a non-canonicalizable value (function/symbol/undefined).");
|
|
327
|
+
return encoder.encode(json);
|
|
328
|
+
}
|
|
329
|
+
//#endregion
|
|
330
|
+
//#region src/receipts/envelope.ts
|
|
331
|
+
const PAYLOAD_TYPE = "application/vnd.lattice.receipt+json";
|
|
332
|
+
const textEncoder = new TextEncoder();
|
|
333
|
+
function base64Encode(bytes) {
|
|
334
|
+
return Buffer.from(bytes).toString("base64");
|
|
335
|
+
}
|
|
336
|
+
function base64Decode(value) {
|
|
337
|
+
return new Uint8Array(Buffer.from(value, "base64"));
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* DSSE v1.0 Pre-Authentication Encoding.
|
|
341
|
+
*
|
|
342
|
+
* Reference: https://github.com/secure-systems-lab/dsse/blob/v1.0.0/protocol.md
|
|
343
|
+
*
|
|
344
|
+
* PAE = UTF-8("DSSEv1 " + len(payloadType) + " " + payloadType
|
|
345
|
+
* + " " + len(payload) + " " + payload)
|
|
346
|
+
*
|
|
347
|
+
* `payload` here is the BASE64-encoded string per DSSE v1.0 spec (NOT raw
|
|
348
|
+
* canonical bytes). Both signing and verification MUST construct PAE the
|
|
349
|
+
* same way; this module is the single source of truth.
|
|
350
|
+
*
|
|
351
|
+
* ASCII length is decimal (no zero-padding). e.g. length 1000 → "1000".
|
|
352
|
+
*/
|
|
353
|
+
function buildPae(payloadType, payloadBase64) {
|
|
354
|
+
const ascii = "DSSEv1 " + payloadType.length.toString() + " " + payloadType + " " + payloadBase64.length.toString() + " " + payloadBase64;
|
|
355
|
+
return textEncoder.encode(ascii);
|
|
356
|
+
}
|
|
357
|
+
function encodeEnvelope(input) {
|
|
358
|
+
return {
|
|
359
|
+
payloadType: PAYLOAD_TYPE,
|
|
360
|
+
payload: base64Encode(input.payloadBytes),
|
|
361
|
+
signatures: input.signatures.map((entry) => ({
|
|
362
|
+
keyid: entry.keyid,
|
|
363
|
+
sig: base64Encode(entry.sig)
|
|
364
|
+
}))
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
function decodeEnvelope(envelope) {
|
|
368
|
+
if (envelope.payloadType !== "application/vnd.lattice.receipt+json") throw new Error(`envelope payloadType mismatch: expected "${PAYLOAD_TYPE}" got "${envelope.payloadType}"`);
|
|
369
|
+
return {
|
|
370
|
+
payloadType: envelope.payloadType,
|
|
371
|
+
payloadBytes: base64Decode(envelope.payload),
|
|
372
|
+
signatures: envelope.signatures.map((entry) => ({
|
|
373
|
+
keyid: entry.keyid,
|
|
374
|
+
sig: base64Decode(entry.sig)
|
|
375
|
+
}))
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
//#endregion
|
|
379
|
+
//#region src/receipts/redact.ts
|
|
380
|
+
/**
|
|
381
|
+
* Default redaction policy id for v1.1. Free-form string per
|
|
382
|
+
* 09-CONTEXT.md — registry enforcement deferred to v1.2.
|
|
383
|
+
*/
|
|
384
|
+
const DEFAULT_REDACTION_POLICY_ID = "lattice.default.v1";
|
|
385
|
+
/**
|
|
386
|
+
* Redact a receipt body BEFORE canonicalization (and BEFORE signing).
|
|
387
|
+
*
|
|
388
|
+
* The signed digest commits to canonicalize(redact(body)). NEVER the
|
|
389
|
+
* other way around. See 09-CONTEXT.md "Redact-Then-Sign Ordering
|
|
390
|
+
* (UNRETROFITTABLE)" and PITFALLS.md Pitfall #1.
|
|
391
|
+
*
|
|
392
|
+
* For v1.1 the default policy is minimal — the heavy lifting already
|
|
393
|
+
* happened upstream:
|
|
394
|
+
* - Tripwire evaluator emits {detector, substring} for no-pii (T-08-01).
|
|
395
|
+
* - Provider responses are hashed into inputHashes/outputHash, never
|
|
396
|
+
* embedded raw.
|
|
397
|
+
* - Router reject messages do not contain PII by construction.
|
|
398
|
+
*
|
|
399
|
+
* This function therefore primarily:
|
|
400
|
+
* 1. Materializes the redactions[] manifest declaring what WAS elided
|
|
401
|
+
* upstream (so receipts are self-describing).
|
|
402
|
+
* 2. Provides the extension point future policies will use.
|
|
403
|
+
*
|
|
404
|
+
* Returns a NEW body — never mutates the input.
|
|
405
|
+
*/
|
|
406
|
+
function redactReceiptBody(body, policyId = DEFAULT_REDACTION_POLICY_ID) {
|
|
407
|
+
const redactions = [];
|
|
408
|
+
if (body.tripwireEvidence !== void 0 && body.tripwireEvidence.kind === "no-pii") redactions.push({
|
|
409
|
+
path: "tripwireEvidence.observed",
|
|
410
|
+
reason: "no-pii-detector-substring-only"
|
|
411
|
+
});
|
|
412
|
+
const sorted = [...redactions].sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
413
|
+
return {
|
|
414
|
+
body: {
|
|
415
|
+
...body,
|
|
416
|
+
redactionPolicyId: policyId,
|
|
417
|
+
redactions: sorted
|
|
418
|
+
},
|
|
419
|
+
redactions: sorted
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
//#endregion
|
|
423
|
+
//#region src/receipts/receipt.ts
|
|
424
|
+
/**
|
|
425
|
+
* Build, redact, canonicalize, sign, and envelope a CapabilityReceipt.
|
|
426
|
+
*
|
|
427
|
+
* Ordering INVARIANT (09-CONTEXT.md, PITFALLS.md Pitfall #1):
|
|
428
|
+
* redact -> canonicalize -> PAE -> sign -> encode
|
|
429
|
+
*
|
|
430
|
+
* The signed digest commits to canonicalize(redact(body)). The function
|
|
431
|
+
* structure makes any other ordering impossible to write by accident —
|
|
432
|
+
* canonicalizeReceiptBody is ONLY called on the output of redactReceiptBody.
|
|
433
|
+
*
|
|
434
|
+
* Defense in depth:
|
|
435
|
+
* - body.kid is assigned from signer.kid, never from input (input has no
|
|
436
|
+
* kid field). The signed body and the envelope keyid CANNOT disagree by
|
|
437
|
+
* construction.
|
|
438
|
+
* - signer.kid is also written to envelope.signatures[0].keyid, so the
|
|
439
|
+
* verifier can cross-check (Step 7 of verifyReceipt).
|
|
440
|
+
*
|
|
441
|
+
* I-JSON guarantees: usage.costUsd is converted to string (or null) via
|
|
442
|
+
* usageToCanonical. Receipts NEVER carry raw floats in the canonical form.
|
|
443
|
+
*/
|
|
444
|
+
async function createReceipt(input, signer) {
|
|
445
|
+
const policyId = input.redactionPolicyId ?? "lattice.default.v1";
|
|
446
|
+
const receiptId = input.receiptId ?? crypto.randomUUID();
|
|
447
|
+
const issuedAt = input.issuedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
448
|
+
const { body } = redactReceiptBody({
|
|
449
|
+
version: "lattice-receipt/v1.1",
|
|
450
|
+
receiptId,
|
|
451
|
+
runId: input.runId,
|
|
452
|
+
issuedAt,
|
|
453
|
+
kid: signer.kid,
|
|
454
|
+
model: input.model,
|
|
455
|
+
route: input.route,
|
|
456
|
+
usage: usageToCanonical(input.usage),
|
|
457
|
+
contractVerdict: input.contractVerdict,
|
|
458
|
+
contractHash: input.contractHash,
|
|
459
|
+
inputHashes: input.inputHashes,
|
|
460
|
+
outputHash: input.outputHash,
|
|
461
|
+
redactionPolicyId: policyId,
|
|
462
|
+
redactions: [],
|
|
463
|
+
...input.noRouteReasons !== void 0 ? { noRouteReasons: input.noRouteReasons } : {},
|
|
464
|
+
...input.tripwireEvidence !== void 0 ? { tripwireEvidence: input.tripwireEvidence } : {},
|
|
465
|
+
...input.stepName !== void 0 ? { stepName: input.stepName } : {},
|
|
466
|
+
...input.stepIndex !== void 0 ? { stepIndex: input.stepIndex } : {},
|
|
467
|
+
...input.parentStepName !== void 0 ? { parentStepName: input.parentStepName } : {},
|
|
468
|
+
...input.previousStepName !== void 0 ? { previousStepName: input.previousStepName } : {},
|
|
469
|
+
...input.sessionId !== void 0 ? { sessionId: input.sessionId } : {},
|
|
470
|
+
...input.timestamp !== void 0 ? { timestamp: input.timestamp } : {}
|
|
471
|
+
}, policyId);
|
|
472
|
+
const payloadBytes = canonicalizeReceiptBody(body);
|
|
473
|
+
const pae = buildPae(PAYLOAD_TYPE, base64Encode(payloadBytes));
|
|
474
|
+
const sig = await signer.sign(pae);
|
|
475
|
+
return encodeEnvelope({
|
|
476
|
+
payloadBytes,
|
|
477
|
+
signatures: [{
|
|
478
|
+
keyid: signer.kid,
|
|
479
|
+
sig
|
|
480
|
+
}]
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
//#endregion
|
|
484
|
+
//#region src/contract/checkpoint.ts
|
|
485
|
+
/**
|
|
486
|
+
* The tracer event name Lattice's checkpoint hook emits per step transition.
|
|
487
|
+
* Identical to the literal added to RunEventKind in tracing.ts.
|
|
488
|
+
*/
|
|
489
|
+
const STEP_TRANSITION_EVENT_NAME = "step.transition";
|
|
490
|
+
/**
|
|
491
|
+
* Default band convention for the checkpoint hook (D-06). The caller is
|
|
492
|
+
* free to register in a different band but the documented convention is
|
|
493
|
+
* OBSERVABILITY -- between SAFETY (runs first) and EXTENSION (runs last).
|
|
494
|
+
*/
|
|
495
|
+
const DEFAULT_CHECKPOINT_BAND = BAND.OBSERVABILITY;
|
|
496
|
+
const DEFAULT_MODEL = {
|
|
497
|
+
requested: "lattice-checkpoint/observability",
|
|
498
|
+
observed: null
|
|
499
|
+
};
|
|
500
|
+
const DEFAULT_ROUTE = {
|
|
501
|
+
providerId: "lattice-checkpoint",
|
|
502
|
+
capabilityId: "lattice-checkpoint/step-transition",
|
|
503
|
+
attemptNumber: 1
|
|
504
|
+
};
|
|
505
|
+
const DEFAULT_USAGE = {
|
|
506
|
+
promptTokens: 0,
|
|
507
|
+
completionTokens: 0,
|
|
508
|
+
costUsd: null
|
|
509
|
+
};
|
|
510
|
+
/**
|
|
511
|
+
* Build a checkpoint hook handler.
|
|
512
|
+
*
|
|
513
|
+
* The returned handler is suitable for registration on a HookPipeline
|
|
514
|
+
* created via createHookPipeline (see ./bands.ts). The handler does not
|
|
515
|
+
* auto-register; the caller passes it to pipeline.register(...) with the
|
|
516
|
+
* desired lifecycle event (typically AFTER_TOOL or BEFORE_TOOL) and band
|
|
517
|
+
* (typically BAND.OBSERVABILITY, exported as DEFAULT_CHECKPOINT_BAND).
|
|
518
|
+
*
|
|
519
|
+
* Per-invocation behavior:
|
|
520
|
+
* 1. Build event metadata from options + per-call context.
|
|
521
|
+
* 2. If signer is configured, attempt createReceipt(...) inside a
|
|
522
|
+
* try/catch. On success, set metadata.receiptId + metadata.envelope.
|
|
523
|
+
* On failure, set metadata.mintError (string from caught error).
|
|
524
|
+
* 3. If tracer is configured, emit exactly one tracer.event?.(
|
|
525
|
+
* STEP_TRANSITION_EVENT_NAME, metadata) call.
|
|
526
|
+
* 4. Return void.
|
|
527
|
+
*
|
|
528
|
+
* NO upstream throw (D-07). NO global mutation (D-05).
|
|
529
|
+
*/
|
|
530
|
+
function createCheckpointHook(options) {
|
|
531
|
+
const runId = options.runId;
|
|
532
|
+
const tracer = options.tracer;
|
|
533
|
+
const signer = options.signer;
|
|
534
|
+
const sessionId = options.sessionId;
|
|
535
|
+
const model = options.model ?? DEFAULT_MODEL;
|
|
536
|
+
const route = options.route ?? DEFAULT_ROUTE;
|
|
537
|
+
const contractVerdict = options.contractVerdict ?? "success";
|
|
538
|
+
return async function checkpointHookHandler(ctx) {
|
|
539
|
+
const baseMetadata = {
|
|
540
|
+
runId,
|
|
541
|
+
stepName: ctx.stepName,
|
|
542
|
+
stepIndex: ctx.stepIndex,
|
|
543
|
+
timestamp: ctx.timestamp,
|
|
544
|
+
...ctx.parentStepName !== void 0 ? { parentStepName: ctx.parentStepName } : {},
|
|
545
|
+
...ctx.previousStepName !== void 0 ? { previousStepName: ctx.previousStepName } : {},
|
|
546
|
+
...sessionId !== void 0 ? { sessionId } : {}
|
|
547
|
+
};
|
|
548
|
+
let envelope;
|
|
549
|
+
let receiptId;
|
|
550
|
+
let mintError;
|
|
551
|
+
if (signer !== void 0) try {
|
|
552
|
+
envelope = await createReceipt({
|
|
553
|
+
runId,
|
|
554
|
+
model,
|
|
555
|
+
route,
|
|
556
|
+
usage: DEFAULT_USAGE,
|
|
557
|
+
contractVerdict,
|
|
558
|
+
contractHash: null,
|
|
559
|
+
inputHashes: [],
|
|
560
|
+
outputHash: null,
|
|
561
|
+
stepName: ctx.stepName,
|
|
562
|
+
stepIndex: ctx.stepIndex,
|
|
563
|
+
timestamp: ctx.timestamp,
|
|
564
|
+
...ctx.parentStepName !== void 0 ? { parentStepName: ctx.parentStepName } : {},
|
|
565
|
+
...ctx.previousStepName !== void 0 ? { previousStepName: ctx.previousStepName } : {},
|
|
566
|
+
...sessionId !== void 0 ? { sessionId } : {}
|
|
567
|
+
}, signer);
|
|
568
|
+
receiptId = extractReceiptId(envelope);
|
|
569
|
+
} catch (err) {
|
|
570
|
+
mintError = err instanceof Error ? err.message : String(err);
|
|
571
|
+
}
|
|
572
|
+
const metadata = {
|
|
573
|
+
...baseMetadata,
|
|
574
|
+
...receiptId !== void 0 ? { receiptId } : {},
|
|
575
|
+
...envelope !== void 0 ? { envelope } : {},
|
|
576
|
+
...mintError !== void 0 ? { mintError } : {}
|
|
577
|
+
};
|
|
578
|
+
tracer?.event?.(STEP_TRANSITION_EVENT_NAME, metadata);
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Decode the canonical payload of a freshly-minted envelope and return
|
|
583
|
+
* its receiptId. The envelope's payload is base64-encoded JSON of the
|
|
584
|
+
* signed body (DSSE v1.0 form); receiptId is a top-level field.
|
|
585
|
+
*
|
|
586
|
+
* Returns undefined if decoding fails (defensive -- the handler still
|
|
587
|
+
* emits the tracer event with the envelope itself so subscribers can
|
|
588
|
+
* re-derive the id if they want).
|
|
589
|
+
*/
|
|
590
|
+
function extractReceiptId(envelope) {
|
|
591
|
+
try {
|
|
592
|
+
const bytes = Uint8Array.from(atob(envelope.payload), (c) => c.charCodeAt(0));
|
|
593
|
+
const body = JSON.parse(new TextDecoder().decode(bytes));
|
|
594
|
+
return typeof body.receiptId === "string" ? body.receiptId : void 0;
|
|
595
|
+
} catch {
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
//#endregion
|
|
600
|
+
//#region src/runtime/survivability.ts
|
|
601
|
+
/**
|
|
602
|
+
* Reference implementation of SurvivabilityAdapter<TState>. Records
|
|
603
|
+
* eviction events but does NOT persist; serialize / deserialize round-
|
|
604
|
+
* trip via JSON.stringify / JSON.parse. Analog to createFakeProvider
|
|
605
|
+
* in the providers/ module -- gives Lattice's vitest a complete shape-
|
|
606
|
+
* conformance target before the real (FSB-side) adapter ships in
|
|
607
|
+
* Plan 05-05.
|
|
608
|
+
*
|
|
609
|
+
* Per CONTEXT.md D-11 the noop adapter ships in Lattice (not FSB)
|
|
610
|
+
* because it covers the contract surface in Lattice's own test suite;
|
|
611
|
+
* FSB's real chrome.storage.session-backed adapter is glue layer.
|
|
612
|
+
*/
|
|
613
|
+
function createNoopSurvivabilityAdapter(options = {}) {
|
|
614
|
+
const id = options.id ?? "noop-survivability";
|
|
615
|
+
const defaultPolicy = options.policy ?? "SAFE";
|
|
616
|
+
const hooks = /* @__PURE__ */ new Set();
|
|
617
|
+
return {
|
|
618
|
+
kind: "survivability-adapter",
|
|
619
|
+
id,
|
|
620
|
+
serialize(state) {
|
|
621
|
+
return {
|
|
622
|
+
kind: "survivability-snapshot",
|
|
623
|
+
version: "lattice-survivability/v1",
|
|
624
|
+
payload: JSON.stringify(state ?? null),
|
|
625
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
626
|
+
};
|
|
627
|
+
},
|
|
628
|
+
deserialize(snapshot) {
|
|
629
|
+
return JSON.parse(snapshot.payload);
|
|
630
|
+
},
|
|
631
|
+
onEviction(hook) {
|
|
632
|
+
hooks.add(hook);
|
|
633
|
+
let unsubscribed = false;
|
|
634
|
+
return () => {
|
|
635
|
+
if (unsubscribed) return;
|
|
636
|
+
unsubscribed = true;
|
|
637
|
+
hooks.delete(hook);
|
|
638
|
+
};
|
|
639
|
+
},
|
|
640
|
+
async resume(_snapshot) {
|
|
641
|
+
return defaultPolicy;
|
|
642
|
+
}
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
//#endregion
|
|
646
|
+
//#region src/outputs/validate.ts
|
|
647
|
+
async function validateSchemaOutput(name, schema, value) {
|
|
648
|
+
const result = schema["~standard"].validate(value);
|
|
649
|
+
const validation = result instanceof Promise ? await result : result;
|
|
650
|
+
if (validation.issues) return {
|
|
651
|
+
ok: false,
|
|
652
|
+
issue: {
|
|
653
|
+
["output"]: name,
|
|
654
|
+
issues: validation.issues.map(normalizeIssue)
|
|
655
|
+
}
|
|
656
|
+
};
|
|
657
|
+
return {
|
|
658
|
+
ok: true,
|
|
659
|
+
value: validation.value
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
async function validateOutputMap(contracts, rawOutputs, plan) {
|
|
663
|
+
const outputs = {};
|
|
664
|
+
for (const [name, contract] of Object.entries(contracts)) {
|
|
665
|
+
const value = rawOutputs[name];
|
|
666
|
+
const issue = await validateOutput(name, contract, value);
|
|
667
|
+
if (!issue.ok) return {
|
|
668
|
+
ok: false,
|
|
669
|
+
error: {
|
|
670
|
+
kind: "validation",
|
|
671
|
+
message: `Invalid output "${name}".`,
|
|
672
|
+
["output"]: name,
|
|
673
|
+
issues: issue.issues
|
|
674
|
+
},
|
|
675
|
+
usage: {
|
|
676
|
+
promptTokens: 0,
|
|
677
|
+
completionTokens: 0,
|
|
678
|
+
costUsd: null
|
|
679
|
+
},
|
|
680
|
+
raw: rawOutputs,
|
|
681
|
+
partialOutputs: outputs,
|
|
682
|
+
plan
|
|
683
|
+
};
|
|
684
|
+
outputs[name] = issue.value;
|
|
685
|
+
}
|
|
686
|
+
return {
|
|
687
|
+
ok: true,
|
|
688
|
+
outputs,
|
|
689
|
+
artifacts: [],
|
|
690
|
+
usage: {
|
|
691
|
+
promptTokens: 0,
|
|
692
|
+
completionTokens: 0,
|
|
693
|
+
costUsd: null
|
|
694
|
+
},
|
|
695
|
+
plan
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
async function validateOutput(name, contract, value) {
|
|
699
|
+
if (contract === "text") {
|
|
700
|
+
if (typeof value !== "string") return {
|
|
701
|
+
ok: false,
|
|
702
|
+
issues: [{ message: "Expected text output to be a string." }]
|
|
703
|
+
};
|
|
704
|
+
return {
|
|
705
|
+
ok: true,
|
|
706
|
+
value
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
if (isStandardSchema(contract)) {
|
|
710
|
+
const result = await validateSchemaOutput(name, contract, value);
|
|
711
|
+
if (!result.ok) return {
|
|
712
|
+
ok: false,
|
|
713
|
+
issues: result.issue.issues
|
|
714
|
+
};
|
|
715
|
+
return {
|
|
716
|
+
ok: true,
|
|
717
|
+
value: result.value
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
if (contract.kind === "citations") {
|
|
721
|
+
if (!Array.isArray(value)) return {
|
|
722
|
+
ok: false,
|
|
723
|
+
issues: [{ message: "Expected citations output to be an array." }]
|
|
724
|
+
};
|
|
725
|
+
return {
|
|
726
|
+
ok: true,
|
|
727
|
+
value
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
if (contract.kind === "artifacts") {
|
|
731
|
+
if (!Array.isArray(value)) return {
|
|
732
|
+
ok: false,
|
|
733
|
+
issues: [{ message: "Expected artifacts output to be an array." }]
|
|
734
|
+
};
|
|
735
|
+
for (const item of value) {
|
|
736
|
+
if (!isArtifactRef(item)) return {
|
|
737
|
+
ok: false,
|
|
738
|
+
issues: [{ message: "Expected artifacts output item to be an artifact ref." }]
|
|
739
|
+
};
|
|
740
|
+
if (contract.artifactKind !== void 0 && item.kind !== contract.artifactKind) return {
|
|
741
|
+
ok: false,
|
|
742
|
+
issues: [{ message: `Expected artifacts output item kind to be "${contract.artifactKind}".` }]
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
return {
|
|
746
|
+
ok: true,
|
|
747
|
+
value: value.map(toArtifactRef)
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
return {
|
|
751
|
+
ok: false,
|
|
752
|
+
issues: [{ message: "Unsupported output contract." }]
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
function isStandardSchema(contract) {
|
|
756
|
+
if (typeof contract !== "object" || contract === null) return false;
|
|
757
|
+
return typeof contract["~standard"]?.validate === "function";
|
|
758
|
+
}
|
|
759
|
+
function normalizeIssue(issue) {
|
|
760
|
+
const path = issue.path?.map(normalizePathSegment).filter((segment) => segment !== void 0);
|
|
761
|
+
return {
|
|
762
|
+
message: issue.message,
|
|
763
|
+
...path !== void 0 && path.length > 0 ? { path } : {}
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
function normalizePathSegment(segment) {
|
|
767
|
+
if (typeof segment === "string" || typeof segment === "number" || typeof segment === "symbol") return segment;
|
|
768
|
+
return normalizePathKey(segment.key);
|
|
769
|
+
}
|
|
770
|
+
function normalizePathKey(key) {
|
|
771
|
+
return key;
|
|
772
|
+
}
|
|
773
|
+
//#endregion
|
|
774
|
+
//#region src/tools/tools.ts
|
|
775
|
+
function defineTool(definition) {
|
|
776
|
+
return {
|
|
777
|
+
kind: "tool",
|
|
778
|
+
...definition
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
async function runTool(tool, input, context = {}) {
|
|
782
|
+
const validation = await validateSchemaOutput(tool.name, tool.inputSchema, input);
|
|
783
|
+
if (!validation.ok) throw new Error(`Invalid input for tool "${tool.name}".`);
|
|
784
|
+
const callId = createToolCallId();
|
|
785
|
+
const output = await tool.execute(validation.value, context);
|
|
786
|
+
return {
|
|
787
|
+
callId,
|
|
788
|
+
toolName: tool.name,
|
|
789
|
+
artifact: artifact.toolResult(output, {
|
|
790
|
+
id: `artifact:tool-result:${tool.name}:${callId}`,
|
|
791
|
+
toolName: tool.name,
|
|
792
|
+
callId
|
|
793
|
+
})
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
async function importMcpTools(client, toolNames) {
|
|
797
|
+
const descriptors = await Promise.resolve(client.listTools?.() ?? []);
|
|
798
|
+
const allowed = toolNames === void 0 ? void 0 : new Set(toolNames);
|
|
799
|
+
return descriptors.filter((descriptor) => allowed === void 0 || allowed.has(descriptor.name)).map((descriptor) => defineTool({
|
|
800
|
+
name: descriptor.name,
|
|
801
|
+
...descriptor.description !== void 0 ? { description: descriptor.description } : {},
|
|
802
|
+
inputSchema: descriptor.inputSchema,
|
|
803
|
+
execute: async (input) => client.callTool({
|
|
804
|
+
name: descriptor.name,
|
|
805
|
+
arguments: input
|
|
806
|
+
})
|
|
807
|
+
}));
|
|
808
|
+
}
|
|
809
|
+
function toolArtifactRef(result) {
|
|
810
|
+
const { value: _value, ...ref } = result.artifact;
|
|
811
|
+
return ref;
|
|
812
|
+
}
|
|
813
|
+
function createToolCallId() {
|
|
814
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") return crypto.randomUUID();
|
|
815
|
+
return `${Date.now()}:${Math.random().toString(16).slice(2)}`;
|
|
816
|
+
}
|
|
817
|
+
//#endregion
|
|
818
|
+
//#region src/agent/format-tools.ts
|
|
819
|
+
/**
|
|
820
|
+
* Convert a Standard Schema to a JSON Schema-shaped descriptor suitable for
|
|
821
|
+
* inclusion in an LLM tool description. Standard Schema vendors can
|
|
822
|
+
* optionally expose `toJSONSchema` on their schema objects; when absent,
|
|
823
|
+
* we fall back to a minimal structural description that lists the schema
|
|
824
|
+
* vendor + version + a placeholder. Models tolerate placeholder schemas in
|
|
825
|
+
* practice because the tool description is supplementary — what matters
|
|
826
|
+
* is the envelope contract (`{tool_call: {name, args}}`).
|
|
827
|
+
*/
|
|
828
|
+
function toolSchemaToJsonSchema(schema) {
|
|
829
|
+
const standardSchema = schema["~standard"];
|
|
830
|
+
if (typeof standardSchema === "object" && standardSchema !== null && "vendor" in standardSchema) {
|
|
831
|
+
const vendor = standardSchema;
|
|
832
|
+
const maybeToJson = schema.toJSONSchema;
|
|
833
|
+
if (typeof maybeToJson === "function") try {
|
|
834
|
+
return maybeToJson();
|
|
835
|
+
} catch {}
|
|
836
|
+
return {
|
|
837
|
+
$comment: `standard-schema vendor: ${vendor.vendor}; toJSONSchema not available`,
|
|
838
|
+
type: "object"
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
return {
|
|
842
|
+
$comment: "non-standard-schema input",
|
|
843
|
+
type: "object"
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
/**
|
|
847
|
+
* Builds the prompt-reencoded tool-use protocol handle for any provider.
|
|
848
|
+
*
|
|
849
|
+
* Phase 19 ships a uniform implementation across all 7 logical providers
|
|
850
|
+
* (openai, openai-compat, anthropic, gemini, xai, openrouter, lm-studio).
|
|
851
|
+
* The `providerName` argument is accepted for forward compatibility but
|
|
852
|
+
* does not branch the implementation in v1.2.
|
|
853
|
+
*/
|
|
854
|
+
function formatToolsForProvider(providerName, tools, options = {}) {
|
|
855
|
+
options.mode;
|
|
856
|
+
const system = options.system?.trim() ?? "";
|
|
857
|
+
const toolDescriptions = tools.map((tool) => {
|
|
858
|
+
const schemaDescriptor = toolSchemaToJsonSchema(tool.inputSchema);
|
|
859
|
+
const schemaJson = JSON.stringify(schemaDescriptor, null, 2);
|
|
860
|
+
const desc = tool.description?.trim() ?? "(no description)";
|
|
861
|
+
return `- name: ${tool.name}\n description: ${desc}\n args_schema: ${schemaJson}`;
|
|
862
|
+
}).join("\n");
|
|
863
|
+
const envelopeInstructions = [
|
|
864
|
+
"You are a single-agent loop. You can either:",
|
|
865
|
+
" (a) answer the user directly with a final response, OR",
|
|
866
|
+
" (b) request one or more tool calls.",
|
|
867
|
+
"",
|
|
868
|
+
"To request tool calls, respond with ONE JSON object on a line by itself:",
|
|
869
|
+
" {\"tool_calls\": [{\"id\": \"...\", \"name\": \"tool_name\", \"args\": {...}}]}",
|
|
870
|
+
"Each tool_call needs a unique id (any string). The args MUST match the tool's args_schema.",
|
|
871
|
+
"",
|
|
872
|
+
"To answer directly, respond with a final answer in natural language with NO JSON envelope.",
|
|
873
|
+
"Do not mix a final answer and a tool_calls envelope in the same response."
|
|
874
|
+
].join("\n");
|
|
875
|
+
const systemBlock = [
|
|
876
|
+
system,
|
|
877
|
+
"",
|
|
878
|
+
"Available tools:",
|
|
879
|
+
toolDescriptions || "(none)",
|
|
880
|
+
"",
|
|
881
|
+
envelopeInstructions
|
|
882
|
+
].filter((s) => s !== "" || true).join("\n").replace(/^\n+/, "").trimEnd();
|
|
883
|
+
function buildTask(conversation) {
|
|
884
|
+
const lines = [];
|
|
885
|
+
lines.push(systemBlock);
|
|
886
|
+
lines.push("");
|
|
887
|
+
lines.push("---");
|
|
888
|
+
lines.push("");
|
|
889
|
+
for (const turn of conversation) {
|
|
890
|
+
if (turn.role === "user") lines.push(`USER:\n${turn.content}`);
|
|
891
|
+
else if (turn.role === "assistant") lines.push(`ASSISTANT:\n${turn.content}`);
|
|
892
|
+
else {
|
|
893
|
+
const idHint = turn.toolCallId !== void 0 ? ` id=${turn.toolCallId}` : "";
|
|
894
|
+
const nameHint = turn.toolName !== void 0 ? ` name=${turn.toolName}` : "";
|
|
895
|
+
lines.push(`TOOL_RESULT (${nameHint.trim() || "tool"}${idHint}):\n${turn.content}`);
|
|
896
|
+
}
|
|
897
|
+
lines.push("");
|
|
898
|
+
}
|
|
899
|
+
lines.push("ASSISTANT:");
|
|
900
|
+
return lines.join("\n");
|
|
901
|
+
}
|
|
902
|
+
function parseToolUse(responseText) {
|
|
903
|
+
if (typeof responseText !== "string" || responseText.length === 0) return null;
|
|
904
|
+
const candidates = extractJsonCandidates(responseText);
|
|
905
|
+
for (const candidate of candidates) {
|
|
906
|
+
const parsed = tryParseEnvelope(candidate);
|
|
907
|
+
if (parsed !== null) return parsed;
|
|
908
|
+
}
|
|
909
|
+
return null;
|
|
910
|
+
}
|
|
911
|
+
function describeForSystem() {
|
|
912
|
+
return systemBlock;
|
|
913
|
+
}
|
|
914
|
+
return {
|
|
915
|
+
buildTask,
|
|
916
|
+
parseToolUse,
|
|
917
|
+
describeForSystem,
|
|
918
|
+
mode: "prompt-reencoded"
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
/**
|
|
922
|
+
* Extracts JSON-looking candidate substrings from a response text.
|
|
923
|
+
*
|
|
924
|
+
* Models routinely wrap JSON in markdown code fences (```json ... ```),
|
|
925
|
+
* prepend explanatory prose ("I'll call the search tool: { ... }"), or
|
|
926
|
+
* produce multiple JSON-shaped blobs. This extractor scans for plausible
|
|
927
|
+
* candidates ordered by likelihood.
|
|
928
|
+
*/
|
|
929
|
+
function extractJsonCandidates(text) {
|
|
930
|
+
const candidates = [];
|
|
931
|
+
const fenceRegex = /```(?:json)?\s*([\s\S]*?)```/g;
|
|
932
|
+
let fenceMatch;
|
|
933
|
+
while ((fenceMatch = fenceRegex.exec(text)) !== null) {
|
|
934
|
+
const inner = fenceMatch[1];
|
|
935
|
+
if (inner !== void 0) candidates.push(inner.trim());
|
|
936
|
+
}
|
|
937
|
+
const braceStart = text.indexOf("{");
|
|
938
|
+
const braceEnd = text.lastIndexOf("}");
|
|
939
|
+
if (braceStart !== -1 && braceEnd > braceStart) candidates.push(text.slice(braceStart, braceEnd + 1));
|
|
940
|
+
candidates.push(text.trim());
|
|
941
|
+
return candidates;
|
|
942
|
+
}
|
|
943
|
+
function tryParseEnvelope(jsonLike) {
|
|
944
|
+
let parsed;
|
|
945
|
+
try {
|
|
946
|
+
parsed = JSON.parse(jsonLike);
|
|
947
|
+
} catch {
|
|
948
|
+
return null;
|
|
949
|
+
}
|
|
950
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
951
|
+
const toolCalls = parsed["tool_calls"];
|
|
952
|
+
if (!Array.isArray(toolCalls) || toolCalls.length === 0) return null;
|
|
953
|
+
const requests = [];
|
|
954
|
+
for (const call of toolCalls) {
|
|
955
|
+
if (typeof call !== "object" || call === null) return null;
|
|
956
|
+
const callRecord = call;
|
|
957
|
+
const id = callRecord["id"];
|
|
958
|
+
const name = callRecord["name"];
|
|
959
|
+
const args = callRecord["args"];
|
|
960
|
+
if (typeof id !== "string" || typeof name !== "string") return null;
|
|
961
|
+
requests.push({
|
|
962
|
+
id,
|
|
963
|
+
name,
|
|
964
|
+
args
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
return requests;
|
|
968
|
+
}
|
|
969
|
+
//#endregion
|
|
970
|
+
//#region src/agent/host.ts
|
|
971
|
+
/**
|
|
972
|
+
* Reference implementation suitable for Node tests + the Phase 19 default
|
|
973
|
+
* behavior.
|
|
974
|
+
*
|
|
975
|
+
* - scheduler: resolves immediately (no yield between iterations).
|
|
976
|
+
* - transport: pass-through to provider.execute().
|
|
977
|
+
* - storage: save() / clear() are no-ops; load() always returns null.
|
|
978
|
+
*
|
|
979
|
+
* Equivalent to passing no host at all.
|
|
980
|
+
*/
|
|
981
|
+
function createNoopAgentHost() {
|
|
982
|
+
return {
|
|
983
|
+
kind: "agent-host",
|
|
984
|
+
scheduler: { async scheduleNext(_iterationIndex) {} },
|
|
985
|
+
transport: { async call(provider, request) {
|
|
986
|
+
if (provider.execute === void 0) throw new Error(`AgentTransport: provider ${provider.id} has no execute() method.`);
|
|
987
|
+
return provider.execute(request);
|
|
988
|
+
} },
|
|
989
|
+
storage: {
|
|
990
|
+
async save(_snapshot) {},
|
|
991
|
+
async load() {
|
|
992
|
+
return null;
|
|
993
|
+
},
|
|
994
|
+
async clear() {}
|
|
995
|
+
}
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
//#endregion
|
|
999
|
+
//#region src/agent/types.ts
|
|
1000
|
+
/**
|
|
1001
|
+
* Typed error raised when a SAFETY-band handler sets `controls.deny(reason)`
|
|
1002
|
+
* during `BEFORE_AGENT_ITERATION`. Carries `terminal: true` semantics to
|
|
1003
|
+
* align with v1.1 `TripwireViolationError`: the failure is NOT retried by
|
|
1004
|
+
* the fallback chain.
|
|
1005
|
+
*
|
|
1006
|
+
* Surfaced via `AgentFailure { kind: "agent-iteration-denied", reason, ... }`
|
|
1007
|
+
* — callers can also catch the typed error if they prefer.
|
|
1008
|
+
*/
|
|
1009
|
+
var AgentDeniedError = class extends Error {
|
|
1010
|
+
kind = "agent-iteration-denied";
|
|
1011
|
+
terminal = true;
|
|
1012
|
+
reason;
|
|
1013
|
+
iterationIndex;
|
|
1014
|
+
constructor(reason, iterationIndex) {
|
|
1015
|
+
super(`Agent iteration ${iterationIndex} denied: ${reason}`);
|
|
1016
|
+
this.name = "AgentDeniedError";
|
|
1017
|
+
this.reason = reason;
|
|
1018
|
+
this.iterationIndex = iterationIndex;
|
|
1019
|
+
}
|
|
1020
|
+
};
|
|
1021
|
+
//#endregion
|
|
1022
|
+
//#region src/agent/runtime.ts
|
|
1023
|
+
var runtime_exports = /* @__PURE__ */ __exportAll({ runAgent: () => runAgent });
|
|
1024
|
+
const ZERO_USAGE = {
|
|
1025
|
+
promptTokens: 0,
|
|
1026
|
+
completionTokens: 0,
|
|
1027
|
+
costUsd: null
|
|
1028
|
+
};
|
|
1029
|
+
/**
|
|
1030
|
+
* Resolves the runtime's behaviour for a single `ai.runAgent(intent)` call.
|
|
1031
|
+
*
|
|
1032
|
+
* Phase 19 ships an in-process default scheduler (the loop runs in the
|
|
1033
|
+
* calling Promise), direct transport (provider.execute()), and in-memory
|
|
1034
|
+
* transcript (the `conversation` array). Phase 20 promotes scheduler /
|
|
1035
|
+
* transport / storage to the pluggable `AgentHost` adapter.
|
|
1036
|
+
*/
|
|
1037
|
+
async function runAgent(intent, config = {}) {
|
|
1038
|
+
const startedAt = Date.now();
|
|
1039
|
+
const cumulativeUsage = {
|
|
1040
|
+
promptTokens: 0,
|
|
1041
|
+
completionTokens: 0,
|
|
1042
|
+
costUsd: null
|
|
1043
|
+
};
|
|
1044
|
+
const iterations = [];
|
|
1045
|
+
const host = intent.host ?? createNoopAgentHost();
|
|
1046
|
+
const survivabilityAdapter = intent.survivabilityAdapter ?? createNoopSurvivabilityAdapter();
|
|
1047
|
+
const pipeline = ensurePipeline(intent);
|
|
1048
|
+
maybeAutoRegisterCheckpoint(pipeline, intent);
|
|
1049
|
+
const provider = pickFirstExecutableProvider(config);
|
|
1050
|
+
if (provider === null) return buildFailure({
|
|
1051
|
+
kind: "execution_unavailable",
|
|
1052
|
+
reason: "No provider adapter with execute() is configured.",
|
|
1053
|
+
iterations,
|
|
1054
|
+
usage: cumulativeUsage
|
|
1055
|
+
});
|
|
1056
|
+
let providerName = provider.id;
|
|
1057
|
+
let conversation = [{
|
|
1058
|
+
role: "user",
|
|
1059
|
+
content: intent.task
|
|
1060
|
+
}];
|
|
1061
|
+
const handle = formatToolsForProvider(providerName, intent.tools);
|
|
1062
|
+
const budget = intent.contract?.budget;
|
|
1063
|
+
const maxIterations = budget?.maxIterations ?? Number.POSITIVE_INFINITY;
|
|
1064
|
+
const maxWallTimeMs = budget?.maxWallTimeMs ?? Number.POSITIVE_INFINITY;
|
|
1065
|
+
const maxCostUsd = budget?.maxCostUsd ?? Number.POSITIVE_INFINITY;
|
|
1066
|
+
let iterationIndex = 0;
|
|
1067
|
+
const existingSnapshot = await host.storage?.load();
|
|
1068
|
+
if (existingSnapshot !== null && existingSnapshot !== void 0) {
|
|
1069
|
+
intent.tracer?.event?.("recovery.start", {
|
|
1070
|
+
snapshotVersion: existingSnapshot.version,
|
|
1071
|
+
capturedAt: existingSnapshot.capturedAt
|
|
1072
|
+
});
|
|
1073
|
+
try {
|
|
1074
|
+
const restored = survivabilityAdapter.deserialize(existingSnapshot);
|
|
1075
|
+
iterationIndex = restored.iterationIndex;
|
|
1076
|
+
conversation = [...restored.conversation];
|
|
1077
|
+
cumulativeUsage.promptTokens = restored.cumulativeUsage.promptTokens;
|
|
1078
|
+
cumulativeUsage.completionTokens = restored.cumulativeUsage.completionTokens;
|
|
1079
|
+
cumulativeUsage.costUsd = restored.cumulativeUsage.costUsd;
|
|
1080
|
+
providerName = restored.providerName;
|
|
1081
|
+
intent.tracer?.event?.("recovery.complete", {
|
|
1082
|
+
iterationIndex,
|
|
1083
|
+
providerName
|
|
1084
|
+
});
|
|
1085
|
+
} catch (error) {
|
|
1086
|
+
intent.tracer?.event?.("recovery.failed", { reason: error instanceof Error ? error.message : "deserialize failed" });
|
|
1087
|
+
await host.storage?.clear();
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
while (iterationIndex < maxIterations) {
|
|
1091
|
+
const elapsedMs = Date.now() - startedAt;
|
|
1092
|
+
if (elapsedMs >= maxWallTimeMs) return buildFailure({
|
|
1093
|
+
kind: "agent-wall-time-exceeded",
|
|
1094
|
+
reason: `Wall-time budget ${maxWallTimeMs}ms exceeded after ${elapsedMs}ms`,
|
|
1095
|
+
iterations,
|
|
1096
|
+
usage: cumulativeUsage
|
|
1097
|
+
});
|
|
1098
|
+
if (cumulativeUsage.costUsd !== null && cumulativeUsage.costUsd >= maxCostUsd) return buildFailure({
|
|
1099
|
+
kind: "no-contract-match",
|
|
1100
|
+
reason: `Cost budget $${maxCostUsd} exceeded at $${cumulativeUsage.costUsd}`,
|
|
1101
|
+
iterations,
|
|
1102
|
+
usage: cumulativeUsage
|
|
1103
|
+
});
|
|
1104
|
+
await pipeline.run("BEFORE_AGENT_ITERATION", {
|
|
1105
|
+
iterationIndex,
|
|
1106
|
+
intent,
|
|
1107
|
+
conversation: conversation.map((t) => ({ ...t })),
|
|
1108
|
+
stepName: `agent-iteration-${iterationIndex}-before`,
|
|
1109
|
+
stepIndex: iterationIndex,
|
|
1110
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1111
|
+
...iterationIndex > 0 ? { previousStepName: `agent-iteration-${iterationIndex - 1}-after` } : {}
|
|
1112
|
+
});
|
|
1113
|
+
const denial = pipeline.lastDenialReason();
|
|
1114
|
+
if (denial !== null) {
|
|
1115
|
+
const failedRecord = {
|
|
1116
|
+
index: iterationIndex,
|
|
1117
|
+
provider: providerName,
|
|
1118
|
+
promptTokens: 0,
|
|
1119
|
+
completionTokens: 0,
|
|
1120
|
+
costUsd: null,
|
|
1121
|
+
durationMs: 0,
|
|
1122
|
+
toolCalls: [],
|
|
1123
|
+
deniedReason: denial
|
|
1124
|
+
};
|
|
1125
|
+
iterations.push(failedRecord);
|
|
1126
|
+
await pipeline.run("AFTER_AGENT_ITERATION", {
|
|
1127
|
+
iterationIndex,
|
|
1128
|
+
intent,
|
|
1129
|
+
record: failedRecord,
|
|
1130
|
+
stepName: `agent-iteration-${iterationIndex}-after`,
|
|
1131
|
+
stepIndex: iterationIndex,
|
|
1132
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1133
|
+
previousStepName: `agent-iteration-${iterationIndex}-before`
|
|
1134
|
+
});
|
|
1135
|
+
return buildFailure({
|
|
1136
|
+
kind: "agent-iteration-denied",
|
|
1137
|
+
reason: denial,
|
|
1138
|
+
iterations,
|
|
1139
|
+
usage: cumulativeUsage
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
const task = handle.buildTask(conversation);
|
|
1143
|
+
const iterStart = Date.now();
|
|
1144
|
+
let response;
|
|
1145
|
+
try {
|
|
1146
|
+
if (provider.execute === void 0) return buildFailure({
|
|
1147
|
+
kind: "execution_unavailable",
|
|
1148
|
+
reason: "Selected provider has no execute() method.",
|
|
1149
|
+
iterations,
|
|
1150
|
+
usage: cumulativeUsage
|
|
1151
|
+
});
|
|
1152
|
+
const providerRequest = {
|
|
1153
|
+
task,
|
|
1154
|
+
artifacts: [],
|
|
1155
|
+
outputs: ["answer"],
|
|
1156
|
+
...intent.policy !== void 0 ? { policy: intent.policy } : {}
|
|
1157
|
+
};
|
|
1158
|
+
response = host.transport !== void 0 ? await host.transport.call(provider, providerRequest) : await provider.execute(providerRequest);
|
|
1159
|
+
} catch (error) {
|
|
1160
|
+
return buildFailure({
|
|
1161
|
+
kind: "provider_execution",
|
|
1162
|
+
reason: error instanceof Error ? error.message : "Provider execution failed",
|
|
1163
|
+
cause: error,
|
|
1164
|
+
iterations,
|
|
1165
|
+
usage: cumulativeUsage
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
const iterDuration = Date.now() - iterStart;
|
|
1169
|
+
const iterUsage = response.normalizedUsage ?? ZERO_USAGE;
|
|
1170
|
+
accumulateUsage(cumulativeUsage, iterUsage);
|
|
1171
|
+
const responseText = extractResponseText(response);
|
|
1172
|
+
const toolUseRequests = handle.parseToolUse(responseText);
|
|
1173
|
+
if (toolUseRequests === null) {
|
|
1174
|
+
const finalRecord = {
|
|
1175
|
+
index: iterationIndex,
|
|
1176
|
+
provider: providerName,
|
|
1177
|
+
promptTokens: iterUsage.promptTokens,
|
|
1178
|
+
completionTokens: iterUsage.completionTokens,
|
|
1179
|
+
costUsd: iterUsage.costUsd,
|
|
1180
|
+
durationMs: iterDuration,
|
|
1181
|
+
toolCalls: []
|
|
1182
|
+
};
|
|
1183
|
+
iterations.push(finalRecord);
|
|
1184
|
+
conversation.push({
|
|
1185
|
+
role: "assistant",
|
|
1186
|
+
content: responseText
|
|
1187
|
+
});
|
|
1188
|
+
await pipeline.run("AFTER_AGENT_ITERATION", {
|
|
1189
|
+
iterationIndex,
|
|
1190
|
+
intent,
|
|
1191
|
+
record: finalRecord,
|
|
1192
|
+
stepName: `agent-iteration-${iterationIndex}-after`,
|
|
1193
|
+
stepIndex: iterationIndex,
|
|
1194
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1195
|
+
previousStepName: `agent-iteration-${iterationIndex}-before`
|
|
1196
|
+
});
|
|
1197
|
+
await host.storage?.clear();
|
|
1198
|
+
return {
|
|
1199
|
+
kind: "success",
|
|
1200
|
+
output: { answer: responseText },
|
|
1201
|
+
usage: snapshotUsage(cumulativeUsage),
|
|
1202
|
+
iterations: Object.freeze([...iterations])
|
|
1203
|
+
};
|
|
1204
|
+
}
|
|
1205
|
+
conversation.push({
|
|
1206
|
+
role: "assistant",
|
|
1207
|
+
content: responseText
|
|
1208
|
+
});
|
|
1209
|
+
const toolCallRecords = [];
|
|
1210
|
+
for (const req of toolUseRequests) {
|
|
1211
|
+
const tool = intent.tools.find((t) => t.name === req.name);
|
|
1212
|
+
let toolResult = null;
|
|
1213
|
+
let resultContent;
|
|
1214
|
+
let resultHash = "tool-not-found";
|
|
1215
|
+
if (tool === void 0) resultContent = JSON.stringify({ error: `Unknown tool: ${req.name}` });
|
|
1216
|
+
else try {
|
|
1217
|
+
await pipeline.run("BEFORE_TOOL", {
|
|
1218
|
+
iterationIndex,
|
|
1219
|
+
toolName: req.name,
|
|
1220
|
+
args: req.args
|
|
1221
|
+
});
|
|
1222
|
+
toolResult = await runTool(tool, req.args);
|
|
1223
|
+
resultContent = stringifyArtifactValue(toolResult.artifact.value);
|
|
1224
|
+
resultHash = toolResult.callId;
|
|
1225
|
+
await pipeline.run("AFTER_TOOL", {
|
|
1226
|
+
iterationIndex,
|
|
1227
|
+
toolName: req.name,
|
|
1228
|
+
args: req.args,
|
|
1229
|
+
result: toolResult.artifact.value
|
|
1230
|
+
});
|
|
1231
|
+
} catch (error) {
|
|
1232
|
+
resultContent = JSON.stringify({ error: error instanceof Error ? error.message : "Tool execution failed" });
|
|
1233
|
+
}
|
|
1234
|
+
conversation.push({
|
|
1235
|
+
role: "tool",
|
|
1236
|
+
content: resultContent,
|
|
1237
|
+
toolCallId: req.id,
|
|
1238
|
+
toolName: req.name
|
|
1239
|
+
});
|
|
1240
|
+
toolCallRecords.push({
|
|
1241
|
+
id: req.id,
|
|
1242
|
+
name: req.name,
|
|
1243
|
+
argsHash: stableHash(req.args),
|
|
1244
|
+
resultHash
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
1247
|
+
const record = {
|
|
1248
|
+
index: iterationIndex,
|
|
1249
|
+
provider: providerName,
|
|
1250
|
+
promptTokens: iterUsage.promptTokens,
|
|
1251
|
+
completionTokens: iterUsage.completionTokens,
|
|
1252
|
+
costUsd: iterUsage.costUsd,
|
|
1253
|
+
durationMs: iterDuration,
|
|
1254
|
+
toolCalls: Object.freeze([...toolCallRecords])
|
|
1255
|
+
};
|
|
1256
|
+
iterations.push(record);
|
|
1257
|
+
await pipeline.run("AFTER_AGENT_ITERATION", {
|
|
1258
|
+
iterationIndex,
|
|
1259
|
+
intent,
|
|
1260
|
+
record,
|
|
1261
|
+
stepName: `agent-iteration-${iterationIndex}-after`,
|
|
1262
|
+
stepIndex: iterationIndex,
|
|
1263
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1264
|
+
previousStepName: `agent-iteration-${iterationIndex}-before`
|
|
1265
|
+
});
|
|
1266
|
+
if (host.storage !== void 0) {
|
|
1267
|
+
const snapshot = survivabilityAdapter.serialize({
|
|
1268
|
+
version: "agent-snapshot/v1",
|
|
1269
|
+
iterationIndex: iterationIndex + 1,
|
|
1270
|
+
conversation: [...conversation],
|
|
1271
|
+
cumulativeUsage: snapshotUsage(cumulativeUsage),
|
|
1272
|
+
providerName,
|
|
1273
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1274
|
+
});
|
|
1275
|
+
await host.storage.save(snapshot);
|
|
1276
|
+
}
|
|
1277
|
+
if (host.scheduler !== void 0) await host.scheduler.scheduleNext(iterationIndex);
|
|
1278
|
+
iterationIndex += 1;
|
|
1279
|
+
}
|
|
1280
|
+
return buildFailure({
|
|
1281
|
+
kind: "agent-max-iterations",
|
|
1282
|
+
reason: `Iteration budget ${maxIterations} reached without a final answer`,
|
|
1283
|
+
iterations,
|
|
1284
|
+
usage: cumulativeUsage
|
|
1285
|
+
});
|
|
1286
|
+
}
|
|
1287
|
+
function ensurePipeline(intent) {
|
|
1288
|
+
if (intent.pipeline !== void 0) return intent.pipeline;
|
|
1289
|
+
return createHookPipeline(intent.tracer !== void 0 ? { tracer: intent.tracer } : {});
|
|
1290
|
+
}
|
|
1291
|
+
function maybeAutoRegisterCheckpoint(pipeline, intent) {
|
|
1292
|
+
if (intent.signer === void 0) return;
|
|
1293
|
+
if (intent.autoRegisterCheckpoint === false) return;
|
|
1294
|
+
if (pipeline.isFrozen()) return;
|
|
1295
|
+
const handler = createCheckpointHook({
|
|
1296
|
+
runId: `runAgent-${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
|
1297
|
+
signer: intent.signer,
|
|
1298
|
+
...intent.tracer !== void 0 ? { tracer: intent.tracer } : {}
|
|
1299
|
+
});
|
|
1300
|
+
pipeline.register("AFTER_AGENT_ITERATION", handler, { band: BAND.OBSERVABILITY });
|
|
1301
|
+
}
|
|
1302
|
+
function pickFirstExecutableProvider(config) {
|
|
1303
|
+
const providers = config.providers ?? [];
|
|
1304
|
+
for (const entry of providers) {
|
|
1305
|
+
if (typeof entry === "string") continue;
|
|
1306
|
+
if ("kind" in entry && entry.kind === "provider-adapter" && entry.execute !== void 0) return entry;
|
|
1307
|
+
}
|
|
1308
|
+
return null;
|
|
1309
|
+
}
|
|
1310
|
+
function extractResponseText(response) {
|
|
1311
|
+
const raw = response.rawOutputs ?? {};
|
|
1312
|
+
const text = raw["answer"];
|
|
1313
|
+
if (typeof text === "string") return text;
|
|
1314
|
+
for (const value of Object.values(raw)) if (typeof value === "string") return value;
|
|
1315
|
+
return "";
|
|
1316
|
+
}
|
|
1317
|
+
function accumulateUsage(cumulative, iter) {
|
|
1318
|
+
cumulative.promptTokens += iter.promptTokens;
|
|
1319
|
+
cumulative.completionTokens += iter.completionTokens;
|
|
1320
|
+
if (iter.costUsd !== null) cumulative.costUsd = (cumulative.costUsd ?? 0) + iter.costUsd;
|
|
1321
|
+
}
|
|
1322
|
+
function snapshotUsage(c) {
|
|
1323
|
+
return {
|
|
1324
|
+
promptTokens: c.promptTokens,
|
|
1325
|
+
completionTokens: c.completionTokens,
|
|
1326
|
+
costUsd: c.costUsd
|
|
1327
|
+
};
|
|
1328
|
+
}
|
|
1329
|
+
function buildFailure(input) {
|
|
1330
|
+
return {
|
|
1331
|
+
kind: input.kind,
|
|
1332
|
+
usage: snapshotUsage(input.usage),
|
|
1333
|
+
iterations: Object.freeze([...input.iterations]),
|
|
1334
|
+
...input.reason !== void 0 ? { reason: input.reason } : {},
|
|
1335
|
+
...input.cause !== void 0 ? { cause: input.cause } : {}
|
|
1336
|
+
};
|
|
1337
|
+
}
|
|
1338
|
+
function stringifyArtifactValue(value) {
|
|
1339
|
+
if (typeof value === "string") return value;
|
|
1340
|
+
try {
|
|
1341
|
+
return JSON.stringify(value);
|
|
1342
|
+
} catch {
|
|
1343
|
+
return String(value);
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
function stableHash(input) {
|
|
1347
|
+
try {
|
|
1348
|
+
const json = JSON.stringify(input);
|
|
1349
|
+
let hash = 5381;
|
|
1350
|
+
for (let i = 0; i < json.length; i += 1) hash = hash * 33 ^ json.charCodeAt(i);
|
|
1351
|
+
return `djb2:${(hash >>> 0).toString(16)}`;
|
|
1352
|
+
} catch {
|
|
1353
|
+
return "djb2:0";
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
//#endregion
|
|
1357
|
+
export { createHookPipeline as C, BAND as S, toArtifactRef as T, PAYLOAD_TYPE as _, formatToolsForProvider as a, decodeEnvelope as b, importMcpTools as c, validateOutputMap as d, createNoopSurvivabilityAdapter as f, createReceipt as g, createCheckpointHook as h, createNoopAgentHost as i, runTool as l, STEP_TRANSITION_EVENT_NAME as m, runtime_exports as n, toolSchemaToJsonSchema as o, DEFAULT_CHECKPOINT_BAND as p, AgentDeniedError as r, defineTool as s, runAgent as t, toolArtifactRef as u, base64Encode as v, artifact as w, canonicalizeReceiptBody as x, buildPae as y };
|
|
1358
|
+
|
|
1359
|
+
//# sourceMappingURL=runtime-CfZ-sGGk.js.map
|