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.
- package/README.md +92 -5
- package/dist/browser/worklets/capture-processor.js +34 -0
- package/dist/browser/worklets/playback-processor.js +188 -0
- package/dist/browser.d.ts +1 -0
- package/dist/browser.js +1 -0
- package/dist/internal/browser/browser-voice-client.d.ts +33 -0
- package/dist/internal/browser/browser-voice-client.js +316 -0
- package/dist/internal/browser/index.d.ts +2 -0
- package/dist/internal/browser/index.js +1 -0
- package/dist/internal/harness/conversation-context.d.ts +45 -0
- package/dist/internal/harness/conversation-context.js +79 -0
- package/dist/internal/harness/conversation-projection.d.ts +55 -0
- package/dist/internal/harness/conversation-projection.js +145 -0
- package/dist/internal/harness/event-stream.d.ts +23 -2
- package/dist/internal/harness/event-stream.js +148 -18
- package/dist/internal/harness/index.d.ts +7 -1
- package/dist/internal/harness/index.js +7 -1
- package/dist/internal/harness/message.d.ts +24 -11
- package/dist/internal/harness/message.js +271 -77
- package/dist/internal/harness/report-diff.d.ts +3 -0
- package/dist/internal/harness/report-diff.js +10 -0
- package/dist/internal/harness/report.d.ts +17 -14
- package/dist/internal/harness/report.js +41 -24
- package/dist/internal/harness/runtime-limits.d.ts +18 -0
- package/dist/internal/harness/runtime-limits.js +19 -0
- package/dist/internal/harness/session-persistence.d.ts +16 -0
- package/dist/internal/harness/session-persistence.js +106 -0
- package/dist/internal/harness/shadow.d.ts +5 -5
- package/dist/internal/harness/shadow.js +214 -52
- package/dist/internal/harness/socket-bridge.d.ts +45 -4
- package/dist/internal/harness/socket-bridge.js +215 -46
- package/dist/internal/harness/task-supervisor.d.ts +6 -2
- package/dist/internal/harness/task-supervisor.js +97 -15
- package/dist/internal/harness/tool-calls.d.ts +40 -0
- package/dist/internal/harness/tool-calls.js +132 -0
- package/dist/internal/harness/tool-policy.d.ts +2 -2
- package/dist/internal/harness/tool-runtime.d.ts +2 -2
- package/dist/internal/harness/tool-runtime.js +37 -40
- package/dist/internal/harness/tools.d.ts +21 -0
- package/dist/internal/harness/tools.js +7 -1
- package/dist/internal/harness/usage-tracker.d.ts +39 -0
- package/dist/internal/harness/usage-tracker.js +104 -0
- package/dist/internal/harness/voice-session.d.ts +39 -8
- package/dist/internal/harness/voice-session.js +353 -58
- package/dist/internal/harness/xo.d.ts +7 -12
- package/dist/internal/harness/xo.js +25 -13
- package/dist/internal/protocol/async-queue.d.ts +22 -1
- package/dist/internal/protocol/async-queue.js +86 -12
- package/dist/internal/protocol/audio.d.ts +16 -2
- package/dist/internal/protocol/audio.js +23 -5
- package/dist/internal/protocol/backend-output.d.ts +70 -0
- package/dist/internal/protocol/backend-output.js +39 -0
- package/dist/internal/protocol/event-json.d.ts +3 -0
- package/dist/internal/protocol/event-json.js +15 -0
- package/dist/internal/protocol/events.d.ts +342 -4
- package/dist/internal/protocol/events.js +53 -25
- package/dist/internal/protocol/index.d.ts +5 -0
- package/dist/internal/protocol/index.js +5 -0
- package/dist/internal/protocol/output-source.d.ts +18 -0
- package/dist/internal/protocol/output-source.js +15 -0
- package/dist/internal/protocol/paced-audio.d.ts +27 -0
- package/dist/internal/protocol/paced-audio.js +117 -0
- package/dist/internal/protocol/parts.d.ts +154 -0
- package/dist/internal/protocol/parts.js +32 -6
- package/dist/internal/protocol/provider.d.ts +308 -1
- package/dist/internal/protocol/provider.js +102 -3
- package/dist/internal/protocol/records.d.ts +773 -0
- package/dist/internal/protocol/records.js +46 -0
- package/dist/internal/protocol/tools.d.ts +40 -0
- package/dist/internal/protocol/tools.js +12 -0
- package/dist/internal/protocol/transcript.d.ts +19 -0
- package/dist/internal/protocol/transcript.js +53 -0
- package/dist/internal/provider/contract.d.ts +25 -2
- package/dist/internal/provider/event-queue.d.ts +17 -0
- package/dist/internal/provider/event-queue.js +78 -0
- package/dist/internal/provider/grok-voice.d.ts +12 -9
- package/dist/internal/provider/grok-voice.js +26 -15
- package/dist/internal/provider/index.d.ts +1 -0
- package/dist/internal/provider/index.js +1 -0
- package/dist/internal/provider/live-session.d.ts +29 -0
- package/dist/internal/provider/live-session.js +840 -0
- package/dist/internal/provider/node-socket.js +6 -0
- package/dist/internal/provider/openai-live.d.ts +62 -0
- package/dist/internal/provider/openai-live.js +160 -0
- package/dist/internal/provider/openai-realtime.d.ts +9 -5
- package/dist/internal/provider/openai-realtime.js +24 -10
- package/dist/internal/provider/realtime-session.d.ts +12 -1
- package/dist/internal/provider/realtime-session.js +373 -70
- package/dist/internal/provider/realtime-socket.d.ts +3 -1
- package/dist/internal/provider/tool-status-context.d.ts +10 -0
- package/dist/internal/provider/tool-status-context.js +30 -0
- package/dist/internal/provider/workers-socket.js +63 -15
- package/dist/internal/provider/workers.d.ts +1 -0
- package/dist/internal/provider/workers.js +1 -0
- package/dist/internal/provider-fake/replay-voice-provider.d.ts +9 -11
- package/dist/internal/provider-fake/replay-voice-provider.js +47 -23
- package/dist/internal/storage/event-store.d.ts +22 -4
- package/dist/internal/storage/jsonl-event-store.d.ts +4 -4
- package/dist/internal/storage/jsonl-event-store.js +21 -16
- package/dist/internal/storage/memory-event-store.d.ts +4 -3
- package/dist/internal/storage/memory-event-store.js +8 -2
- package/dist/internal/storage/memory.d.ts +1 -1
- package/dist/internal/testkit/events.d.ts +14 -0
- package/dist/internal/testkit/events.js +33 -0
- package/dist/internal/testkit/runtime.d.ts +3 -0
- package/dist/internal/testkit/runtime.js +3 -0
- package/dist/internal/testkit/trajectory.d.ts +23 -0
- package/dist/internal/testkit/trajectory.js +58 -0
- package/dist/internal/tools-openai/index.d.ts +2 -0
- package/dist/internal/tools-openai/index.js +79 -72
- package/dist/internal/tools-openai/responses.d.ts +5 -1
- package/dist/internal/tools-openai/responses.js +53 -5
- package/dist/testing.d.ts +1 -0
- package/dist/testing.js +1 -0
- package/package.json +7 -1
|
@@ -1,42 +1,172 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
11
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
78
|
+
next: () => {
|
|
79
|
+
if (cursor.pending)
|
|
80
|
+
return Promise.reject(new Error("Only one next() may be pending"));
|
|
25
81
|
try {
|
|
26
|
-
|
|
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
|
-
|
|
29
|
-
|
|
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
|
|
39
|
-
|
|
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 "./
|
|
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 "./
|
|
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
|
|
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
|
|
4
|
-
*
|
|
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
|
-
* -
|
|
16
|
-
*
|
|
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
|
|
23
|
-
*
|
|
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
|
|
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
|
+
};
|