xo-harness 0.2.0 → 0.3.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.
Files changed (115) hide show
  1. package/README.md +92 -5
  2. package/dist/browser/worklets/capture-processor.js +34 -0
  3. package/dist/browser/worklets/playback-processor.js +188 -0
  4. package/dist/browser.d.ts +1 -0
  5. package/dist/browser.js +1 -0
  6. package/dist/internal/browser/browser-voice-client.d.ts +33 -0
  7. package/dist/internal/browser/browser-voice-client.js +316 -0
  8. package/dist/internal/browser/index.d.ts +2 -0
  9. package/dist/internal/browser/index.js +1 -0
  10. package/dist/internal/harness/conversation-context.d.ts +45 -0
  11. package/dist/internal/harness/conversation-context.js +79 -0
  12. package/dist/internal/harness/conversation-projection.d.ts +55 -0
  13. package/dist/internal/harness/conversation-projection.js +145 -0
  14. package/dist/internal/harness/event-stream.d.ts +23 -2
  15. package/dist/internal/harness/event-stream.js +148 -18
  16. package/dist/internal/harness/index.d.ts +7 -1
  17. package/dist/internal/harness/index.js +7 -1
  18. package/dist/internal/harness/message.d.ts +24 -11
  19. package/dist/internal/harness/message.js +271 -77
  20. package/dist/internal/harness/report-diff.d.ts +3 -0
  21. package/dist/internal/harness/report-diff.js +10 -0
  22. package/dist/internal/harness/report.d.ts +17 -14
  23. package/dist/internal/harness/report.js +41 -24
  24. package/dist/internal/harness/runtime-limits.d.ts +18 -0
  25. package/dist/internal/harness/runtime-limits.js +19 -0
  26. package/dist/internal/harness/session-persistence.d.ts +16 -0
  27. package/dist/internal/harness/session-persistence.js +106 -0
  28. package/dist/internal/harness/shadow.d.ts +5 -5
  29. package/dist/internal/harness/shadow.js +214 -52
  30. package/dist/internal/harness/socket-bridge.d.ts +45 -4
  31. package/dist/internal/harness/socket-bridge.js +215 -46
  32. package/dist/internal/harness/task-supervisor.d.ts +6 -2
  33. package/dist/internal/harness/task-supervisor.js +97 -15
  34. package/dist/internal/harness/tool-calls.d.ts +40 -0
  35. package/dist/internal/harness/tool-calls.js +132 -0
  36. package/dist/internal/harness/tool-policy.d.ts +2 -2
  37. package/dist/internal/harness/tool-runtime.d.ts +2 -2
  38. package/dist/internal/harness/tool-runtime.js +37 -40
  39. package/dist/internal/harness/tools.d.ts +21 -0
  40. package/dist/internal/harness/tools.js +7 -1
  41. package/dist/internal/harness/usage-tracker.d.ts +39 -0
  42. package/dist/internal/harness/usage-tracker.js +104 -0
  43. package/dist/internal/harness/voice-session.d.ts +39 -8
  44. package/dist/internal/harness/voice-session.js +353 -58
  45. package/dist/internal/harness/xo.d.ts +7 -12
  46. package/dist/internal/harness/xo.js +25 -13
  47. package/dist/internal/protocol/async-queue.d.ts +22 -1
  48. package/dist/internal/protocol/async-queue.js +86 -12
  49. package/dist/internal/protocol/audio.d.ts +16 -2
  50. package/dist/internal/protocol/audio.js +23 -5
  51. package/dist/internal/protocol/backend-output.d.ts +70 -0
  52. package/dist/internal/protocol/backend-output.js +39 -0
  53. package/dist/internal/protocol/event-json.d.ts +3 -0
  54. package/dist/internal/protocol/event-json.js +15 -0
  55. package/dist/internal/protocol/events.d.ts +342 -4
  56. package/dist/internal/protocol/events.js +53 -25
  57. package/dist/internal/protocol/index.d.ts +5 -0
  58. package/dist/internal/protocol/index.js +5 -0
  59. package/dist/internal/protocol/output-source.d.ts +18 -0
  60. package/dist/internal/protocol/output-source.js +15 -0
  61. package/dist/internal/protocol/paced-audio.d.ts +27 -0
  62. package/dist/internal/protocol/paced-audio.js +117 -0
  63. package/dist/internal/protocol/parts.d.ts +154 -0
  64. package/dist/internal/protocol/parts.js +32 -6
  65. package/dist/internal/protocol/provider.d.ts +308 -1
  66. package/dist/internal/protocol/provider.js +102 -3
  67. package/dist/internal/protocol/records.d.ts +773 -0
  68. package/dist/internal/protocol/records.js +46 -0
  69. package/dist/internal/protocol/tools.d.ts +40 -0
  70. package/dist/internal/protocol/tools.js +12 -0
  71. package/dist/internal/protocol/transcript.d.ts +19 -0
  72. package/dist/internal/protocol/transcript.js +53 -0
  73. package/dist/internal/provider/contract.d.ts +25 -2
  74. package/dist/internal/provider/event-queue.d.ts +17 -0
  75. package/dist/internal/provider/event-queue.js +78 -0
  76. package/dist/internal/provider/grok-voice.d.ts +12 -9
  77. package/dist/internal/provider/grok-voice.js +26 -15
  78. package/dist/internal/provider/index.d.ts +1 -0
  79. package/dist/internal/provider/index.js +1 -0
  80. package/dist/internal/provider/live-session.d.ts +29 -0
  81. package/dist/internal/provider/live-session.js +840 -0
  82. package/dist/internal/provider/node-socket.js +6 -0
  83. package/dist/internal/provider/openai-live.d.ts +62 -0
  84. package/dist/internal/provider/openai-live.js +160 -0
  85. package/dist/internal/provider/openai-realtime.d.ts +9 -5
  86. package/dist/internal/provider/openai-realtime.js +24 -10
  87. package/dist/internal/provider/realtime-session.d.ts +12 -1
  88. package/dist/internal/provider/realtime-session.js +373 -70
  89. package/dist/internal/provider/realtime-socket.d.ts +3 -1
  90. package/dist/internal/provider/tool-status-context.d.ts +10 -0
  91. package/dist/internal/provider/tool-status-context.js +30 -0
  92. package/dist/internal/provider/workers-socket.js +63 -15
  93. package/dist/internal/provider/workers.d.ts +1 -0
  94. package/dist/internal/provider/workers.js +1 -0
  95. package/dist/internal/provider-fake/replay-voice-provider.d.ts +9 -11
  96. package/dist/internal/provider-fake/replay-voice-provider.js +47 -23
  97. package/dist/internal/storage/event-store.d.ts +22 -4
  98. package/dist/internal/storage/jsonl-event-store.d.ts +4 -4
  99. package/dist/internal/storage/jsonl-event-store.js +21 -16
  100. package/dist/internal/storage/memory-event-store.d.ts +4 -3
  101. package/dist/internal/storage/memory-event-store.js +8 -2
  102. package/dist/internal/storage/memory.d.ts +1 -1
  103. package/dist/internal/testkit/events.d.ts +14 -0
  104. package/dist/internal/testkit/events.js +33 -0
  105. package/dist/internal/testkit/runtime.d.ts +3 -0
  106. package/dist/internal/testkit/runtime.js +3 -0
  107. package/dist/internal/testkit/trajectory.d.ts +23 -0
  108. package/dist/internal/testkit/trajectory.js +58 -0
  109. package/dist/internal/tools-openai/index.d.ts +2 -0
  110. package/dist/internal/tools-openai/index.js +79 -72
  111. package/dist/internal/tools-openai/responses.d.ts +5 -1
  112. package/dist/internal/tools-openai/responses.js +53 -5
  113. package/dist/testing.d.ts +1 -0
  114. package/dist/testing.js +1 -0
  115. package/package.json +7 -1
