@springbrand/agent-runtime 0.2.0-alpha.15 → 0.2.0-alpha.17

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 (36) hide show
  1. package/package.json +1 -1
  2. package/src/adapter/cloudflare/sandbox/adapter.ts +61 -36
  3. package/src/adapter/cloudflare/universal-agent/preparation.ts +0 -2
  4. package/src/adapter/cloudflare/workspace/scoped-workspace.ts +23 -18
  5. package/src/db/index.ts +5 -0
  6. package/src/db/schema.ts +15 -0
  7. package/src/db/telemetry-outbox.repo.ts +151 -0
  8. package/src/index.ts +1 -0
  9. package/src/kernel/approval-lifecycle.ts +35 -3
  10. package/src/kernel/bindings.ts +2 -1
  11. package/src/kernel/interaction-lifecycle.ts +35 -6
  12. package/src/layers/orchestration/temporary-agent/workspace.ts +4 -4
  13. package/src/lib/prompt.ts +22 -15
  14. package/src/pi/assembly/context.ts +2 -2
  15. package/src/pi/message/conversion.ts +13 -1
  16. package/src/pi/runtime-adapter/assembly.ts +1 -0
  17. package/src/pi/runtime-adapter/execution.ts +63 -27
  18. package/src/pi/runtime-adapter/models.ts +144 -44
  19. package/src/pi/tool/ai-adapter.ts +2 -2
  20. package/src/pi/tool/base.ts +31 -25
  21. package/src/pi/tool/compiler.ts +6 -102
  22. package/src/pi/turn/tool-recovery.ts +11 -1
  23. package/src/runtime-agent.ts +24 -0
  24. package/src/runtime-assembler.ts +38 -19
  25. package/src/runtime-definition.ts +2 -0
  26. package/src/runtime.ts +362 -20
  27. package/src/telemetry/contract.ts +389 -0
  28. package/src/telemetry/coordinator.ts +143 -0
  29. package/src/telemetry/delivery.ts +138 -0
  30. package/src/telemetry/ids.ts +60 -0
  31. package/src/telemetry/index.ts +7 -0
  32. package/src/telemetry/recorder.ts +61 -0
  33. package/src/telemetry/runtime-telemetry.ts +484 -0
  34. package/src/telemetry/sanitize.ts +97 -0
  35. package/src/tool-registry.ts +18 -11
  36. package/src/lib/telemetry-dev.ts +0 -47
