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
@@ -1,42 +1,172 @@
1
- import { AsyncQueue } from "../protocol/index.js";
1
+ export class EventReplayUnavailableError extends Error {
2
+ afterSequence;
3
+ oldestAvailableSequence;
4
+ latestSequence;
5
+ constructor(afterSequence, oldestAvailableSequence, latestSequence) {
6
+ super(`Event replay after sequence ${afterSequence} is unavailable; retained history starts at ${oldestAvailableSequence ?? "no event"} (latest sequence ${latestSequence})`);
7
+ this.afterSequence = afterSequence;
8
+ this.oldestAvailableSequence = oldestAvailableSequence;
9
+ this.latestSequence = latestSequence;
10
+ this.name = "EventReplayUnavailableError";
11
+ }
12
+ }
13
+ const done = () => ({ done: true, value: undefined });
14
+ /** One shared replay buffer; iterators retain cursors, never private event queues. */
2
15
  export class ReplayEventStream {
3
16
  #history = [];
4
17
  #subscribers = new Set();
18
+ #maxEvents;
19
+ #maxAudioBytes;
20
+ #head = 0;
21
+ #offset = 0;
22
+ #audioBytes = 0;
23
+ #lastEvictedSequence = 0;
24
+ #latestSequence = 0;
5
25
  #closed = false;
26
+ #failure;
27
+ constructor(options = {}) {
28
+ for (const [name, value] of Object.entries(options)) {
29
+ if (value !== undefined && (!Number.isSafeInteger(value) || value < 0))
30
+ throw new Error(`${name} must be a nonnegative safe integer`);
31
+ }
32
+ this.#maxEvents = options.maxEvents ?? Number.POSITIVE_INFINITY;
33
+ this.#maxAudioBytes = options.maxAudioBytes ?? Number.POSITIVE_INFINITY;
34
+ }
6
35
  publish(event) {
7
36
  if (this.#closed)
8
37
  throw new Error("Cannot publish after the event stream is closed");
38
+ if (event.sequence <= this.#latestSequence)
39
+ throw new Error("Event stream sequences must increase");
40
+ this.#latestSequence = event.sequence;
9
41
  this.#history.push(event);
10
- for (const subscriber of this.#subscribers)
11
- subscriber.push(event);
42
+ this.#audioBytes += audioBytes(event);
43
+ // Already waiting readers can receive an event even when replay retention is zero.
44
+ for (const cursor of this.#subscribers)
45
+ this.#flush(cursor);
46
+ while (this.#history.length - this.#head > this.#maxEvents || this.#audioBytes > this.#maxAudioBytes) {
47
+ const oldest = this.#history[this.#head];
48
+ if (!oldest)
49
+ break;
50
+ this.#audioBytes -= audioBytes(oldest);
51
+ this.#lastEvictedSequence = oldest.sequence;
52
+ this.#history[this.#head++] = undefined;
53
+ }
54
+ if (this.#head >= 1024 && this.#head * 2 >= this.#history.length) {
55
+ this.#history = this.#history.slice(this.#head);
56
+ this.#offset += this.#head;
57
+ this.#head = 0;
58
+ }
12
59
  }
60
+ /** At most one next() may be pending per iterator. Evicted cursors fail rather than skip events. */
13
61
  subscribe(afterSequence = 0) {
14
- const queue = new AsyncQueue();
15
- const backlog = this.#history.filter((event) => event.sequence > afterSequence);
16
- for (const event of backlog)
17
- queue.push(event);
18
- if (this.#closed)
19
- queue.close();
20
- else
21
- this.#subscribers.add(queue);
22
- const subscribers = this.#subscribers;
62
+ if (!Number.isSafeInteger(afterSequence) || afterSequence < 0)
63
+ throw new Error("afterSequence must be a nonnegative safe integer");
64
+ let low = this.#head;
65
+ let high = this.#history.length;
66
+ while (low < high) {
67
+ const middle = Math.floor((low + high) / 2);
68
+ const event = this.#history[middle];
69
+ if (event && event.sequence <= afterSequence)
70
+ low = middle + 1;
71
+ else
72
+ high = middle;
73
+ }
74
+ const cursor = { afterSequence, position: this.#offset + low, done: false };
75
+ if (!this.#closed)
76
+ this.#subscribers.add(cursor);
23
77
  return {
24
- async *[Symbol.asyncIterator]() {
78
+ next: () => {
79
+ if (cursor.pending)
80
+ return Promise.reject(new Error("Only one next() may be pending"));
25
81
  try {
26
- yield* queue;
82
+ const result = this.#take(cursor);
83
+ if (result)
84
+ return Promise.resolve(result);
85
+ cursor.pending = Promise.withResolvers();
86
+ return cursor.pending.promise;
27
87
  }
28
- finally {
29
- subscribers.delete(queue);
88
+ catch (error) {
89
+ this.#release(cursor);
90
+ return Promise.reject(error);
30
91
  }
31
92
  },
93
+ return: () => {
94
+ this.#release(cursor);
95
+ return Promise.resolve(done());
96
+ },
97
+ [Symbol.asyncIterator]() {
98
+ return this;
99
+ },
32
100
  };
33
101
  }
34
102
  close() {
35
103
  if (this.#closed)
36
104
  return;
37
105
  this.#closed = true;
38
- for (const subscriber of this.#subscribers)
39
- subscriber.close();
106
+ for (const cursor of this.#subscribers)
107
+ this.#flush(cursor);
40
108
  this.#subscribers.clear();
41
109
  }
110
+ /** Drain retained history, then reject reads with the failure instead of reporting a clean end. */
111
+ fail(error) {
112
+ if (this.#closed)
113
+ return;
114
+ this.#failure = { error };
115
+ this.close();
116
+ }
117
+ stats() {
118
+ return {
119
+ retainedEvents: this.#history.length - this.#head,
120
+ retainedAudioBytes: this.#audioBytes,
121
+ oldestSequence: this.#history[this.#head]?.sequence,
122
+ latestSequence: this.#latestSequence,
123
+ subscribers: this.#subscribers.size,
124
+ };
125
+ }
126
+ #take(cursor) {
127
+ if (cursor.done)
128
+ return done();
129
+ if (cursor.afterSequence < this.#lastEvictedSequence)
130
+ throw new EventReplayUnavailableError(cursor.afterSequence, this.#history[this.#head]?.sequence, this.#latestSequence);
131
+ cursor.position = Math.max(cursor.position, this.#offset + this.#head);
132
+ while (cursor.position < this.#offset + this.#history.length) {
133
+ const event = this.#history[cursor.position++ - this.#offset];
134
+ if (event && event.sequence > cursor.afterSequence) {
135
+ cursor.afterSequence = event.sequence;
136
+ return { done: false, value: event };
137
+ }
138
+ }
139
+ if (!this.#closed)
140
+ return undefined;
141
+ if (this.#failure)
142
+ throw this.#failure.error;
143
+ this.#release(cursor);
144
+ return done();
145
+ }
146
+ #flush(cursor) {
147
+ if (!cursor.pending)
148
+ return;
149
+ const pending = cursor.pending;
150
+ try {
151
+ const result = this.#take(cursor);
152
+ if (!result)
153
+ return;
154
+ cursor.pending = undefined;
155
+ pending.resolve(result);
156
+ }
157
+ catch (error) {
158
+ cursor.pending = undefined;
159
+ this.#release(cursor);
160
+ pending.reject(error);
161
+ }
162
+ }
163
+ #release(cursor) {
164
+ cursor.done = true;
165
+ this.#subscribers.delete(cursor);
166
+ cursor.pending?.resolve(done());
167
+ cursor.pending = undefined;
168
+ }
169
+ }
170
+ function audioBytes(event) {
171
+ return event.type === "audio.input" || event.type === "audio.output" ? event.chunk.data.byteLength : 0;
42
172
  }
@@ -1,10 +1,16 @@
1
- export * from "./message.js";
1
+ export * from "./conversation-context.js";
2
+ export * from "./conversation-projection.js";
3
+ export { EventReplayUnavailableError } from "./event-stream.js";
4
+ export { projectMessages } from "./message.js";
2
5
  export * from "./report.js";
3
6
  export * from "./report-diff.js";
7
+ export { SessionPersistenceError, type SessionRuntimeLimits } from "./runtime-limits.js";
4
8
  export * from "./shadow.js";
5
9
  export * from "./socket-bridge.js";
10
+ export * from "./tool-calls.js";
6
11
  export * from "./tool-delivery.js";
7
12
  export * from "./tool-policy.js";
8
13
  export * from "./tools.js";
14
+ export * from "./usage-tracker.js";
9
15
  export * from "./voice-session.js";
10
16
  export * from "./xo.js";
@@ -1,10 +1,16 @@
1
- export * from "./message.js";
1
+ export * from "./conversation-context.js";
2
+ export * from "./conversation-projection.js";
3
+ export { EventReplayUnavailableError } from "./event-stream.js";
4
+ export { projectMessages } from "./message.js";
2
5
  export * from "./report.js";
3
6
  export * from "./report-diff.js";
7
+ export { SessionPersistenceError } from "./runtime-limits.js";
4
8
  export * from "./shadow.js";
5
9
  export * from "./socket-bridge.js";
10
+ export * from "./tool-calls.js";
6
11
  export * from "./tool-delivery.js";
7
12
  export * from "./tool-policy.js";
8
13
  export * from "./tools.js";
14
+ export * from "./usage-tracker.js";
9
15
  export * from "./voice-session.js";
10
16
  export * from "./xo.js";
@@ -1,10 +1,9 @@
1
- import type { HarnessEvent, Message } from "../protocol/index.js";
1
+ import { type HarnessRecord, type Message } from "../protocol/index.js";
2
+ /** Text evidence, independent of display state, session sealing, and device playback. */
3
+ export type ConversationTextCompletion = "input" | "provider-final" | "unconfirmed";
2
4
  /**
3
- * Projects the flat, sequence-ordered event log into the v2 `Message[]` part model —
4
- * the shape every agent harness renders and persists (opencode / Vercel AI SDK v5 /
5
- * agent-server / echo). Pure AND deterministic over the recorded events: ids derive
6
- * from the opening event, so the same log always projects to the same identities —
7
- * re-folds diff cleanly, upserts stay idempotent, and renderers can key on part ids.
5
+ * Projects the sequence-ordered source log into display messages. Identities derive
6
+ * from opening events, so replay produces the same message and part IDs.
8
7
  *
9
8
  * Folding rules:
10
9
  * - `message.input` → a user message; its recorded `parts` project with full fidelity
@@ -12,14 +11,28 @@ import type { HarnessEvent, Message } from "../protocol/index.js";
12
11
  * - `transcript.delta` from either speaker → a `streaming` text part accumulating deltas,
13
12
  * finalized in place by the matching `transcript` (streamId-correlated, so a
14
13
  * barge-in interrupted stream still finalizes into its original part).
15
- * - `audio.output` → an `audio` part on the current assistant message (deduped per
16
- * streamId), linked to its transcript text part by that streamId.
14
+ * - Response audio and its transcript share a stream's message owner, including
15
+ * when the first caption arrives after another speaker's input.
16
+ * - Confirmed interruption separates the next new assistant speech stream; late
17
+ * updates still belong to their original stream. Timed fragments keep their
18
+ * independent, revisable display grouping. Interrupted response streams retain
19
+ * their generated text with playback evidence; a final caption does not erase it.
20
+ * - `backend.output.*` → backend item-owned text/refusal/summary parts, separate
21
+ * from speech. Final items replace their content; failed responses settle only
22
+ * unfinished parts. Backend completion is never evidence of spoken delivery.
17
23
  * - `tool.requested` → ONE `tool` part in `running` state; `tool.completed`/`tool.failed`
18
24
  * settle it with the canonical outcome, and a task terminal upgrades an
19
25
  * `accepted_task` outcome to the task's real result. This is why a tool result
20
26
  * (e.g. a crisis card) replays from history: the outcome lives on the part.
21
27
  *
22
- * A tool part belongs to the assistant message open when it was requested; a new
23
- * assistant transcript or user turn opens the next message.
28
+ * A tool part retains an existing native item owner when one is known. Otherwise,
29
+ * continuous-output tools own a separate assistant message, independent of revisable
30
+ * captions; response-based tools belong to the assistant message open at invocation.
31
+ * Display messages are not provider turns or evidence of device playback.
24
32
  */
25
- export declare function projectMessages(events: readonly HarnessEvent[]): Message[];
33
+ export declare function projectMessages(events: readonly HarnessRecord[]): Message[];
34
+ /** Internal shared fold: persistence and batch rendering use the same text evidence. */
35
+ export declare function foldMessages(events: readonly HarnessRecord[]): {
36
+ messages: Message[];
37
+ textCompletion: Record<string, ConversationTextCompletion>;
38
+ };