@@ -5,29 +5,41 @@ export class XO {
5
5
  tools;
6
6
  #toolPolicy;
7
7
  #maxToolResultBytes;
8
+ #runtimeLimits;
9
+ #startingSessionIds = new Set();
8
10
  constructor(options) {
9
11
  this.store = options.store;
10
12
  this.tools = new ToolRegistry(options.tools);
11
13
  this.#toolPolicy = options.toolPolicy;
12
14
  this.#maxToolResultBytes = options.maxToolResultBytes;
15
+ this.#runtimeLimits = options.runtimeLimits === undefined ? undefined : { ...options.runtimeLimits };
13
16
  }
14
17
  registerTool(tool) {
15
18
  this.tools.register(tool);
16
19
  }
17
- startSession(options) {
20
+ async startSession(options) {
21
+ const sessionId = options.sessionId ?? crypto.randomUUID();
22
+ if (this.#startingSessionIds.has(sessionId))
23
+ throw new Error(`Session id already exists: ${sessionId}`);
24
+ // Reserve before startup's first await. Once it settles, the persisted start record
25
+ // owns the ID; other XO instances/processes still need host-owned admission.
26
+ this.#startingSessionIds.add(sessionId);
18
27
  const toolPolicy = options.toolPolicy ?? this.#toolPolicy;
19
28
  const maxToolResultBytes = options.maxToolResultBytes ?? this.#maxToolResultBytes;
20
- return VoiceSession.create({
21
- sessionId: options.sessionId ?? crypto.randomUUID(),
22
- provider: options.provider,
23
- ...(options.signal === undefined ? {} : { signal: options.signal }),
24
- store: this.store,
25
- tools: this.tools,
26
- ...(toolPolicy === undefined ? {} : { toolPolicy }),
27
- ...(maxToolResultBytes === undefined ? {} : { maxToolResultBytes }),
28
- ...(options.instructions === undefined ? {} : { instructions: options.instructions }),
29
- ...(options.outputModalities === undefined ? {} : { outputModalities: options.outputModalities }),
30
- ...(options.maxDurationMs === undefined ? {} : { maxDurationMs: options.maxDurationMs }),
31
- });
29
+ try {
30
+ return await VoiceSession.create({
31
+ ...options,
32
+ sessionId,
33
+ provider: options.provider,
34
+ store: this.store,
35
+ tools: this.tools,
36
+ ...(toolPolicy === undefined ? {} : { toolPolicy }),
37
+ ...(maxToolResultBytes === undefined ? {} : { maxToolResultBytes }),
38
+ runtimeLimits: { ...this.#runtimeLimits, ...options.runtimeLimits },
39
+ });
40
+ }
41
+ finally {
42
+ this.#startingSessionIds.delete(sessionId);
43
+ }
32
44
  }
33
45
  }
@@ -1,8 +1,29 @@
1
- /** Unbounded push/pull bridge between a callback producer and an async-iterating consumer. */
1
+ export interface AsyncQueueOptions<Value> {
2
+ maxItems?: number;
3
+ maxBytes?: number;
4
+ sizeOf?: (value: Value) => number;
5
+ }
6
+ export declare class AsyncQueueOverflowError extends Error {
7
+ readonly limit: "items" | "bytes";
8
+ readonly maximum: number;
9
+ readonly code: "queue_items_exceeded" | "queue_bytes_exceeded";
10
+ constructor(limit: "items" | "bytes", maximum: number);
11
+ }
12
+ /** FIFO callback/iterator bridge. Optional limits fail explicitly and discard undelivered values. */
2
13
  export declare class AsyncQueue<Value> implements AsyncIterableIterator<Value> {
3
14
  #private;
15
+ constructor(options?: AsyncQueueOptions<Value>);
4
16
  push(value: Value): void;
5
17
  close(): void;
18
+ /** Failure is outside capacity: it remains observable even when the queue was full. */
19
+ fail(error: unknown): void;
20
+ stats(): {
21
+ pendingItems: number;
22
+ pendingBytes: number;
23
+ state: "open" | "closed" | "failed";
24
+ };
6
25
  next(): Promise<IteratorResult<Value>>;
26
+ /** Explicit abandonment; iterator breaks retain the historical shared-queue behavior. */
27
+ discard(): void;
7
28
  [Symbol.asyncIterator](): AsyncIterableIterator<Value>;
8
29
  }
@@ -1,34 +1,108 @@
1
- /** Unbounded push/pull bridge between a callback producer and an async-iterating consumer. */
1
+ export class AsyncQueueOverflowError extends Error {
2
+ limit;
3
+ maximum;
4
+ code;
5
+ constructor(limit, maximum) {
6
+ super(`Event queue exceeds its ${maximum} ${limit} limit; undelivered events were discarded`);
7
+ this.limit = limit;
8
+ this.maximum = maximum;
9
+ this.name = "AsyncQueueOverflowError";
10
+ this.code = limit === "items" ? "queue_items_exceeded" : "queue_bytes_exceeded";
11
+ }
12
+ }
13
+ /** FIFO callback/iterator bridge. Optional limits fail explicitly and discard undelivered values. */
2
14
  export class AsyncQueue {
3
15
  #values = [];
4
16
  #resolvers = [];
17
+ #maxItems;
18
+ #maxBytes;
19
+ #sizeOf;
20
+ #bytes = 0;
5
21
  #closed = false;
22
+ #failure;
23
+ constructor(options = {}) {
24
+ this.#maxItems = queueLimit(options.maxItems, "maxItems");
25
+ this.#maxBytes = queueLimit(options.maxBytes, "maxBytes");
26
+ this.#sizeOf = options.sizeOf ?? (() => 0);
27
+ }
6
28
  push(value) {
29
+ if (this.#failure)
30
+ throw this.#failure.error;
7
31
  if (this.#closed)
8
32
  throw new Error("Cannot push to a closed queue");
9
- const resolve = this.#resolvers.shift();
10
- if (resolve)
11
- resolve({ done: false, value });
12
- else
13
- this.#values.push(value);
33
+ const bytes = this.#sizeOf(value);
34
+ if (!Number.isSafeInteger(bytes) || bytes < 0)
35
+ throw new RangeError("Queue size must be a nonnegative safe integer");
36
+ const limit = this.#bytes + bytes > this.#maxBytes
37
+ ? "bytes"
38
+ : this.#resolvers.length === 0 && this.#values.length >= this.#maxItems
39
+ ? "items"
40
+ : undefined;
41
+ if (limit) {
42
+ const error = new AsyncQueueOverflowError(limit, limit === "items" ? this.#maxItems : this.#maxBytes);
43
+ this.fail(error);
44
+ throw error;
45
+ }
46
+ const waiter = this.#resolvers.shift();
47
+ if (waiter)
48
+ waiter.resolve({ done: false, value });
49
+ else {
50
+ this.#values.push({ value, bytes });
51
+ this.#bytes += bytes;
52
+ }
14
53
  }
15
54
  close() {
16
55
  if (this.#closed)
17
56
  return;
18
57
  this.#closed = true;
19
- for (const resolve of this.#resolvers.splice(0)) {
20
- resolve({ done: true, value: undefined });
58
+ for (const waiter of this.#resolvers.splice(0)) {
59
+ waiter.resolve({ done: true, value: undefined });
21
60
  }
22
61
  }
62
+ /** Failure is outside capacity: it remains observable even when the queue was full. */
63
+ fail(error) {
64
+ if (this.#failure)
65
+ return;
66
+ this.#failure = { error };
67
+ this.#closed = true;
68
+ this.#values.length = 0;
69
+ this.#bytes = 0;
70
+ for (const waiter of this.#resolvers.splice(0))
71
+ waiter.reject(error);
72
+ }
73
+ stats() {
74
+ return {
75
+ pendingItems: this.#values.length,
76
+ pendingBytes: this.#bytes,
77
+ state: this.#failure ? "failed" : this.#closed ? "closed" : "open",
78
+ };
79
+ }
23
80
  next() {
24
- const value = this.#values.shift();
25
- if (value !== undefined)
26
- return Promise.resolve({ done: false, value });
81
+ if (this.#failure)
82
+ return Promise.reject(this.#failure.error);
83
+ const entry = this.#values.shift();
84
+ if (entry !== undefined) {
85
+ this.#bytes -= entry.bytes;
86
+ return Promise.resolve({ done: false, value: entry.value });
87
+ }
27
88
  if (this.#closed)
28
89
  return Promise.resolve({ done: true, value: undefined });
29
- return new Promise((resolve) => this.#resolvers.push(resolve));
90
+ return new Promise((resolve, reject) => this.#resolvers.push({ resolve, reject }));
91
+ }
92
+ /** Explicit abandonment; iterator breaks retain the historical shared-queue behavior. */
93
+ discard() {
94
+ this.#values.length = 0;
95
+ this.#bytes = 0;
96
+ this.close();
30
97
  }
31
98
  [Symbol.asyncIterator]() {
32
99
  return this;
33
100
  }
34
101
  }
102
+ function queueLimit(value, name) {
103
+ if (value === undefined || value === Number.POSITIVE_INFINITY)
104
+ return Number.POSITIVE_INFINITY;
105
+ if (!Number.isSafeInteger(value) || value < 1)
106
+ throw new RangeError(`${name} must be a positive safe integer or Infinity`);
107
+ return value;
108
+ }
@@ -7,6 +7,20 @@ export declare const AudioChunkSchema: z.ZodObject<{
7
7
  channels: z.ZodInt;
8
8
  startSample: z.ZodInt;
9
9
  data: z.ZodCustom<Uint8Array<ArrayBuffer>, Uint8Array<ArrayBuffer>>;
10
- }, z.core.$strip>;
10
+ }, z.core.$strict>;
11
11
  export type AudioChunk = z.infer<typeof AudioChunkSchema>;
12
- export declare function audioSampleCount(chunk: AudioChunk): number;
12
+ /** A recorded sample range whose PCM was deliberately not retained. Never playable audio. */
13
+ export declare const AudioChunkMetadataSchema: z.ZodObject<{
14
+ streamId: z.ZodString;
15
+ sequence: z.ZodInt;
16
+ encoding: z.ZodLiteral<"pcm_s16le">;
17
+ sampleRate: z.ZodInt;
18
+ channels: z.ZodInt;
19
+ startSample: z.ZodInt;
20
+ sampleCount: z.ZodInt;
21
+ availability: z.ZodLiteral<"omitted">;
22
+ data: z.ZodOptional<z.ZodNever>;
23
+ }, z.core.$strict>;
24
+ export type AudioChunkMetadata = z.infer<typeof AudioChunkMetadataSchema>;
25
+ export declare function audioSampleCount(chunk: AudioChunk | AudioChunkMetadata): number;
26
+ export declare function toAudioMetadata(chunk: AudioChunk | AudioChunkMetadata): AudioChunkMetadata;
@@ -1,14 +1,14 @@
1
1
  import { z } from "zod";
2
- export const AudioChunkSchema = z
3
- .object({
2
+ const AudioFormatSchema = z.object({
4
3
  streamId: z.string().min(1),
5
4
  sequence: z.int().nonnegative(),
6
5
  encoding: z.literal("pcm_s16le"),
7
6
  sampleRate: z.int().positive(),
8
7
  channels: z.int().positive(),
9
8
  startSample: z.int().nonnegative(),
10
- data: z.instanceof(Uint8Array),
11
- })
9
+ });
10
+ export const AudioChunkSchema = AudioFormatSchema.extend({ data: z.instanceof(Uint8Array) })
11
+ .strict()
12
12
  .superRefine((chunk, context) => {
13
13
  const frameWidth = chunk.channels * 2;
14
14
  if (chunk.data.byteLength % frameWidth !== 0) {
@@ -19,6 +19,24 @@ export const AudioChunkSchema = z
19
19
  });
20
20
  }
21
21
  });
22
+ /** A recorded sample range whose PCM was deliberately not retained. Never playable audio. */
23
+ export const AudioChunkMetadataSchema = AudioFormatSchema.extend({
24
+ sampleCount: z.int().nonnegative(),
25
+ availability: z.literal("omitted"),
26
+ data: z.never().optional(),
27
+ }).strict();
22
28
  export function audioSampleCount(chunk) {
23
- return chunk.data.byteLength / (chunk.channels * 2);
29
+ return chunk.data === undefined ? chunk.sampleCount : chunk.data.byteLength / (chunk.channels * 2);
30
+ }
31
+ export function toAudioMetadata(chunk) {
32
+ return AudioChunkMetadataSchema.parse({
33
+ streamId: chunk.streamId,
34
+ sequence: chunk.sequence,
35
+ encoding: chunk.encoding,
36
+ sampleRate: chunk.sampleRate,
37
+ channels: chunk.channels,
38
+ startSample: chunk.startSample,
39
+ sampleCount: audioSampleCount(chunk),
40
+ availability: "omitted",
41
+ });
24
42
  }
@@ -0,0 +1,70 @@
1
+ import { z } from "zod";
2
+ /** Backend output is not a spoken caption or evidence of device playback. */
3
+ export declare const BackendOutputSourceSchema: z.ZodObject<{
4
+ delegationId: z.ZodOptional<z.ZodString>;
5
+ scope: z.ZodLiteral<"backend">;
6
+ responseId: z.ZodString;
7
+ itemId: z.ZodString;
8
+ outputIndex: z.ZodInt;
9
+ }, z.core.$strip>;
10
+ /** Public content positions within one native item; private/encrypted reasoning is excluded. */
11
+ export declare const BackendOutputContentSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
12
+ kind: z.ZodEnum<{
13
+ refusal: "refusal";
14
+ text: "text";
15
+ }>;
16
+ text: z.ZodString;
17
+ contentIndex: z.ZodInt;
18
+ }, z.core.$strip>, z.ZodObject<{
19
+ kind: z.ZodLiteral<"reasoning_summary">;
20
+ text: z.ZodString;
21
+ summaryIndex: z.ZodInt;
22
+ }, z.core.$strip>], "kind">;
23
+ export declare const BackendOutputDeltaSchema: z.ZodObject<{
24
+ kind: z.ZodEnum<{
25
+ reasoning_summary: "reasoning_summary";
26
+ refusal: "refusal";
27
+ text: "text";
28
+ }>;
29
+ source: z.ZodObject<{
30
+ delegationId: z.ZodOptional<z.ZodString>;
31
+ scope: z.ZodLiteral<"backend">;
32
+ responseId: z.ZodString;
33
+ itemId: z.ZodString;
34
+ outputIndex: z.ZodInt;
35
+ contentIndex: z.ZodOptional<z.ZodInt>;
36
+ summaryIndex: z.ZodOptional<z.ZodInt>;
37
+ }, z.core.$strip>;
38
+ delta: z.ZodString;
39
+ }, z.core.$strip>;
40
+ export declare const BackendOutputSchema: z.ZodObject<{
41
+ source: z.ZodObject<{
42
+ delegationId: z.ZodOptional<z.ZodString>;
43
+ scope: z.ZodLiteral<"backend">;
44
+ responseId: z.ZodString;
45
+ itemId: z.ZodString;
46
+ outputIndex: z.ZodInt;
47
+ }, z.core.$strip>;
48
+ content: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
49
+ kind: z.ZodEnum<{
50
+ refusal: "refusal";
51
+ text: "text";
52
+ }>;
53
+ text: z.ZodString;
54
+ contentIndex: z.ZodInt;
55
+ }, z.core.$strip>, z.ZodObject<{
56
+ kind: z.ZodLiteral<"reasoning_summary">;
57
+ text: z.ZodString;
58
+ summaryIndex: z.ZodInt;
59
+ }, z.core.$strip>], "kind">>;
60
+ status: z.ZodOptional<z.ZodEnum<{
61
+ cancelled: "cancelled";
62
+ completed: "completed";
63
+ failed: "failed";
64
+ in_progress: "in_progress";
65
+ incomplete: "incomplete";
66
+ }>>;
67
+ }, z.core.$strip>;
68
+ export type BackendOutput = z.infer<typeof BackendOutputSchema>;
69
+ export type BackendOutputDelta = z.infer<typeof BackendOutputDeltaSchema>;
70
+ export type BackendOutputContent = z.infer<typeof BackendOutputContentSchema>;
@@ -0,0 +1,39 @@
1
+ import { z } from "zod";
2
+ import { ProviderOutputSourceSchema } from "./output-source.js";
3
+ /** Backend output is not a spoken caption or evidence of device playback. */
4
+ export const BackendOutputSourceSchema = ProviderOutputSourceSchema.omit({
5
+ contentIndex: true,
6
+ summaryIndex: true,
7
+ }).extend({
8
+ scope: z.literal("backend"),
9
+ responseId: z.string().min(1),
10
+ itemId: z.string().min(1),
11
+ outputIndex: z.int().nonnegative(),
12
+ });
13
+ /** Public content positions within one native item; private/encrypted reasoning is excluded. */
14
+ export const BackendOutputContentSchema = z.discriminatedUnion("kind", [
15
+ z.object({ kind: z.enum(["text", "refusal"]), text: z.string(), contentIndex: z.int().nonnegative() }),
16
+ z.object({ kind: z.literal("reasoning_summary"), text: z.string(), summaryIndex: z.int().nonnegative() }),
17
+ ]);
18
+ export const BackendOutputDeltaSchema = z
19
+ .object({
20
+ kind: z.enum(["text", "refusal", "reasoning_summary"]),
21
+ source: BackendOutputSourceSchema.extend({
22
+ contentIndex: z.int().nonnegative().optional(),
23
+ summaryIndex: z.int().nonnegative().optional(),
24
+ }).refine((source) => (source.contentIndex === undefined) !== (source.summaryIndex === undefined), "Backend delta requires exactly one native content or summary position"),
25
+ delta: z.string(),
26
+ })
27
+ .refine((event) => event.kind === "reasoning_summary"
28
+ ? event.source.summaryIndex !== undefined
29
+ : event.source.contentIndex !== undefined, "Backend delta kind must match its native content or summary position");
30
+ export const BackendOutputSchema = z.object({
31
+ source: BackendOutputSourceSchema,
32
+ /** Authoritative final item snapshot, including an empty list or removed positions. */
33
+ content: z.array(BackendOutputContentSchema).refine((content) => {
34
+ const positions = content.map((part) => "contentIndex" in part ? `content:${part.contentIndex}` : `summary:${part.summaryIndex}`);
35
+ return new Set(positions).size === positions.length;
36
+ }, "Backend item snapshot cannot repeat a content position"),
37
+ /** Actual native item status, when supplied. The final event alone does not imply success. */
38
+ status: z.enum(["in_progress", "completed", "incomplete", "cancelled", "failed"]).optional(),
39
+ });
@@ -0,0 +1,3 @@
1
+ /** Shared wire encoding for live events and retained records. */
2
+ export declare function stringifyEventJson(value: unknown): string;
3
+ export declare function parseEventJson(serialized: string): unknown;
@@ -0,0 +1,15 @@
1
+ /** Shared wire encoding for live events and retained records. */
2
+ export function stringifyEventJson(value) {
3
+ return JSON.stringify(value, (_key, field) => field instanceof Uint8Array ? { $xoBytes: Array.from(field) } : field);
4
+ }
5
+ export function parseEventJson(serialized) {
6
+ return JSON.parse(serialized, (_key, candidate) => {
7
+ if (typeof candidate === "object" && candidate !== null && "$xoBytes" in candidate) {
8
+ const encoded = candidate.$xoBytes;
9
+ if (Array.isArray(encoded) &&
10
+ encoded.every((byte) => Number.isInteger(byte) && byte >= 0 && byte <= 255))
11
+ return Uint8Array.from(encoded);
12
+ }
13
+ return candidate;
14
+ });
15
+ }