@@ -0,0 +1,60 @@
1
+ import type { UniversalTelemetryEventType } from "./contract";
2
+
3
+ export type TelemetryOperationKind =
4
+ | "turn"
5
+ | "generation"
6
+ | "tool"
7
+ | "approval"
8
+ | "interaction"
9
+ | "subagent"
10
+ | "compaction";
11
+
12
+ function segment(value: string): string {
13
+ if (value.length === 0) throw new TypeError("Telemetry ID parts cannot be empty");
14
+ return `${value.length}:${value}`;
15
+ }
16
+
17
+ /** Length-prefixed parts make the deterministic identity unambiguous. */
18
+ export function telemetryOperationId(
19
+ kind: TelemetryOperationKind,
20
+ ...identity: readonly string[]
21
+ ): string {
22
+ if (identity.length === 0) {
23
+ throw new TypeError("Telemetry operation identity cannot be empty");
24
+ }
25
+ return `ua:op:${kind}:${identity.map(segment).join("")}`;
26
+ }
27
+
28
+ /**
29
+ * Stable identity for a fact. `occurrenceId` is required by callers when the
30
+ * same operation may emit a type more than once (for example recovery/steer).
31
+ */
32
+ export function telemetryEventId(
33
+ operationId: string,
34
+ type: UniversalTelemetryEventType,
35
+ occurrenceId?: string,
36
+ ): string {
37
+ if (operationId.length === 0) {
38
+ throw new TypeError("Telemetry operationId cannot be empty");
39
+ }
40
+ return `ua:event:${segment(operationId)}${segment(type)}${
41
+ occurrenceId === undefined ? "" : segment(occurrenceId)
42
+ }`;
43
+ }
44
+
45
+ export const telemetryOperations = {
46
+ turn: (sessionId: string, submissionId: string) =>
47
+ telemetryOperationId("turn", sessionId, submissionId),
48
+ generation: (submissionId: string, generationId: string) =>
49
+ telemetryOperationId("generation", submissionId, generationId),
50
+ tool: (submissionId: string, toolCallId: string) =>
51
+ telemetryOperationId("tool", submissionId, toolCallId),
52
+ approval: (submissionId: string, executionId: string) =>
53
+ telemetryOperationId("approval", submissionId, executionId),
54
+ interaction: (submissionId: string, interactionId: string) =>
55
+ telemetryOperationId("interaction", submissionId, interactionId),
56
+ subagent: (submissionId: string, subagentId: string) =>
57
+ telemetryOperationId("subagent", submissionId, subagentId),
58
+ compaction: (sessionId: string, compactionId: string) =>
59
+ telemetryOperationId("compaction", sessionId, compactionId),
60
+ } as const;
@@ -0,0 +1,7 @@
1
+ export * from "./contract";
2
+ export * from "./coordinator";
3
+ export * from "./delivery";
4
+ export * from "./ids";
5
+ export * from "./recorder";
6
+ export * from "./runtime-telemetry";
7
+ export * from "./sanitize";
@@ -0,0 +1,61 @@
1
+ import type {
2
+ NewTelemetryOutboxEvent,
3
+ TelemetryOutboxInsertResult,
4
+ } from "../db/telemetry-outbox.repo";
5
+ import {
6
+ assertUniversalTelemetryEvent,
7
+ type TelemetryPrivacyMode,
8
+ type UniversalTelemetryEvent,
9
+ } from "./contract";
10
+ import {
11
+ sanitizeTelemetryEvent,
12
+ } from "./sanitize";
13
+
14
+ export type TelemetryRecordResult =
15
+ | { readonly status: "recorded" | "duplicate" }
16
+ | {
17
+ readonly status: "dropped";
18
+ readonly reason: "capacity" | "invalid" | "conflict";
19
+ readonly error?: string;
20
+ };
21
+
22
+ export interface TelemetryOutboxWriter {
23
+ insert(event: NewTelemetryOutboxEvent): TelemetryOutboxInsertResult;
24
+ }
25
+
26
+ function errorText(error: unknown): string {
27
+ return error instanceof Error ? error.message : String(error);
28
+ }
29
+
30
+ /** Validate, sanitize and durably enqueue without changing Agent outcomes. */
31
+ export class TelemetryRecorder {
32
+ constructor(
33
+ private readonly outbox: TelemetryOutboxWriter,
34
+ private readonly privacyMode: TelemetryPrivacyMode,
35
+ ) {}
36
+
37
+ record(event: UniversalTelemetryEvent): TelemetryRecordResult {
38
+ try {
39
+ assertUniversalTelemetryEvent(event);
40
+ const sanitized = sanitizeTelemetryEvent(event, this.privacyMode);
41
+ const result = this.outbox.insert({
42
+ eventId: sanitized.eventId,
43
+ body: JSON.stringify(sanitized),
44
+ createdAt: sanitized.occurredAt,
45
+ });
46
+ if (result === "full") {
47
+ return { status: "dropped", reason: "capacity" };
48
+ }
49
+ return { status: result === "inserted" ? "recorded" : "duplicate" };
50
+ } catch (error) {
51
+ const message = errorText(error);
52
+ return {
53
+ status: "dropped",
54
+ reason: message.startsWith("Conflicting telemetry event:")
55
+ ? "conflict"
56
+ : "invalid",
57
+ error: message,
58
+ };
59
+ }
60
+ }
61
+ }
@@ -0,0 +1,484 @@
1
+ import type { TelemetryRecordResult, TelemetryRecorder } from "./recorder";
2
+ import {
3
+ createUniversalTelemetryEvent,
4
+ type TelemetryPayloadByType,
5
+ type UniversalTelemetryEventType,
6
+ type UniversalTelemetryResource,
7
+ } from "./contract";
8
+ import { telemetryEventId, telemetryOperations } from "./ids";
9
+
10
+ type EventOptions = Readonly<{
11
+ submissionId?: string;
12
+ toolCallId?: string;
13
+ parentOperationId?: string;
14
+ occurredAt?: number;
15
+ occurrenceId?: string;
16
+ }>;
17
+
18
+ /** Typed fact facade used by Runtime callsites. */
19
+ export class RuntimeTelemetry {
20
+ constructor(
21
+ private readonly resource: UniversalTelemetryResource,
22
+ private readonly recorder: TelemetryRecorder,
23
+ ) {}
24
+
25
+ private record<T extends UniversalTelemetryEventType>(
26
+ type: T,
27
+ operationId: string,
28
+ payload: TelemetryPayloadByType[T],
29
+ options: EventOptions = {},
30
+ ): TelemetryRecordResult {
31
+ try {
32
+ return this.recorder.record(createUniversalTelemetryEvent({
33
+ type,
34
+ eventId: telemetryEventId(operationId, type, options.occurrenceId),
35
+ operationId,
36
+ ...(options.parentOperationId
37
+ ? { parentOperationId: options.parentOperationId }
38
+ : {}),
39
+ ...(options.submissionId
40
+ ? { submissionId: options.submissionId }
41
+ : {}),
42
+ ...(options.toolCallId ? { toolCallId: options.toolCallId } : {}),
43
+ occurredAt: options.occurredAt ?? Date.now(),
44
+ resource: this.resource,
45
+ payload,
46
+ }));
47
+ } catch (error) {
48
+ return {
49
+ status: "dropped",
50
+ reason: "invalid",
51
+ error: error instanceof Error ? error.message : String(error),
52
+ };
53
+ }
54
+ }
55
+
56
+ turnAdmitted(input: Readonly<{
57
+ submissionId: string;
58
+ requestId: string;
59
+ idempotencyKey?: string;
60
+ occurredAt?: number;
61
+ }>): TelemetryRecordResult {
62
+ const operationId = telemetryOperations.turn(
63
+ this.resource.sessionId,
64
+ input.submissionId,
65
+ );
66
+ return this.record("turn.admitted", operationId, {
67
+ requestId: input.requestId,
68
+ ...(input.idempotencyKey ? { idempotencyKey: input.idempotencyKey } : {}),
69
+ }, input);
70
+ }
71
+
72
+ turnStarted(input: Readonly<{
73
+ submissionId: string;
74
+ assemblyRevision?: string;
75
+ input?: unknown;
76
+ occurredAt?: number;
77
+ }>): TelemetryRecordResult {
78
+ const operationId = telemetryOperations.turn(
79
+ this.resource.sessionId,
80
+ input.submissionId,
81
+ );
82
+ return this.record("turn.started", operationId, {
83
+ ...(input.assemblyRevision
84
+ ? { assemblyRevision: input.assemblyRevision }
85
+ : {}),
86
+ ...(input.input === undefined ? {} : { input: input.input as never }),
87
+ }, input);
88
+ }
89
+
90
+ turnSteered(input: Readonly<{
91
+ submissionId: string;
92
+ steerId: string;
93
+ messageId?: string;
94
+ content?: unknown;
95
+ occurredAt?: number;
96
+ }>): TelemetryRecordResult {
97
+ const operationId = telemetryOperations.turn(
98
+ this.resource.sessionId,
99
+ input.submissionId,
100
+ );
101
+ return this.record("turn.steered", operationId, {
102
+ steerId: input.steerId,
103
+ ...(input.messageId ? { messageId: input.messageId } : {}),
104
+ ...(input.content === undefined ? {} : { content: input.content as never }),
105
+ }, { ...input, occurrenceId: input.steerId });
106
+ }
107
+
108
+ turnRecovering(input: Readonly<{
109
+ submissionId: string;
110
+ reason: string;
111
+ attempt: number;
112
+ error?: string;
113
+ occurredAt?: number;
114
+ }>): TelemetryRecordResult {
115
+ const operationId = telemetryOperations.turn(
116
+ this.resource.sessionId,
117
+ input.submissionId,
118
+ );
119
+ return this.record("turn.recovering", operationId, {
120
+ reason: input.reason,
121
+ attempt: input.attempt,
122
+ ...(input.error ? { error: input.error } : {}),
123
+ }, { ...input, occurrenceId: `${input.attempt}:${input.reason}` });
124
+ }
125
+
126
+ turnCancelled(input: Readonly<{
127
+ submissionId: string;
128
+ reason?: string;
129
+ occurredAt?: number;
130
+ }>): TelemetryRecordResult {
131
+ const operationId = telemetryOperations.turn(
132
+ this.resource.sessionId,
133
+ input.submissionId,
134
+ );
135
+ return this.record("turn.cancelled", operationId, {
136
+ ...(input.reason ? { reason: input.reason } : {}),
137
+ }, input);
138
+ }
139
+
140
+ turnFinished(input: Readonly<{
141
+ submissionId: string;
142
+ outcome: "completed" | "failed" | "cancelled";
143
+ durationMs?: number;
144
+ output?: unknown;
145
+ error?: string;
146
+ occurredAt?: number;
147
+ }>): TelemetryRecordResult {
148
+ const operationId = telemetryOperations.turn(
149
+ this.resource.sessionId,
150
+ input.submissionId,
151
+ );
152
+ return this.record("turn.finished", operationId, {
153
+ outcome: input.outcome,
154
+ ...(input.durationMs === undefined ? {} : { durationMs: input.durationMs }),
155
+ ...(input.output === undefined ? {} : { output: input.output as never }),
156
+ ...(input.error ? { error: input.error } : {}),
157
+ }, input);
158
+ }
159
+
160
+ generationStarted(input: Readonly<{
161
+ submissionId: string;
162
+ generationId: string;
163
+ provider?: string;
164
+ model?: string;
165
+ input?: unknown;
166
+ occurredAt?: number;
167
+ }>): TelemetryRecordResult {
168
+ const operationId = telemetryOperations.generation(
169
+ input.submissionId,
170
+ input.generationId,
171
+ );
172
+ return this.record("generation.started", operationId, {
173
+ generationId: input.generationId,
174
+ ...(input.provider ? { provider: input.provider } : {}),
175
+ ...(input.model ? { model: input.model } : {}),
176
+ ...(input.input === undefined ? {} : { input: input.input as never }),
177
+ }, {
178
+ ...input,
179
+ parentOperationId: telemetryOperations.turn(
180
+ this.resource.sessionId,
181
+ input.submissionId,
182
+ ),
183
+ });
184
+ }
185
+
186
+ generationFinished(input: Readonly<{
187
+ submissionId: string;
188
+ generationId: string;
189
+ outcome: "completed" | "failed" | "cancelled";
190
+ durationMs?: number;
191
+ inputTokens?: number;
192
+ outputTokens?: number;
193
+ totalTokens?: number;
194
+ cost?: number;
195
+ output?: unknown;
196
+ error?: string;
197
+ occurredAt?: number;
198
+ }>): TelemetryRecordResult {
199
+ const operationId = telemetryOperations.generation(
200
+ input.submissionId,
201
+ input.generationId,
202
+ );
203
+ return this.record("generation.finished", operationId, {
204
+ generationId: input.generationId,
205
+ outcome: input.outcome,
206
+ ...(input.durationMs === undefined ? {} : { durationMs: input.durationMs }),
207
+ ...(input.inputTokens === undefined ? {} : { inputTokens: input.inputTokens }),
208
+ ...(input.outputTokens === undefined ? {} : { outputTokens: input.outputTokens }),
209
+ ...(input.totalTokens === undefined ? {} : { totalTokens: input.totalTokens }),
210
+ ...(input.cost === undefined ? {} : { cost: input.cost }),
211
+ ...(input.output === undefined ? {} : { output: input.output as never }),
212
+ ...(input.error ? { error: input.error } : {}),
213
+ }, {
214
+ ...input,
215
+ parentOperationId: telemetryOperations.turn(
216
+ this.resource.sessionId,
217
+ input.submissionId,
218
+ ),
219
+ });
220
+ }
221
+
222
+ toolStarted(input: Readonly<{
223
+ submissionId: string;
224
+ toolCallId: string;
225
+ toolName: string;
226
+ input?: unknown;
227
+ occurredAt?: number;
228
+ }>): TelemetryRecordResult {
229
+ const operationId = telemetryOperations.tool(
230
+ input.submissionId,
231
+ input.toolCallId,
232
+ );
233
+ return this.record("tool.started", operationId, {
234
+ toolName: input.toolName,
235
+ ...(input.input === undefined ? {} : { input: input.input as never }),
236
+ }, {
237
+ ...input,
238
+ parentOperationId: telemetryOperations.turn(
239
+ this.resource.sessionId,
240
+ input.submissionId,
241
+ ),
242
+ });
243
+ }
244
+
245
+ toolFinished(input: Readonly<{
246
+ submissionId: string;
247
+ toolCallId: string;
248
+ toolName: string;
249
+ outcome: "completed" | "failed" | "cancelled";
250
+ durationMs?: number;
251
+ outputBytes?: number;
252
+ spilled?: boolean;
253
+ output?: unknown;
254
+ error?: string;
255
+ occurredAt?: number;
256
+ }>): TelemetryRecordResult {
257
+ const operationId = telemetryOperations.tool(
258
+ input.submissionId,
259
+ input.toolCallId,
260
+ );
261
+ return this.record("tool.finished", operationId, {
262
+ toolName: input.toolName,
263
+ outcome: input.outcome,
264
+ ...(input.durationMs === undefined ? {} : { durationMs: input.durationMs }),
265
+ ...(input.outputBytes === undefined ? {} : { outputBytes: input.outputBytes }),
266
+ ...(input.spilled === undefined ? {} : { spilled: input.spilled }),
267
+ ...(input.output === undefined ? {} : { output: input.output as never }),
268
+ ...(input.error ? { error: input.error } : {}),
269
+ }, {
270
+ ...input,
271
+ parentOperationId: telemetryOperations.turn(
272
+ this.resource.sessionId,
273
+ input.submissionId,
274
+ ),
275
+ });
276
+ }
277
+
278
+ approvalRequested(input: Readonly<{
279
+ submissionId: string;
280
+ toolCallId: string;
281
+ executionId: string;
282
+ toolName: string;
283
+ summary?: string;
284
+ input?: unknown;
285
+ occurredAt?: number;
286
+ }>): TelemetryRecordResult {
287
+ const operationId = telemetryOperations.approval(
288
+ input.submissionId,
289
+ input.executionId,
290
+ );
291
+ return this.record("approval.requested", operationId, {
292
+ executionId: input.executionId,
293
+ toolName: input.toolName,
294
+ ...(input.summary ? { summary: input.summary } : {}),
295
+ ...(input.input === undefined ? {} : { input: input.input as never }),
296
+ }, {
297
+ ...input,
298
+ parentOperationId: telemetryOperations.tool(
299
+ input.submissionId,
300
+ input.toolCallId,
301
+ ),
302
+ });
303
+ }
304
+
305
+ approvalDecided(input: Readonly<{
306
+ submissionId: string;
307
+ toolCallId: string;
308
+ executionId: string;
309
+ decision: "approved" | "rejected" | "cancelled";
310
+ reason?: string;
311
+ occurredAt?: number;
312
+ }>): TelemetryRecordResult {
313
+ const operationId = telemetryOperations.approval(
314
+ input.submissionId,
315
+ input.executionId,
316
+ );
317
+ return this.record("approval.decided", operationId, {
318
+ executionId: input.executionId,
319
+ decision: input.decision,
320
+ ...(input.reason ? { reason: input.reason } : {}),
321
+ }, {
322
+ ...input,
323
+ parentOperationId: telemetryOperations.tool(
324
+ input.submissionId,
325
+ input.toolCallId,
326
+ ),
327
+ });
328
+ }
329
+
330
+ interactionRequested(input: Readonly<{
331
+ submissionId: string;
332
+ toolCallId: string;
333
+ interactionId: string;
334
+ toolName: string;
335
+ input?: unknown;
336
+ occurredAt?: number;
337
+ }>): TelemetryRecordResult {
338
+ const operationId = telemetryOperations.interaction(
339
+ input.submissionId,
340
+ input.interactionId,
341
+ );
342
+ return this.record("interaction.requested", operationId, {
343
+ interactionId: input.interactionId,
344
+ toolName: input.toolName,
345
+ ...(input.input === undefined ? {} : { input: input.input as never }),
346
+ }, {
347
+ ...input,
348
+ parentOperationId: telemetryOperations.tool(
349
+ input.submissionId,
350
+ input.toolCallId,
351
+ ),
352
+ });
353
+ }
354
+
355
+ interactionResponded(input: Readonly<{
356
+ submissionId: string;
357
+ toolCallId: string;
358
+ interactionId: string;
359
+ response?: unknown;
360
+ occurredAt?: number;
361
+ }>): TelemetryRecordResult {
362
+ const operationId = telemetryOperations.interaction(
363
+ input.submissionId,
364
+ input.interactionId,
365
+ );
366
+ return this.record("interaction.responded", operationId, {
367
+ interactionId: input.interactionId,
368
+ ...(input.response === undefined
369
+ ? {}
370
+ : { response: input.response as never }),
371
+ }, {
372
+ ...input,
373
+ parentOperationId: telemetryOperations.tool(
374
+ input.submissionId,
375
+ input.toolCallId,
376
+ ),
377
+ });
378
+ }
379
+
380
+ interactionCancelled(input: Readonly<{
381
+ submissionId: string;
382
+ toolCallId: string;
383
+ interactionId: string;
384
+ reason?: string;
385
+ occurredAt?: number;
386
+ }>): TelemetryRecordResult {
387
+ const operationId = telemetryOperations.interaction(
388
+ input.submissionId,
389
+ input.interactionId,
390
+ );
391
+ return this.record("interaction.cancelled", operationId, {
392
+ interactionId: input.interactionId,
393
+ ...(input.reason ? { reason: input.reason } : {}),
394
+ }, {
395
+ ...input,
396
+ parentOperationId: telemetryOperations.tool(
397
+ input.submissionId,
398
+ input.toolCallId,
399
+ ),
400
+ });
401
+ }
402
+
403
+ subagentStarted(input: Readonly<{
404
+ submissionId: string;
405
+ toolCallId?: string;
406
+ subagentId: string;
407
+ agentType?: string;
408
+ prompt?: unknown;
409
+ occurredAt?: number;
410
+ }>): TelemetryRecordResult {
411
+ const operationId = telemetryOperations.subagent(
412
+ input.submissionId,
413
+ input.subagentId,
414
+ );
415
+ return this.record("subagent.started", operationId, {
416
+ subagentId: input.subagentId,
417
+ ...(input.agentType ? { agentType: input.agentType } : {}),
418
+ ...(input.prompt === undefined ? {} : { prompt: input.prompt as never }),
419
+ }, {
420
+ ...input,
421
+ parentOperationId: input.toolCallId
422
+ ? telemetryOperations.tool(input.submissionId, input.toolCallId)
423
+ : telemetryOperations.turn(this.resource.sessionId, input.submissionId),
424
+ });
425
+ }
426
+
427
+ subagentFinished(input: Readonly<{
428
+ submissionId: string;
429
+ toolCallId?: string;
430
+ subagentId: string;
431
+ outcome: "completed" | "failed" | "cancelled";
432
+ durationMs?: number;
433
+ output?: unknown;
434
+ error?: string;
435
+ occurredAt?: number;
436
+ }>): TelemetryRecordResult {
437
+ const operationId = telemetryOperations.subagent(
438
+ input.submissionId,
439
+ input.subagentId,
440
+ );
441
+ return this.record("subagent.finished", operationId, {
442
+ subagentId: input.subagentId,
443
+ outcome: input.outcome,
444
+ ...(input.durationMs === undefined ? {} : { durationMs: input.durationMs }),
445
+ ...(input.output === undefined ? {} : { output: input.output as never }),
446
+ ...(input.error ? { error: input.error } : {}),
447
+ }, {
448
+ ...input,
449
+ parentOperationId: input.toolCallId
450
+ ? telemetryOperations.tool(input.submissionId, input.toolCallId)
451
+ : telemetryOperations.turn(this.resource.sessionId, input.submissionId),
452
+ });
453
+ }
454
+
455
+ contextCompacted(input: Readonly<{
456
+ submissionId: string;
457
+ compactionId: string;
458
+ inputTokens?: number;
459
+ outputTokens?: number;
460
+ removedMessages?: number;
461
+ summary?: unknown;
462
+ occurredAt?: number;
463
+ }>): TelemetryRecordResult {
464
+ const operationId = telemetryOperations.compaction(
465
+ this.resource.sessionId,
466
+ input.compactionId,
467
+ );
468
+ return this.record("context.compacted", operationId, {
469
+ compactionId: input.compactionId,
470
+ ...(input.inputTokens === undefined ? {} : { inputTokens: input.inputTokens }),
471
+ ...(input.outputTokens === undefined ? {} : { outputTokens: input.outputTokens }),
472
+ ...(input.removedMessages === undefined
473
+ ? {}
474
+ : { removedMessages: input.removedMessages }),
475
+ ...(input.summary === undefined ? {} : { summary: input.summary as never }),
476
+ }, {
477
+ ...input,
478
+ parentOperationId: telemetryOperations.turn(
479
+ this.resource.sessionId,
480
+ input.submissionId,
481
+ ),
482
+ });
483
+ }
484
+ }
@@ -0,0 +1,97 @@
1
+ import {
2
+ assertUniversalTelemetryEvent,
3
+ type TelemetryPrivacyMode,
4
+ type TelemetryValue,
5
+ type UniversalTelemetryEvent,
6
+ type UniversalTelemetryEventType,
7
+ } from "./contract";
8
+
9
+ export const TELEMETRY_SANITIZE_LIMITS = {
10
+ maxDepth: 16,
11
+ maxArrayItems: 200,
12
+ maxObjectEntries: 500,
13
+ metadataStringLength: 512,
14
+ contentStringLength: 20_000,
15
+ } as const;
16
+
17
+ export const TELEMETRY_REDACTED = "[REDACTED]";
18
+ export const TELEMETRY_CONTENT_OMITTED = "[CONTENT OMITTED]";
19
+
20
+ const SECRET_KEYS = /(?:authorization|cookie|credential|password|passwd|secret|api.?key|access.?token|refresh.?token|private.?key)/i;
21
+ const CONTENT_KEYS = /^(?:args?|input|output|result|response|prompt|messages?|content|text|partialtext|partialparts|summary|reason|error)$/i;
22
+ const SECRET_VALUE_PATTERNS = [
23
+ /\bbearer\s+[a-z0-9._~+/=-]+/i,
24
+ /\b(?:sk|pk)_(?:live|test)_[a-z0-9_-]{12,}\b/i,
25
+ /\bsk-[a-z0-9_-]{12,}\b/i,
26
+ /\beyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\b/,
27
+ ];
28
+
29
+ function normalizedKey(key: string): string {
30
+ return key.replace(/[^a-z0-9]/gi, "");
31
+ }
32
+
33
+ function truncate(value: string, limit: number): string {
34
+ if (value.length <= limit) return value;
35
+ return `${value.slice(0, limit)}…[truncated ${value.length - limit} chars]`;
36
+ }
37
+
38
+ function sanitizeString(value: string, limit: number): string {
39
+ if (SECRET_VALUE_PATTERNS.some((pattern) => pattern.test(value))) {
40
+ return TELEMETRY_REDACTED;
41
+ }
42
+ return truncate(value, limit);
43
+ }
44
+
45
+ function sanitizeValue(
46
+ value: unknown,
47
+ mode: TelemetryPrivacyMode,
48
+ depth: number,
49
+ key?: string,
50
+ ): TelemetryValue | undefined {
51
+ const normalized = key === undefined ? "" : normalizedKey(key);
52
+ if (key !== undefined && SECRET_KEYS.test(normalized)) {
53
+ return TELEMETRY_REDACTED;
54
+ }
55
+ if (mode === "metadata" && key !== undefined && CONTENT_KEYS.test(normalized)) {
56
+ return TELEMETRY_CONTENT_OMITTED;
57
+ }
58
+ if (depth >= TELEMETRY_SANITIZE_LIMITS.maxDepth) {
59
+ return "[MAX DEPTH]";
60
+ }
61
+ if (value === null || typeof value === "boolean") return value;
62
+ if (typeof value === "number") return Number.isFinite(value) ? value : null;
63
+ if (typeof value === "string") {
64
+ const limit = mode === "content"
65
+ ? TELEMETRY_SANITIZE_LIMITS.contentStringLength
66
+ : TELEMETRY_SANITIZE_LIMITS.metadataStringLength;
67
+ return sanitizeString(value, limit);
68
+ }
69
+ if (Array.isArray(value)) {
70
+ return value
71
+ .slice(0, TELEMETRY_SANITIZE_LIMITS.maxArrayItems)
72
+ .map((item) => sanitizeValue(item, mode, depth + 1) ?? null);
73
+ }
74
+ if (typeof value === "object" && value !== null) {
75
+ const output: Record<string, TelemetryValue> = {};
76
+ for (const [entryKey, entryValue] of Object.entries(value).slice(
77
+ 0,
78
+ TELEMETRY_SANITIZE_LIMITS.maxObjectEntries,
79
+ )) {
80
+ const sanitized = sanitizeValue(entryValue, mode, depth + 1, entryKey);
81
+ if (sanitized !== undefined) output[entryKey] = sanitized;
82
+ }
83
+ return output;
84
+ }
85
+ return undefined;
86
+ }
87
+
88
+ /** Sanitize before persistence; downstream consumers never see raw secrets. */
89
+ export function sanitizeTelemetryEvent<T extends UniversalTelemetryEventType>(
90
+ event: UniversalTelemetryEvent<T>,
91
+ mode: TelemetryPrivacyMode,
92
+ ): UniversalTelemetryEvent<T> {
93
+ assertUniversalTelemetryEvent(event);
94
+ const sanitized = sanitizeValue(event, mode, 0);
95
+ assertUniversalTelemetryEvent(sanitized);
96
+ return sanitized as unknown as UniversalTelemetryEvent<T>;
97
+ }