@runtypelabs/cli 2.59.1 → 2.59.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/dist/chunk-0-T9NWjH.js +3899 -0
  2. package/dist/chunk-0x7UKC1U.js +17 -0
  3. package/dist/chunk-2ySxXtcg.js +418 -0
  4. package/dist/chunk-3An_BRHv.js +501 -0
  5. package/dist/chunk-6uTxgdiY.js +16218 -0
  6. package/dist/chunk-87nRnNRG.js +1363 -0
  7. package/dist/chunk-B3wo8uRx.js +322 -0
  8. package/dist/chunk-BGp3ZdBy.js +560 -0
  9. package/dist/chunk-BJQv9Elp.js +107 -0
  10. package/dist/chunk-BOpO6xE-.js +6053 -0
  11. package/dist/chunk-BQXXCL8B.js +102 -0
  12. package/dist/chunk-BV-8isOd.js +34 -0
  13. package/dist/chunk-BVMhjGVe.js +826 -0
  14. package/dist/chunk-BW2fg46T.js +141 -0
  15. package/dist/chunk-BWJdY5Ag.js +36 -0
  16. package/dist/chunk-BYJOrLhh.js +302 -0
  17. package/dist/chunk-BYab8EVL.js +127 -0
  18. package/dist/chunk-BfEKvp7h.js +82 -0
  19. package/dist/chunk-BhgLSfH6.js +1123 -0
  20. package/dist/chunk-BirUxTtH.js +296 -0
  21. package/dist/chunk-BmB6UVZ9.js +1559 -0
  22. package/dist/chunk-BnWhIqW1.js +109 -0
  23. package/dist/chunk-Bpfnfk0t.js +123 -0
  24. package/dist/chunk-BrhgdnM3.js +108 -0
  25. package/dist/chunk-Bt0XAgX5.js +327 -0
  26. package/dist/chunk-Bu0FJr6S.js +84 -0
  27. package/dist/chunk-BvEoKpN3.js +255 -0
  28. package/dist/chunk-BxNJgYbE.js +28 -0
  29. package/dist/chunk-C1OFhd7F.js +6131 -0
  30. package/dist/chunk-C7e7WwbU.js +331 -0
  31. package/dist/chunk-CDTYP6bN.js +558 -0
  32. package/dist/chunk-CGDQoqDC.js +24 -0
  33. package/dist/chunk-CJraEBVd.js +127 -0
  34. package/dist/chunk-CQrNhOtv.js +40 -0
  35. package/dist/chunk-CW6AzBH6.js +128 -0
  36. package/dist/chunk-CXgL4w-9.js +317 -0
  37. package/dist/chunk-CZrqoPyf.js +233 -0
  38. package/dist/chunk-CcFCyqbB.js +83 -0
  39. package/dist/chunk-CdaIvO-y.js +485 -0
  40. package/dist/chunk-CehSXtgK.js +624 -0
  41. package/dist/chunk-Ciqkop65.js +218 -0
  42. package/dist/chunk-CoAWdSiE.js +43 -0
  43. package/dist/chunk-Crb4y7t4.js +830 -0
  44. package/dist/chunk-CrlqIfmb.js +337 -0
  45. package/dist/chunk-Cs4pwCQ-.js +324 -0
  46. package/dist/chunk-CwM_9y01.js +519 -0
  47. package/dist/chunk-CyRbIJg8.js +440 -0
  48. package/dist/chunk-D0OHsyMy.js +212 -0
  49. package/dist/chunk-D3y2bURs.js +1347 -0
  50. package/dist/chunk-D4_mJk03.js +2338 -0
  51. package/dist/chunk-D6C8rned.js +578 -0
  52. package/dist/chunk-D9ZbUnks.js +11 -0
  53. package/dist/chunk-DEPmLshX.js +834 -0
  54. package/dist/chunk-DFj1UPB2.js +330 -0
  55. package/dist/chunk-DGdFdZVf.js +197 -0
  56. package/dist/chunk-DGv7X9S_.js +63 -0
  57. package/dist/chunk-DJQ47WYZ.js +664 -0
  58. package/dist/chunk-DJw3O61j.js +1974 -0
  59. package/dist/chunk-DK4FbWVK.js +100 -0
  60. package/dist/chunk-DSRYhUus.js +993 -0
  61. package/dist/chunk-DajZ9Eg4.js +299 -0
  62. package/dist/chunk-Df4dunvU.js +195 -0
  63. package/dist/chunk-DjNTPhfO.js +481 -0
  64. package/dist/chunk-Dn8eaJuu.js +1042 -0
  65. package/dist/chunk-DwDWqSxM.js +637 -0
  66. package/dist/chunk-Dz7cEtxl.js +84 -0
  67. package/dist/chunk-E_0bgjrS.js +359 -0
  68. package/dist/chunk-KPYN2vUp.js +776 -0
  69. package/dist/chunk-KcqPQJBi.js +86 -0
  70. package/dist/chunk-N9H0JnoG.js +349 -0
  71. package/dist/chunk-NONFcXkn.js +728 -0
  72. package/dist/chunk-PrFjcpmH.js +256 -0
  73. package/dist/chunk-QSfNYiw9.js +11 -0
  74. package/dist/chunk-R265bzj9.js +226 -0
  75. package/dist/chunk-X9V_r6jM.js +166 -0
  76. package/dist/chunk-ZBtPUtyA.js +55 -0
  77. package/dist/chunk-ZCTfmOs7.js +4939 -0
  78. package/dist/chunk-h6oavj0t.js +97 -0
  79. package/dist/chunk-i7DwkvvD.js +42 -0
  80. package/dist/chunk-mG-4eZ6S.js +203 -0
  81. package/dist/chunk-o0ygUbCf.js +313 -0
  82. package/dist/chunk-ucu5lrDZ.js +5715 -0
  83. package/dist/index.js +11367 -73789
  84. package/package.json +4 -4
@@ -0,0 +1,830 @@
1
+ #!/usr/bin/env node
2
+ import { F as getCliVersion } from "./index.js";
3
+ import { n as RUNTYPE_ATTRIBUTES, r as GEN_AI_ATTRIBUTES, t as GEN_AI_CONTENT_ATTRIBUTES } from "./chunk-R265bzj9.js";
4
+ import { n as HARNESS_BINDING_HEADER, t as HARNESS_ATTRIBUTES } from "./chunk-Bu0FJr6S.js";
5
+ import { t as HARNESS_LIMITS } from "./chunk-0x7UKC1U.js";
6
+ import { n as redactObservation, t as redactHarnessValue } from "./chunk-KcqPQJBi.js";
7
+ import { join } from "node:path";
8
+ import { createHash, randomUUID } from "node:crypto";
9
+ import { mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
10
+ // src/lib/local-harness/delivery/otlp.ts
11
+ function harnessSpanId(traceId, identity) {
12
+ return createHash("sha256").update(`${traceId}:${identity}`).digest("hex").slice(0, 16);
13
+ }
14
+ function makeHarnessSpan(input, identity, operation, attributes, start, end, status, root = false) {
15
+ const full = {
16
+ [GEN_AI_ATTRIBUTES.operationName]: operation,
17
+ [RUNTYPE_ATTRIBUTES.executionId]: input.binding.executionId,
18
+ [HARNESS_ATTRIBUTES.version]: 1,
19
+ [HARNESS_ATTRIBUTES.sessionId]: input.sessionId,
20
+ [HARNESS_ATTRIBUTES.turnId]: input.turnId,
21
+ ...attributes
22
+ };
23
+ return {
24
+ traceId: input.binding.traceId,
25
+ spanId: harnessSpanId(input.binding.traceId, identity),
26
+ ...root ? {} : { parentSpanId: harnessSpanId(input.binding.traceId, "root") },
27
+ name: operation,
28
+ kind: 1,
29
+ startTimeUnixNano: String(BigInt(Math.trunc(start)) * 1000000n),
30
+ endTimeUnixNano: String(BigInt(Math.trunc(end)) * 1000000n),
31
+ attributes: Object.entries(full).filter(([, value]) => value !== void 0).map(([key, value]) => ({
32
+ key,
33
+ value: typeof value === "boolean" ? { boolValue: value } : typeof value === "number" && Number.isFinite(value) ? { doubleValue: value } : { stringValue: typeof value === "string" ? value : JSON.stringify(value) }
34
+ })),
35
+ status: { code: status }
36
+ };
37
+ }
38
+ function observationSpan(input, observation, sequence, observedAt) {
39
+ return makeHarnessSpan(input, `event:${sequence}`, "harness.observation", {
40
+ [HARNESS_ATTRIBUTES.sequence]: sequence,
41
+ [HARNESS_ATTRIBUTES.eventId]: `${input.turnId}:${sequence}`,
42
+ [HARNESS_ATTRIBUTES.nativeId]: observation.nativeId,
43
+ [HARNESS_ATTRIBUTES.nativeSessionId]: observation.nativeSessionId,
44
+ [HARNESS_ATTRIBUTES.nativeTurnId]: observation.nativeTurnId,
45
+ [HARNESS_ATTRIBUTES.parentId]: observation.parentId,
46
+ [HARNESS_ATTRIBUTES.usage]: observation.usage,
47
+ [HARNESS_ATTRIBUTES.toolUpdate]: observation.tool ? {
48
+ id: observation.tool.id,
49
+ name: observation.tool.name,
50
+ nativeName: observation.tool.nativeName,
51
+ mcpServer: observation.tool.mcpServer,
52
+ mcpTool: observation.tool.mcpTool,
53
+ status: observation.tool.status,
54
+ durationMs: observation.tool.durationMs
55
+ } : void 0,
56
+ ...input.dataPolicy === "metrics" ? {} : { [HARNESS_ATTRIBUTES.nativeContent]: observation }
57
+ }, observedAt, observedAt, 0);
58
+ }
59
+ function toolSpan(input, tool, observedStart, observedEnd) {
60
+ const durationKnown = typeof tool.durationMs === "number" && Number.isFinite(tool.durationMs) && tool.durationMs >= 0;
61
+ const ended = Number.isFinite(tool.endedAt) ? tool.endedAt : observedEnd;
62
+ const started = durationKnown ? ended - tool.durationMs : Number.isFinite(tool.startedAt) ? tool.startedAt : observedStart;
63
+ return makeHarnessSpan(input, `tool:${tool.id}`, "execute_tool", {
64
+ [GEN_AI_ATTRIBUTES.toolCallId]: tool.id,
65
+ [GEN_AI_ATTRIBUTES.toolName]: tool.name ?? tool.nativeName ?? "unknown",
66
+ [GEN_AI_ATTRIBUTES.toolType]: tool.mcpServer ? "mcp" : "function",
67
+ [HARNESS_ATTRIBUTES.nativeId]: tool.id,
68
+ [HARNESS_ATTRIBUTES.toolOutcome]: tool.status === "succeeded" ? "success" : tool.status === "failed" ? "failed" : "unknown",
69
+ [HARNESS_ATTRIBUTES.durationKnown]: durationKnown,
70
+ ...input.dataPolicy === "metrics" ? {} : {
71
+ [GEN_AI_CONTENT_ATTRIBUTES.toolCallArguments]: tool.input,
72
+ [GEN_AI_CONTENT_ATTRIBUTES.toolCallResult]: tool.output ?? tool.error
73
+ }
74
+ }, started, Math.max(started, ended), tool.status === "failed" ? 2 : tool.status === "succeeded" ? 1 : 0);
75
+ }
76
+ function harnessExport(spans, agentId) {
77
+ return { resourceSpans: [{
78
+ resource: { attributes: [
79
+ {
80
+ key: RUNTYPE_ATTRIBUTES.agentId,
81
+ value: { stringValue: agentId }
82
+ },
83
+ {
84
+ key: RUNTYPE_ATTRIBUTES.adapterName,
85
+ value: { stringValue: "runtype-local-harness" }
86
+ },
87
+ {
88
+ key: RUNTYPE_ATTRIBUTES.adapterVersion,
89
+ value: { stringValue: getCliVersion() }
90
+ }
91
+ ] },
92
+ scopeSpans: [{
93
+ scope: {
94
+ name: "runtype.local-harness",
95
+ version: "1"
96
+ },
97
+ spans
98
+ }]
99
+ }] };
100
+ }
101
+ // src/lib/local-harness/delivery/spool.ts
102
+ /** Private bounded retry storage contains already-redacted spans, never authentication headers. */
103
+ var HarnessSpool = class {
104
+ directory;
105
+ maxBytes;
106
+ queuedBytes = 0;
107
+ initialization;
108
+ constructor(directory, maxBytes = HARNESS_LIMITS.maxSpoolBytes) {
109
+ this.directory = directory;
110
+ this.maxBytes = maxBytes;
111
+ }
112
+ get bytes() {
113
+ return this.queuedBytes;
114
+ }
115
+ initialize() {
116
+ this.initialization ??= (async () => {
117
+ await mkdir(this.directory, {
118
+ recursive: true,
119
+ mode: 448
120
+ });
121
+ for (const name of await readdir(this.directory)) {
122
+ if (!/^[0-9a-f-]+\.json$/.test(name)) continue;
123
+ const path = join(this.directory, name);
124
+ const info = await stat(path);
125
+ if (Date.now() - info.mtimeMs > HARNESS_LIMITS.spoolRetentionMs) await rm(path);
126
+ else this.queuedBytes += info.size;
127
+ }
128
+ })();
129
+ return this.initialization;
130
+ }
131
+ async put(body) {
132
+ await this.initialize();
133
+ const serialized = JSON.stringify(body);
134
+ const bytes = Buffer.byteLength(serialized);
135
+ if (bytes > HARNESS_LIMITS.maxBatchBytes || this.queuedBytes + bytes > this.maxBytes) throw new Error("Harness telemetry spool capacity exceeded");
136
+ this.queuedBytes += bytes;
137
+ const path = join(this.directory, `${randomUUID()}.json`);
138
+ try {
139
+ await writeFile(path, serialized, {
140
+ mode: 384,
141
+ flag: "wx"
142
+ });
143
+ } catch (error) {
144
+ this.queuedBytes -= bytes;
145
+ throw error;
146
+ }
147
+ return {
148
+ path,
149
+ bytes
150
+ };
151
+ }
152
+ async acknowledge(entry) {
153
+ await rm(entry.path, { force: true });
154
+ this.queuedBytes -= entry.bytes;
155
+ }
156
+ async read(entry) {
157
+ return JSON.parse(await readFile(entry.path, "utf8"));
158
+ }
159
+ };
160
+ function ingestFullyAccepted(value) {
161
+ if (typeof value !== "object" || value === null) return false;
162
+ const record = value;
163
+ const partial = record.partialSuccess ?? record.partial_success;
164
+ if (partial === void 0) return true;
165
+ if (typeof partial !== "object" || partial === null) return false;
166
+ const rejected = partial.rejectedSpans ?? partial.rejected_spans;
167
+ return rejected === void 0 || rejected === 0 || rejected === "0";
168
+ }
169
+ // ../model-execution/src/base64.ts
170
+ /**
171
+ * Base64 helpers shared by the asset offload/rehydrate walk and the tool-result media-handle walk.
172
+ * Workers/Node/browser safe: prefers `Buffer` when present, falls back to `atob` / `btoa`.
173
+ * Internal to this package on purpose, not re-exported from the barrel.
174
+ */
175
+ function base64ToBytes(base64) {
176
+ const bufferCtor = globalThis.Buffer;
177
+ if (bufferCtor) return new Uint8Array(bufferCtor.from(base64, "base64"));
178
+ const binary = atob(base64);
179
+ const bytes = new Uint8Array(binary.length);
180
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
181
+ return bytes;
182
+ }
183
+ /**
184
+ * True when a string is plausibly raw base64 (alphabet + padding only).
185
+ * `Buffer.from(str, 'base64')` silently drops non-alphabet characters rather than throwing.
186
+ * This predicate is the guard that keeps non-base64 text out of byte-level paths.
187
+ */
188
+ function looksLikeBase64(value) {
189
+ return value.length >= 16 && /^[A-Za-z0-9+/]+={0,2}$/.test(value);
190
+ }
191
+ /** Lowercase hex of a digest, signature or random buffer. */
192
+ function bufferToHex(buffer) {
193
+ let hex = "";
194
+ for (const byte of new Uint8Array(buffer)) hex += byte.toString(16).padStart(2, "0");
195
+ return hex;
196
+ }
197
+ // ../model-execution/src/media-part-descriptor.ts
198
+ function payloadOf(part) {
199
+ return part.image ?? part.data;
200
+ }
201
+ function declaredMimeType(part) {
202
+ if (typeof part.mimeType === "string" && part.mimeType.length > 0) return part.mimeType;
203
+ if (typeof part.mediaType === "string" && part.mediaType.length > 0) return part.mediaType;
204
+ }
205
+ function inlineBytes(payload) {
206
+ if (payload instanceof Uint8Array) return { bytes: payload };
207
+ if (payload instanceof ArrayBuffer) return { bytes: new Uint8Array(payload) };
208
+ if (ArrayBuffer.isView(payload)) return { bytes: new Uint8Array(payload.buffer, payload.byteOffset, payload.byteLength) };
209
+ if (typeof payload !== "string") return void 0;
210
+ if (payload.startsWith("data:")) {
211
+ const match = /^data:([^;,]*)(?:;[^,]*)?;base64,(.*)$/s.exec(payload);
212
+ if (!match) return void 0;
213
+ const bytes = base64ToBytes(match[2] ?? "");
214
+ return match[1] ? {
215
+ bytes,
216
+ mimeType: match[1]
217
+ } : { bytes };
218
+ }
219
+ return looksLikeBase64(payload) ? { bytes: base64ToBytes(payload) } : void 0;
220
+ }
221
+ /** Web Crypto takes no view over a `SharedArrayBuffer`; such bytes are copied first. */
222
+ function ownedBytes(bytes) {
223
+ return isArrayBufferBacked(bytes) ? bytes : new Uint8Array(bytes);
224
+ }
225
+ function isArrayBufferBacked(bytes) {
226
+ return bytes.buffer instanceof ArrayBuffer;
227
+ }
228
+ /** Message and tool-result part types whose `image` or `data` field can carry inline bytes. */
229
+ const MEDIA_PART_TYPES = /* @__PURE__ */ new Set([
230
+ "image",
231
+ "file",
232
+ "media",
233
+ "audio",
234
+ "video",
235
+ "image-data",
236
+ "file-data"
237
+ ]);
238
+ /** Past this nesting depth a value is left as-is; real message and tool-result shapes sit far above it. */
239
+ const MAX_DESCRIBE_DEPTH = 8;
240
+ function isMediaPart(part) {
241
+ if (part === null || typeof part !== "object") return false;
242
+ const type = part.type;
243
+ return typeof type === "string" && MEDIA_PART_TYPES.has(type);
244
+ }
245
+ function isPlainObject(value) {
246
+ if (value === null || typeof value !== "object") return false;
247
+ const proto = Object.getPrototypeOf(value);
248
+ return proto === Object.prototype || proto === null;
249
+ }
250
+ /**
251
+ * The descriptor for a media part whose payload is inline bytes (a
252
+ * `Uint8Array`, an `ArrayBuffer`, a base64 data URL or raw base64), or
253
+ * `undefined` for a URL reference or any other value.
254
+ */
255
+ async function describeMediaPart(part) {
256
+ if (!isMediaPart(part)) return void 0;
257
+ let media;
258
+ try {
259
+ media = inlineBytes(payloadOf(part));
260
+ } catch {
261
+ return;
262
+ }
263
+ if (!media) return void 0;
264
+ const digest = await crypto.subtle.digest("SHA-256", ownedBytes(media.bytes));
265
+ const mimeType = declaredMimeType(part) ?? media.mimeType;
266
+ return {
267
+ type: part.type,
268
+ ...mimeType ? { mimeType } : {},
269
+ ...typeof part.filename === "string" && part.filename.length > 0 ? { filename: part.filename } : {},
270
+ bytes: media.bytes.byteLength,
271
+ sha256: bufferToHex(digest)
272
+ };
273
+ }
274
+ /**
275
+ * `value` with every inline media part, at any depth of arrays and plain
276
+ * objects (messages, tool results, MCP `content` envelopes), replaced by its
277
+ * descriptor. A value holding no inline media comes back as the same reference.
278
+ */
279
+ async function describeInlineMedia(value, depth = 0) {
280
+ if (depth > MAX_DESCRIBE_DEPTH) return value;
281
+ if (isMediaPart(value)) return await describeMediaPart(value) ?? value;
282
+ if (Array.isArray(value)) {
283
+ const items = await Promise.all(value.map((item) => describeInlineMedia(item, depth + 1)));
284
+ return items.some((item, index) => item !== value[index]) ? items : value;
285
+ }
286
+ if (!isPlainObject(value)) return value;
287
+ const entries = Object.entries(value);
288
+ const described = await Promise.all(entries.map(([, item]) => describeInlineMedia(item, depth + 1)));
289
+ if (described.every((item, index) => item === entries[index][1])) return value;
290
+ return Object.fromEntries(entries.map(([key], index) => [key, described[index]]));
291
+ }
292
+ // src/lib/local-harness/delivery/media.ts
293
+ const MEDIA_TYPES = /* @__PURE__ */ new Set([
294
+ "image",
295
+ "file",
296
+ "media",
297
+ "audio",
298
+ "video",
299
+ "image-data",
300
+ "file-data"
301
+ ]);
302
+ const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
303
+ /** Describe native and JSON-encoded MCP media before telemetry serialization. */
304
+ async function normalizeHarnessMedia(value, timeoutMs = 1e3) {
305
+ let truncated = false;
306
+ let visited = 0;
307
+ let expired = false;
308
+ const describe = async (part) => {
309
+ const payloadKey = part.image !== void 0 ? "image" : "data";
310
+ const payload = part[payloadKey];
311
+ const inline = typeof payload === "string" ? !/^https?:\/\//.test(payload) : payload instanceof ArrayBuffer || ArrayBuffer.isView(payload);
312
+ if (inline && typeof payload === "string") {
313
+ const encoded = payload.startsWith("data:") ? /^data:[^,]*;base64,(.*)$/s.exec(payload)?.[1] : payload;
314
+ if (encoded === void 0 || !/^[A-Za-z0-9+/]*={0,2}$/.test(encoded) || encoded.length % 4 === 1) throw new Error("Inline media could not be described");
315
+ }
316
+ const candidate = typeof payload === "string" && !payload.startsWith("data:") && inline ? {
317
+ ...part,
318
+ [payloadKey]: `data:${String(part.mimeType ?? part.mediaType ?? "application/octet-stream")};base64,${payload}`
319
+ } : part;
320
+ const described = await describeInlineMedia(candidate);
321
+ if (inline && described === candidate) throw new Error("Inline media could not be described");
322
+ return described;
323
+ };
324
+ const walk = async (item, depth) => {
325
+ if (expired) throw new Error("Media normalization deadline exceeded");
326
+ if (depth > 24 || ++visited > 2e4) {
327
+ truncated = true;
328
+ return "[capture nesting limit]";
329
+ }
330
+ if (typeof item === "string") {
331
+ if (/^\s*[[{]/.test(item)) {
332
+ let parsed;
333
+ try {
334
+ parsed = JSON.parse(item);
335
+ } catch {
336
+ return item;
337
+ }
338
+ return JSON.stringify(await walk(parsed, depth + 1));
339
+ }
340
+ if (item.startsWith("data:") && item.includes(";base64,")) return describe({
341
+ type: "media",
342
+ data: item
343
+ });
344
+ return item;
345
+ }
346
+ if (Array.isArray(item)) {
347
+ const result = [];
348
+ for (const child of item) result.push(await walk(child, depth + 1));
349
+ return result;
350
+ }
351
+ if (!object(item)) return item;
352
+ const source = object(item.source) ? item.source : void 0;
353
+ const imageUrl = object(item.image_url) ? item.image_url.url : item.image_url;
354
+ const audio = object(item.input_audio) ? item.input_audio : void 0;
355
+ const candidate = source?.type === "base64" && typeof source.data === "string" ? {
356
+ type: item.type === "document" ? "file" : MEDIA_TYPES.has(String(item.type)) ? item.type : "media",
357
+ data: source.data,
358
+ mimeType: source.media_type,
359
+ filename: item.filename
360
+ } : item.type === "base64" && typeof item.data === "string" ? {
361
+ type: "media",
362
+ data: item.data,
363
+ mimeType: item.media_type
364
+ } : ["input_image", "image_url"].includes(String(item.type)) && typeof imageUrl === "string" ? {
365
+ type: "image",
366
+ image: imageUrl
367
+ } : item.type === "input_audio" && audio ? {
368
+ type: "audio",
369
+ data: audio.data,
370
+ mimeType: typeof audio.format === "string" ? `audio/${audio.format}` : void 0
371
+ } : item.type === "input_file" && item.file_data !== void 0 ? {
372
+ type: "file",
373
+ data: item.file_data,
374
+ filename: item.filename
375
+ } : item;
376
+ if (MEDIA_TYPES.has(String(candidate.type))) {
377
+ const described = await describe(candidate);
378
+ if (described !== candidate) return described;
379
+ }
380
+ const entries = [];
381
+ for (const [key, child] of Object.entries(item)) entries.push([key, await walk(child, depth + 1)]);
382
+ return Object.fromEntries(entries);
383
+ };
384
+ let timer;
385
+ try {
386
+ return await Promise.race([walk(value, 0).then((normalized) => ({
387
+ value: normalized,
388
+ truncated
389
+ })), new Promise((_, reject) => {
390
+ timer = setTimeout(() => {
391
+ expired = true;
392
+ reject(/* @__PURE__ */ new Error("Media normalization deadline exceeded"));
393
+ }, Math.max(1, timeoutMs));
394
+ })]);
395
+ } finally {
396
+ if (timer) clearTimeout(timer);
397
+ }
398
+ }
399
+ // src/lib/local-harness/delivery/index.ts
400
+ const STREAM_RELEASE_CHARS = 32768;
401
+ const STREAM_BUFFER_CAP_CHARS = 36864;
402
+ const STREAM_HOLDBACK_CHARS = 1024;
403
+ function continuesStream(current, next) {
404
+ return current.message?.role === next.message?.role && current.nativeSessionId === next.nativeSessionId && current.nativeTurnId === next.nativeTurnId && current.parentId === next.parentId;
405
+ }
406
+ /**
407
+ * Where a long joined stream may be released: before any known secret straddling the cut and at a
408
+ * word start, so a pattern-matched credential still arriving in later deltas is never split.
409
+ * Returns 0 when no safe boundary exists. A holdback of 0 releases up to the final unfinished word.
410
+ */
411
+ function streamCut(text, secrets, holdback = STREAM_HOLDBACK_CHARS) {
412
+ const longest = Math.max(0, ...secrets.map((secret) => secret.length));
413
+ let cut = Math.max(0, text.length - Math.max(longest, holdback));
414
+ for (let moved = true; moved && cut > 0;) {
415
+ moved = false;
416
+ for (const secret of secrets) {
417
+ if (secret.length < 2) continue;
418
+ const start = text.indexOf(secret, Math.max(0, cut - secret.length + 1));
419
+ if (start >= 0 && start < cut) {
420
+ cut = start;
421
+ moved = true;
422
+ }
423
+ }
424
+ }
425
+ const line = text.lastIndexOf("\n", cut - 1);
426
+ if (holdback > 0 && line >= 0 && cut - line < holdback) cut = line + 1;
427
+ while (cut > 0 && !/\s/.test(text[cut - 1])) cut -= 1;
428
+ const scheme = /\bbearer\s+$/i.exec(text.slice(Math.max(0, cut - 64), cut));
429
+ return scheme ? cut - (Math.min(cut, 64) - scheme.index) : cut;
430
+ }
431
+ function createHarnessCaptureFactory(options) {
432
+ const spool = new HarnessSpool(options.spoolDirectory);
433
+ const now = options.now ?? Date.now;
434
+ let pendingBatches = 0;
435
+ let deliveryFailures = 0;
436
+ const factory = (originalInput) => {
437
+ const ranks = {
438
+ metrics: 0,
439
+ redacted: 1,
440
+ full: 2
441
+ };
442
+ const dataPolicy = ranks[originalInput.dataPolicy] < ranks[originalInput.binding.dataPolicy] ? originalInput.dataPolicy : originalInput.binding.dataPolicy;
443
+ const input = {
444
+ ...originalInput,
445
+ dataPolicy,
446
+ provenance: {
447
+ ...originalInput.provenance,
448
+ cliVersion: getCliVersion()
449
+ }
450
+ };
451
+ const started = now();
452
+ const secrets = [...options.secrets ?? [], input.binding.token];
453
+ const tools = /* @__PURE__ */ new Map();
454
+ const coverage = {
455
+ ...input.provenance.coverage,
456
+ content: { state: dataPolicy === "metrics" ? "disabled" : "available" }
457
+ };
458
+ let sequence = 0;
459
+ let bytes = 0;
460
+ let dropped = 0;
461
+ let delivered = 0;
462
+ let failed = false;
463
+ let finalAccepted = false;
464
+ let flushDeadline = Infinity;
465
+ let finishDeadline = Infinity;
466
+ let drainTimer;
467
+ let expireDrain;
468
+ const drainExpired = new Promise((resolve) => {
469
+ expireDrain = resolve;
470
+ });
471
+ let finished;
472
+ let pending = [];
473
+ let pendingBytes = 0;
474
+ let chain = Promise.resolve();
475
+ let normalization = Promise.resolve();
476
+ let admittedBytes = 0;
477
+ let omittedFragments = 0;
478
+ const post = async (body, deadline, final) => {
479
+ let timer;
480
+ try {
481
+ return await Promise.race([
482
+ options.client.post("/otel/v1/traces", body, { [HARNESS_BINDING_HEADER]: input.binding.token }, { timeoutMs: Math.max(1, deadline - now()) }),
483
+ new Promise((_, reject) => {
484
+ timer = setTimeout(() => reject(/* @__PURE__ */ new Error("Harness evidence delivery deadline exceeded")), Math.max(0, deadline - now()));
485
+ }),
486
+ ...final ? [] : [drainExpired.then(() => {
487
+ throw new Error("Harness evidence drain deadline exceeded");
488
+ })]
489
+ ]);
490
+ } finally {
491
+ if (timer) clearTimeout(timer);
492
+ }
493
+ };
494
+ const enqueue = (spans, count, final = false) => {
495
+ pendingBatches += 1;
496
+ chain = chain.then(async () => {
497
+ try {
498
+ if (failed && !final) return;
499
+ const entry = await spool.put(harnessExport(spans, options.agentId));
500
+ const deadline = Math.min(final ? finishDeadline : flushDeadline, input.binding.expiresAt, now() + (options.deliveryTimeoutMs ?? HARNESS_LIMITS.deliveryTimeoutMs));
501
+ let accepted = false;
502
+ for (let attempt = 0; attempt < 3 && now() < deadline; attempt += 1) try {
503
+ if (!ingestFullyAccepted(await post(await spool.read(entry), deadline, final))) break;
504
+ accepted = true;
505
+ break;
506
+ } catch {
507
+ if (attempt < 2 && now() < deadline && (final || now() < flushDeadline)) await new Promise((resolve) => setTimeout(resolve, Math.min(100 * (attempt + 1), deadline - now())));
508
+ if (!final && now() >= flushDeadline) break;
509
+ }
510
+ if (!accepted) {
511
+ failed = true;
512
+ deliveryFailures += 1;
513
+ return;
514
+ }
515
+ await spool.acknowledge(entry);
516
+ delivered += count;
517
+ if (final) finalAccepted = true;
518
+ } catch {
519
+ failed = true;
520
+ deliveryFailures += 1;
521
+ } finally {
522
+ pendingBatches -= 1;
523
+ }
524
+ });
525
+ };
526
+ const flush = () => {
527
+ if (!pending.length) return;
528
+ enqueue(pending, pending.filter((span) => span.name === "harness.observation").length);
529
+ pending = [];
530
+ pendingBytes = 0;
531
+ };
532
+ const append = (span) => {
533
+ const size = Buffer.byteLength(JSON.stringify(span));
534
+ if (size > HARNESS_LIMITS.maxEventBytes) {
535
+ dropped += 1;
536
+ coverage.content = {
537
+ state: "truncated",
538
+ reason: "Event exceeded the capture byte limit"
539
+ };
540
+ return;
541
+ }
542
+ if (pendingBytes + size > 131072) flush();
543
+ pending.push(span);
544
+ pendingBytes += size;
545
+ if (pending.length >= 16) flush();
546
+ };
547
+ const admit = (raw) => {
548
+ if (finished) return;
549
+ const observedSequence = ++sequence;
550
+ const timestamp = now();
551
+ if (failed || sequence > 1e3) {
552
+ dropped += 1;
553
+ coverage.delivery = {
554
+ state: "partial",
555
+ reason: "Capture capacity or delivery failed"
556
+ };
557
+ return;
558
+ }
559
+ let snapshot;
560
+ try {
561
+ const admitted = dataPolicy === "metrics" ? redactObservation(raw, dataPolicy, secrets).observation : raw;
562
+ const size = Buffer.byteLength(JSON.stringify(admitted));
563
+ if (size > HARNESS_LIMITS.maxEventBytes || admittedBytes + size > HARNESS_LIMITS.maxTurnBytes) {
564
+ dropped += 1;
565
+ coverage.content = {
566
+ state: "truncated",
567
+ reason: "Turn capture byte limit reached"
568
+ };
569
+ return;
570
+ }
571
+ admittedBytes += size;
572
+ snapshot = structuredClone(admitted);
573
+ } catch {
574
+ dropped += 1;
575
+ coverage.content = {
576
+ state: "partial",
577
+ reason: "An observation could not be serialized"
578
+ };
579
+ return;
580
+ }
581
+ normalization = normalization.then(async () => {
582
+ try {
583
+ if (now() >= flushDeadline) throw new Error("Capture normalization deadline exceeded");
584
+ const normalized = dataPolicy === "metrics" ? {
585
+ value: snapshot,
586
+ truncated: false
587
+ } : await normalizeHarnessMedia(snapshot, Math.min(1e3, flushDeadline - now()));
588
+ const redacted = redactObservation(normalized.value, dataPolicy, secrets);
589
+ const observation = redacted.observation;
590
+ if (normalized.truncated) coverage.content = {
591
+ state: "truncated",
592
+ reason: "Content exceeded capture nesting limits"
593
+ };
594
+ else if (redacted.changed && dataPolicy !== "metrics" && coverage.content?.state === "available") coverage.content = { state: "redacted" };
595
+ const size = Buffer.byteLength(JSON.stringify(observation));
596
+ if (size > HARNESS_LIMITS.maxEventBytes || bytes + size > HARNESS_LIMITS.maxTurnBytes) {
597
+ dropped += 1;
598
+ coverage.content = {
599
+ state: "truncated",
600
+ reason: "Turn capture byte limit reached"
601
+ };
602
+ return;
603
+ }
604
+ bytes += size;
605
+ if (observation.tool) {
606
+ const tool = observation.tool;
607
+ const prior = tools.get(tool.id);
608
+ const values = Object.fromEntries(Object.entries(tool).filter(([, value]) => value !== void 0));
609
+ const merged = {
610
+ ...prior?.value,
611
+ ...values
612
+ };
613
+ if (prior?.value.status === "failed") merged.status = "failed";
614
+ else if (prior?.value.status === "succeeded" && [
615
+ "running",
616
+ "pending",
617
+ "unknown"
618
+ ].includes(merged.status)) merged.status = "succeeded";
619
+ tools.set(tool.id, {
620
+ value: merged,
621
+ first: prior?.first ?? timestamp,
622
+ last: timestamp
623
+ });
624
+ }
625
+ append(observationSpan(input, observation, observedSequence, timestamp));
626
+ if (observation.tool) flush();
627
+ } catch {
628
+ dropped += 1;
629
+ coverage.content = {
630
+ state: "partial",
631
+ reason: "An observation could not be normalized"
632
+ };
633
+ }
634
+ });
635
+ };
636
+ let stream;
637
+ const releaseStream = (text) => {
638
+ if (!stream || !text) return;
639
+ admit({
640
+ ...stream.observation,
641
+ message: {
642
+ ...stream.observation.message,
643
+ content: text,
644
+ delta: true
645
+ }
646
+ });
647
+ };
648
+ const flushStream = () => {
649
+ if (stream) releaseStream(stream.text);
650
+ stream = void 0;
651
+ };
652
+ const observe = (raw) => {
653
+ if (finished) return;
654
+ if (dataPolicy === "metrics") return admit(raw);
655
+ if (raw.fragment) {
656
+ omittedFragments += 1;
657
+ return;
658
+ }
659
+ const message = raw.message;
660
+ if (!message?.delta || typeof message.content !== "string") {
661
+ flushStream();
662
+ return admit(raw);
663
+ }
664
+ if (!stream || !continuesStream(stream.observation, raw)) {
665
+ flushStream();
666
+ stream = {
667
+ observation: raw,
668
+ text: "",
669
+ withholding: false
670
+ };
671
+ }
672
+ let text = message.content;
673
+ if (stream.withholding) {
674
+ text = text.replace(/^\S+/, "");
675
+ if (!text) return;
676
+ stream.withholding = false;
677
+ }
678
+ stream.text += text;
679
+ if (stream.text.length < STREAM_RELEASE_CHARS) return;
680
+ const capped = stream.text.length >= STREAM_BUFFER_CAP_CHARS;
681
+ const cut = streamCut(stream.text, secrets, capped ? 0 : STREAM_HOLDBACK_CHARS);
682
+ if (cut > 0) {
683
+ releaseStream(stream.text.slice(0, cut));
684
+ stream.text = stream.text.slice(cut);
685
+ return;
686
+ }
687
+ if (!capped) return;
688
+ dropped += 1;
689
+ coverage.content = {
690
+ state: "truncated",
691
+ reason: "Streamed text without a word break exceeded the capture window"
692
+ };
693
+ stream.text = "";
694
+ stream.withholding = true;
695
+ };
696
+ const finish = (result) => {
697
+ if (!finished) flushStream();
698
+ finished ??= (async () => {
699
+ const budget = options.deliveryTimeoutMs ?? HARNESS_LIMITS.deliveryTimeoutMs;
700
+ finishDeadline = now() + budget - Math.min(250, budget / 10);
701
+ flushDeadline = finishDeadline - Math.min(1e3, budget / 4);
702
+ drainTimer = setTimeout(expireDrain, Math.max(0, flushDeadline - now()));
703
+ await normalization;
704
+ if (result.coverage) {
705
+ for (const [field, value] of Object.entries(result.coverage)) if (!coverage[field] || coverage[field].state === "available") coverage[field] = value;
706
+ }
707
+ let unresolvedTools = 0;
708
+ const toolCalls = [];
709
+ for (const { value: tool, first, last } of tools.values()) {
710
+ const isError = tool.status === "failed" ? true : tool.status === "succeeded" ? false : void 0;
711
+ if (isError === void 0) unresolvedTools += 1;
712
+ append(toolSpan(input, tool, first, last));
713
+ toolCalls.push({
714
+ id: tool.id,
715
+ name: tool.name ?? tool.nativeName ?? "unknown",
716
+ ...tool.input === void 0 ? {} : { input: tool.input },
717
+ ...tool.output === void 0 ? {} : { output: tool.output },
718
+ ...isError === void 0 ? {} : { isError },
719
+ ...tool.durationMs === void 0 ? {} : { durationMs: tool.durationMs }
720
+ });
721
+ }
722
+ flush();
723
+ if (omittedFragments && (coverage.native?.state ?? "available") === "available") coverage.native = {
724
+ state: "partial",
725
+ reason: "Raw stream fragments are omitted; their text is kept as joined messages."
726
+ };
727
+ if (unresolvedTools) coverage.tools = {
728
+ state: "partial",
729
+ reason: "Some tool outcomes are unknown"
730
+ };
731
+ let rootContent;
732
+ let safeProvenance;
733
+ try {
734
+ const finalContent = {
735
+ provenance: {
736
+ ...input.provenance,
737
+ configuration: dataPolicy === "metrics" ? {} : input.provenance.configuration
738
+ },
739
+ ...dataPolicy === "metrics" ? {} : { content: {
740
+ finalText: result.text,
741
+ reportedUsage: result.reportedUsage
742
+ } }
743
+ };
744
+ if (Buffer.byteLength(JSON.stringify(finalContent)) > HARNESS_LIMITS.maxEventBytes) throw new Error("Final content exceeded the capture byte limit");
745
+ const normalized = await normalizeHarnessMedia(finalContent, Math.min(1e3, Math.max(1, flushDeadline - now())));
746
+ const safe = redactHarnessValue(normalized.value, dataPolicy, secrets).value;
747
+ safeProvenance = safe.provenance;
748
+ rootContent = safe.content;
749
+ if (normalized.truncated) coverage.content = {
750
+ state: "truncated",
751
+ reason: "Content exceeded capture nesting limits"
752
+ };
753
+ } catch {
754
+ coverage.content = {
755
+ state: "partial",
756
+ reason: "Final content could not be normalized"
757
+ };
758
+ coverage.delivery = {
759
+ state: "partial",
760
+ reason: "Final content was omitted"
761
+ };
762
+ dropped += 1;
763
+ }
764
+ await chain;
765
+ if (drainTimer) clearTimeout(drainTimer);
766
+ if (failed || dropped) coverage.delivery = {
767
+ state: "partial",
768
+ reason: "Some evidence was not durably accepted"
769
+ };
770
+ const summary = {
771
+ version: 1,
772
+ traceId: input.binding.traceId,
773
+ executionId: input.binding.executionId,
774
+ delivery: failed ? "failed" : dropped ? "partial" : "accepted",
775
+ dataPolicy,
776
+ coverage,
777
+ observedEvents: sequence,
778
+ deliveredEvents: Math.min(sequence, delivered),
779
+ droppedEvents: dropped,
780
+ unresolvedTools
781
+ };
782
+ const root = makeHarnessSpan(input, "root", "invoke_agent", {
783
+ [HARNESS_ATTRIBUTES.summary]: summary,
784
+ [HARNESS_ATTRIBUTES.coverage]: coverage,
785
+ [HARNESS_ATTRIBUTES.provenance]: safeProvenance,
786
+ ...dataPolicy === "metrics" ? {} : { [HARNESS_ATTRIBUTES.nativeContent]: rootContent }
787
+ }, started, now(), result.status === "completed" ? 1 : result.status === "failed" ? 2 : 0, true);
788
+ enqueue([root], 0, true);
789
+ await chain;
790
+ if (failed && !finalAccepted) {
791
+ summary.delivery = "failed";
792
+ summary.coverage.delivery = {
793
+ state: "partial",
794
+ reason: "Final evidence was not durably accepted"
795
+ };
796
+ }
797
+ while (Buffer.byteLength(JSON.stringify(toolCalls)) > HARNESS_LIMITS.maxControlReplyBytes / 2) {
798
+ toolCalls.pop();
799
+ summary.coverage.controlProjection = {
800
+ state: "truncated",
801
+ reason: "Read the canonical trace for remaining tool evidence"
802
+ };
803
+ }
804
+ return {
805
+ summary,
806
+ toolCalls,
807
+ ...result.reportedUsage ? { reportedUsage: result.reportedUsage } : {}
808
+ };
809
+ })().finally(() => {
810
+ if (drainTimer) clearTimeout(drainTimer);
811
+ });
812
+ return finished;
813
+ };
814
+ return {
815
+ async prepare() {
816
+ if (!ingestFullyAccepted(await options.client.post("/otel/v1/traces", { resourceSpans: [] }, { [HARNESS_BINDING_HEADER]: input.binding.token }, { timeoutMs: HARNESS_LIMITS.deliveryTimeoutMs }))) throw new Error("Harness turn authorization was refused");
817
+ },
818
+ observe,
819
+ finish
820
+ };
821
+ };
822
+ factory.status = () => ({
823
+ queuedBytes: spool.bytes,
824
+ pendingBatches,
825
+ deliveryFailures
826
+ });
827
+ return factory;
828
+ }
829
+ export { createHarnessCaptureFactory, streamCut };
830
+