nanocodex 0.6.1 → 0.6.5
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 +180 -16
- package/browser/Agent.d.mts +1 -1
- package/browser/ElevenLabsPlayback.mjs +119 -0
- package/browser/InlineAgent.mjs +24 -17
- package/browser/Transport.d.mts +5 -0
- package/browser/Transport.mjs +4 -2
- package/browser/Voice.d.mts +6 -0
- package/browser/Voice.mjs +39 -8
- package/browser/VoiceSession.mjs +90 -24
- package/browser/WorkerAgent.mjs +2 -1
- package/browser/host.d.mts +27 -4
- package/browser/host.mjs +175 -18
- package/cloud/Decorator.d.mts +9 -0
- package/cloud/Decorator.mjs +9 -0
- package/cloud/Transport.mjs +6 -0
- package/cloud/actions/connection.mjs +3 -0
- package/cloud/actions/connector.d.mts +18 -0
- package/cloud/actions/connector.mjs +22 -0
- package/cloud/actions/index.d.mts +2 -0
- package/cloud/actions/index.mjs +2 -0
- package/cloud/internal.mjs +5 -0
- package/cloud/types.d.mts +2 -0
- package/cloudflare/Agent.d.mts +10 -9
- package/cloudflare/Agent.mjs +261 -91
- package/cloudflare/agent-settings.d.mts +15 -0
- package/cloudflare/agent-settings.mjs +135 -0
- package/cloudflare/chat-reasoning.mjs +20 -0
- package/cloudflare/durable-placement.d.mts +12 -0
- package/cloudflare/durable-placement.mjs +82 -0
- package/cloudflare/egress.d.mts +34 -1
- package/cloudflare/egress.mjs +14 -0
- package/cloudflare/gateway-binding-responses.mjs +84 -0
- package/cloudflare/gateway-responses.d.mts +48 -0
- package/cloudflare/gateway-responses.mjs +191 -0
- package/cloudflare/index.d.mts +6 -0
- package/cloudflare/index.mjs +3 -0
- package/cloudflare/managed-access.d.mts +23 -0
- package/cloudflare/managed-access.mjs +120 -0
- package/cloudflare/managed-auth.d.mts +15 -0
- package/cloudflare/managed-auth.mjs +113 -0
- package/cloudflare/managed-live.d.mts +7 -0
- package/cloudflare/managed-live.mjs +56 -0
- package/cloudflare/prepared-connection.mjs +49 -0
- package/cloudflare/provider-stream.mjs +353 -0
- package/cloudflare/rpc.d.mts +9 -0
- package/cloudflare/rpc.mjs +14 -0
- package/cloudflare/tool-alias-diagnostic.mjs +21 -0
- package/cloudflare/workers-ai-responses.d.mts +28 -0
- package/cloudflare/workers-ai-responses.mjs +398 -0
- package/host/Agent.d.mts +1 -1
- package/index.d.mts +2 -0
- package/internal.mjs +75 -14
- package/managed/Access.d.mts +5 -0
- package/managed/Access.mjs +66 -0
- package/managed/Agent.d.mts +29 -67
- package/managed/Agent.mjs +74 -183
- package/managed/README.md +9 -38
- package/managed/Voice.mjs +3 -1
- package/managed/index.d.mts +1 -3
- package/managed/index.mjs +1 -0
- package/node/Agent.d.mts +1 -1
- package/node/Agent.mjs +9 -3
- package/node/host.mjs +17 -0
- package/package.json +39 -2
- package/pkg-node/nanocodex.d.ts +8 -16
- package/pkg-node/nanocodex.js +107 -89
- package/pkg-node/nanocodex_bg.wasm.d.ts +4 -5
- package/pkg-web/.nanocodex-bindgen-stamp +7 -1
- package/pkg-web/nanocodex-build.json +1 -1
- package/pkg-web/nanocodex.d.ts +12 -21
- package/pkg-web/nanocodex.js +107 -88
- package/pkg-web/nanocodex_bg.js +107 -88
- package/pkg-web/nanocodex_bg.wasm +0 -0
- package/pkg-web/nanocodex_bg.wasm.d.ts +4 -5
- package/pkg-web/nanocodex_worker.js +1 -1
- package/runtime/before-compaction.mjs +49 -0
- package/runtime/code-evaluator.worker.mjs +30 -31
- package/runtime/managed-transport.mjs +11 -2
- package/runtime/mcp-runtime.mjs +15 -1
- package/runtime/quickjs-evaluator.mjs +69 -21
- package/runtime/response-controls.mjs +94 -25
- package/runtime/responses-http.mjs +69 -0
- package/runtime/steer-receipt.mjs +13 -0
- package/runtime/subagent-routing.d.mts +38 -0
- package/runtime/subagent-routing.mjs +80 -0
- package/runtime/subagents.d.mts +1 -1
- package/runtime/tempo-provider.d.mts +1 -1
- package/runtime/tempo-provider.mjs +1 -1
- package/tools/browser/accountInfo.mjs +53 -8
- package/tools/browser/browserShell.mjs +2 -2
- package/tools/browser/index.mjs +3 -3
- package/tools/environment.d.mts +40 -0
- package/tools/environment.mjs +73 -0
- package/tools/index.d.mts +2 -0
- package/types.d.mts +28 -1
package/browser/Voice.mjs
CHANGED
|
@@ -91,6 +91,11 @@ export function create(agent, options = {}) {
|
|
|
91
91
|
if (destroyed) throw new Error("voice resource is destroyed");
|
|
92
92
|
const selectedVoice = parameters.voice ?? options.voice ?? defaultVoice;
|
|
93
93
|
if (!voices.includes(selectedVoice)) throw new TypeError(`unsupported ChatGPT voice: ${selectedVoice}`);
|
|
94
|
+
const settings = voiceSettings({ ...options, ...parameters, voice: selectedVoice });
|
|
95
|
+
validateOutputSettings(settings);
|
|
96
|
+
if (agent.type === "connect" && settings.outputProvider === "elevenlabs" && !options.synthesize) {
|
|
97
|
+
throw new TypeError("Connect ElevenLabs output requires an explicit authorized synthesis transport");
|
|
98
|
+
}
|
|
94
99
|
if (session) return startPromise;
|
|
95
100
|
if (stopPromise) await stopPromise.catch(() => {});
|
|
96
101
|
if (destroyed) throw new Error("voice resource is destroyed");
|
|
@@ -114,17 +119,32 @@ export function create(agent, options = {}) {
|
|
|
114
119
|
: createBrowserVoice(agent, selectedVoice));
|
|
115
120
|
const transport = managedTransport;
|
|
116
121
|
let transcriptSequence = 0;
|
|
122
|
+
let publishedReady = false;
|
|
123
|
+
const publishReady = () => {
|
|
124
|
+
if (publishedReady || destroyed || session !== next || generation !== current) return;
|
|
125
|
+
publishedReady = true;
|
|
126
|
+
publish({ ...snapshot, error: undefined, status: "active", statusText: `Voice active (${selectedVoice})` });
|
|
127
|
+
emit(Object.freeze({ type: "started", voice: selectedVoice }));
|
|
128
|
+
};
|
|
117
129
|
const next = new BrowserVoiceSession({
|
|
118
130
|
core,
|
|
119
131
|
sessionId,
|
|
120
132
|
voice: selectedVoice,
|
|
121
|
-
|
|
133
|
+
synthesize: options.synthesize ?? (transport?.synthesize
|
|
134
|
+
? (text, signal) => transport.synthesize(text, signal, settings.elevenLabsVoiceId)
|
|
135
|
+
: (text, signal) => fetch("/api/voice/elevenlabs/speech", {
|
|
136
|
+
method: "POST", credentials: "same-origin", signal,
|
|
137
|
+
headers: { "Content-Type": "application/json" },
|
|
138
|
+
body: JSON.stringify({ voice_id: settings.elevenLabsVoiceId, text, output_format: "pcm_24000" }),
|
|
139
|
+
})),
|
|
140
|
+
settings,
|
|
122
141
|
...(transport?.call === undefined ? {} : { call: transport.call }),
|
|
123
142
|
...(transport?.sidebandUrl === undefined ? {} : { sidebandUrl: transport.sidebandUrl }),
|
|
124
143
|
...(options.callUrl === undefined ? {} : { callUrl: options.callUrl }),
|
|
125
|
-
...(options.sidebandUrl === undefined ? {} : { sidebandUrl: options.sidebandUrl }),
|
|
144
|
+
...(options.sidebandUrl === undefined ? {} : { sidebandUrl: options.sidebandUrl, dataChannelControl: false }),
|
|
126
145
|
...(options.captureMicrophone === undefined ? {} : { captureMicrophone: options.captureMicrophone }),
|
|
127
146
|
...(options.beforeAgentTurn === undefined ? {} : { beforeAgentTurn: options.beforeAgentTurn }),
|
|
147
|
+
onReady: publishReady,
|
|
128
148
|
onStatus(text) {
|
|
129
149
|
if (session === next && generation === current && snapshot.status !== "idle" && snapshot.status !== "error") {
|
|
130
150
|
publish({ ...snapshot, statusText: text });
|
|
@@ -175,11 +195,9 @@ export function create(agent, options = {}) {
|
|
|
175
195
|
session = next;
|
|
176
196
|
next.setMuted(snapshot.muted);
|
|
177
197
|
observeAgentEvents(next);
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
emit(Object.freeze({ type: "started", voice: selectedVoice }));
|
|
182
|
-
}).catch(async (cause) => {
|
|
198
|
+
// Media readiness updates the UI independently; start still waits for task
|
|
199
|
+
// admission, and a later rejection closes media through the same error path.
|
|
200
|
+
startPromise = next.start().then(publishReady).catch(async (cause) => {
|
|
183
201
|
if (session === next) session = undefined;
|
|
184
202
|
if (activeResources.get(agent) === resource) activeResources.delete(agent);
|
|
185
203
|
cleanupWatcher();
|
|
@@ -293,6 +311,9 @@ function validateOptions(options) {
|
|
|
293
311
|
if (!options || typeof options !== "object" || Array.isArray(options)) {
|
|
294
312
|
throw new TypeError("Voice.create options must be an object");
|
|
295
313
|
}
|
|
314
|
+
if (options.synthesize !== undefined && typeof options.synthesize !== "function") {
|
|
315
|
+
throw new TypeError("voice synthesize must be a function");
|
|
316
|
+
}
|
|
296
317
|
if (options.captureMicrophone !== undefined && typeof options.captureMicrophone !== "function") {
|
|
297
318
|
throw new TypeError("voice captureMicrophone must be a function");
|
|
298
319
|
}
|
|
@@ -311,6 +332,16 @@ function browserLocationOrigin() {
|
|
|
311
332
|
}
|
|
312
333
|
|
|
313
334
|
function voiceSettings(options) {
|
|
314
|
-
return Object.fromEntries(["voice", "instructions", "pace", "updates", "handoffMode", "acknowledgements"]
|
|
335
|
+
return Object.fromEntries(["voice", "instructions", "pace", "updates", "handoffMode", "acknowledgements", "outputProvider", "elevenLabsVoiceId"]
|
|
315
336
|
.filter((key) => options[key] !== undefined).map((key) => [key, options[key]]));
|
|
316
337
|
}
|
|
338
|
+
|
|
339
|
+
function validateOutputSettings(settings) {
|
|
340
|
+
if (settings.outputProvider !== undefined && !["openai", "elevenlabs"].includes(settings.outputProvider)) {
|
|
341
|
+
throw new TypeError("unsupported voice output provider");
|
|
342
|
+
}
|
|
343
|
+
if (settings.outputProvider === "elevenlabs"
|
|
344
|
+
&& (typeof settings.elevenLabsVoiceId !== "string" || !/^[A-Za-z0-9_-]{1,128}$/.test(settings.elevenLabsVoiceId))) {
|
|
345
|
+
throw new TypeError("ElevenLabs output requires a valid voice ID");
|
|
346
|
+
}
|
|
347
|
+
}
|
package/browser/VoiceSession.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { ElevenLabsPlayback } from "./ElevenLabsPlayback.mjs";
|
|
2
|
+
|
|
1
3
|
export const MICROPHONE_CAPTURE_TIMEOUT_MS = 15_000;
|
|
2
4
|
export const ICE_GATHERING_TIMEOUT_MS = 15_000;
|
|
3
5
|
export const REALTIME_CALL_TIMEOUT_MS = 15_000;
|
|
@@ -76,6 +78,7 @@ export class BrowserVoiceSession {
|
|
|
76
78
|
#admission;
|
|
77
79
|
#peer;
|
|
78
80
|
#channel;
|
|
81
|
+
#directControl = false;
|
|
79
82
|
#sideband;
|
|
80
83
|
#sidebandUrl;
|
|
81
84
|
#sidebandCallId;
|
|
@@ -84,6 +87,7 @@ export class BrowserVoiceSession {
|
|
|
84
87
|
#microphone;
|
|
85
88
|
#speaker;
|
|
86
89
|
#playbackEnabled = false;
|
|
90
|
+
#elevenLabs;
|
|
87
91
|
#muted = false;
|
|
88
92
|
#inputGeneration = 0;
|
|
89
93
|
#meterTimer;
|
|
@@ -101,10 +105,17 @@ export class BrowserVoiceSession {
|
|
|
101
105
|
|
|
102
106
|
constructor(options) {
|
|
103
107
|
this.#options = options;
|
|
108
|
+
if (options.settings?.outputProvider === "elevenlabs") {
|
|
109
|
+
if (typeof options.synthesize !== "function") throw new TypeError("ElevenLabs output requires an authenticated synthesis transport");
|
|
110
|
+
this.#elevenLabs = new ElevenLabsPlayback(options.synthesize, (error) => {
|
|
111
|
+
if (!this.#closed) options.onTerminated(error.message);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
104
114
|
this.#backendReady = new Promise((resolve) => { this.#resolveBackendReady = resolve; });
|
|
105
115
|
}
|
|
106
116
|
|
|
107
117
|
start() {
|
|
118
|
+
if (!this.#starting) void this.#elevenLabs?.prime()?.catch((error) => this.#options.onTerminated(error.message));
|
|
108
119
|
this.#starting ??= this.#start();
|
|
109
120
|
return this.#starting;
|
|
110
121
|
}
|
|
@@ -129,15 +140,18 @@ export class BrowserVoiceSession {
|
|
|
129
140
|
const coreReady = Promise.resolve(this.#options.core).then(async (core) => {
|
|
130
141
|
if (this.#closed || this.#closing.signal.aborted) { core.free(); return; }
|
|
131
142
|
this.#core = core;
|
|
143
|
+
this.#directControl = core.dataChannelControl === true && this.#options.dataChannelControl !== false;
|
|
132
144
|
if (this.#options.settings) await core.configure(JSON.stringify(this.#options.settings));
|
|
133
|
-
await this.#options.beforeAgentTurn?.();
|
|
145
|
+
if (!this.#directControl) await this.#options.beforeAgentTurn?.();
|
|
134
146
|
if (this.#closed || this.#closing.signal.aborted) return;
|
|
135
147
|
return core;
|
|
136
148
|
});
|
|
137
|
-
//
|
|
138
|
-
//
|
|
149
|
+
// Media and live conversation do not depend on durable task admission.
|
|
150
|
+
// Only delegated work waits for admission on the direct control path.
|
|
139
151
|
const coreStartup = coreReady.then(async (core) => {
|
|
140
|
-
await
|
|
152
|
+
if (this.#directControl) await this.#options.beforeAgentTurn?.();
|
|
153
|
+
const effects = await core?.start();
|
|
154
|
+
if (!this.#closed && !this.#closing.signal.aborted) await this.#apply(effects);
|
|
141
155
|
return core;
|
|
142
156
|
});
|
|
143
157
|
this.#admission = coreStartup;
|
|
@@ -203,7 +217,7 @@ export class BrowserVoiceSession {
|
|
|
203
217
|
const completed = JSON.parse(await core.completeCall(callResponse.body, callResponse.location));
|
|
204
218
|
if (this.#closed || peer.signalingState === "closed") return;
|
|
205
219
|
this.#sidebandCallId = completed.call_id;
|
|
206
|
-
this.#sidebandUrl = this.#options.sidebandUrl
|
|
220
|
+
this.#sidebandUrl = this.#directControl || this.#options.sidebandUrl
|
|
207
221
|
? undefined
|
|
208
222
|
: String(await core.sidebandUrl(completed.call_id));
|
|
209
223
|
if (this.#closed) return;
|
|
@@ -215,7 +229,7 @@ export class BrowserVoiceSession {
|
|
|
215
229
|
}, { signal: this.#closing.signal, timeoutMs: PEER_CONNECTION_TIMEOUT_MS,
|
|
216
230
|
timeoutError: new VoiceError("peer_connection_timeout", "Voice media did not connect in time."),
|
|
217
231
|
onTimeout: () => { peer.close(); } }),
|
|
218
|
-
this.#openSideband().then(() => withStartupDeadline(() => this.#backendReady, { signal: this.#closing.signal,
|
|
232
|
+
(this.#directControl ? this.#openDataChannel() : this.#openSideband()).then(() => withStartupDeadline(() => this.#backendReady, { signal: this.#closing.signal,
|
|
219
233
|
timeoutMs: SIDEBAND_OPEN_TIMEOUT_MS,
|
|
220
234
|
timeoutError: new VoiceError("session_ready_timeout", "The Realtime session did not become ready in time.") })),
|
|
221
235
|
]);
|
|
@@ -227,6 +241,7 @@ export class BrowserVoiceSession {
|
|
|
227
241
|
if (this.#closed) return;
|
|
228
242
|
this.#sampleLevels();
|
|
229
243
|
this.#status(`Voice active (${this.#options.voice})`);
|
|
244
|
+
this.#options.onReady?.();
|
|
230
245
|
}
|
|
231
246
|
|
|
232
247
|
async #prepareMedia(capture) {
|
|
@@ -249,6 +264,17 @@ export class BrowserVoiceSession {
|
|
|
249
264
|
this.#peer = peer;
|
|
250
265
|
for (const track of microphone.getAudioTracks()) peer.addTrack(track, microphone);
|
|
251
266
|
this.#channel = peer.createDataChannel("oai-events");
|
|
267
|
+
const channel = this.#channel;
|
|
268
|
+
channel.addEventListener("message", (event) => {
|
|
269
|
+
if (this.#directControl && !this.#closed && this.#channel === channel) {
|
|
270
|
+
this.#receiveControl(event.data, () => this.#channel === channel);
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
channel.addEventListener("close", () => {
|
|
274
|
+
if (this.#directControl && !this.#closed && !this.#closing.signal.aborted && this.#channel === channel) {
|
|
275
|
+
this.#options.onTerminated("Voice control connection closed — tap Voice to reconnect");
|
|
276
|
+
}
|
|
277
|
+
});
|
|
252
278
|
peer.addEventListener("track", (event) => {
|
|
253
279
|
if (this.#closed || this.#closing.signal.aborted || this.#peer !== peer) {
|
|
254
280
|
event.track.stop();
|
|
@@ -256,7 +282,7 @@ export class BrowserVoiceSession {
|
|
|
256
282
|
}
|
|
257
283
|
const stream = event.streams[0] ?? new MediaStream([event.track]);
|
|
258
284
|
this.#speaker ??= new SpeakerPlayback(new Audio(), this.#options.onStatus);
|
|
259
|
-
this.#speaker.setEnabled(this.#playbackEnabled);
|
|
285
|
+
this.#speaker.setEnabled(this.#playbackEnabled && !this.#elevenLabs);
|
|
260
286
|
this.#speaker.attach(stream);
|
|
261
287
|
});
|
|
262
288
|
peer.addEventListener("connectionstatechange", () => {
|
|
@@ -286,6 +312,7 @@ export class BrowserVoiceSession {
|
|
|
286
312
|
|
|
287
313
|
noteTypedInput() {
|
|
288
314
|
this.#playbackEnabled = false;
|
|
315
|
+
this.#elevenLabs?.interrupt();
|
|
289
316
|
this.#speaker?.setEnabled(false);
|
|
290
317
|
return this.#applyLive(async () => {
|
|
291
318
|
const core = await this.#options.core;
|
|
@@ -402,15 +429,20 @@ export class BrowserVoiceSession {
|
|
|
402
429
|
for (const text of effects.undelivered_answers ?? []) this.#options.onUndeliveredAnswer?.(text);
|
|
403
430
|
if (effects.input_generation !== undefined) {
|
|
404
431
|
if (effects.input_generation < this.#inputGeneration) return;
|
|
432
|
+
if (effects.input_generation > this.#inputGeneration) this.#elevenLabs?.interrupt();
|
|
405
433
|
this.#inputGeneration = effects.input_generation;
|
|
406
434
|
}
|
|
407
435
|
if (effects.playback_enabled === false) {
|
|
408
436
|
this.#playbackEnabled = false;
|
|
437
|
+
this.#elevenLabs?.interrupt();
|
|
409
438
|
this.#speaker?.setEnabled(false);
|
|
410
439
|
}
|
|
411
440
|
let sent = 0;
|
|
412
441
|
for (const frame of effects.frames ?? []) {
|
|
413
|
-
if (this.#
|
|
442
|
+
if (this.#directControl && this.#channel?.readyState === "open") {
|
|
443
|
+
this.#channel.send(frame);
|
|
444
|
+
sent += 1;
|
|
445
|
+
} else if (!this.#directControl && this.#sideband?.readyState === WebSocket.OPEN) {
|
|
414
446
|
this.#sideband.send(frame);
|
|
415
447
|
sent += 1;
|
|
416
448
|
}
|
|
@@ -418,9 +450,10 @@ export class BrowserVoiceSession {
|
|
|
418
450
|
if (effects.acknowledge_frames && sent > 0) await this.#core?.framesSent(sent);
|
|
419
451
|
if (!this.#closed && effects.playback_enabled === true && sent === (effects.frames?.length ?? 0)) {
|
|
420
452
|
this.#playbackEnabled = true;
|
|
421
|
-
this.#speaker?.setEnabled(
|
|
453
|
+
this.#speaker?.setEnabled(!this.#elevenLabs);
|
|
422
454
|
}
|
|
423
455
|
for (const entry of effects.transcripts ?? []) {
|
|
456
|
+
if (this.#playbackEnabled && !this.#closed) this.#elevenLabs?.transcript(entry);
|
|
424
457
|
this.#options.onTranscript(entry.speaker, entry.text, entry);
|
|
425
458
|
}
|
|
426
459
|
if (effects.status) this.#status(effects.status);
|
|
@@ -457,20 +490,7 @@ export class BrowserVoiceSession {
|
|
|
457
490
|
let opened = false;
|
|
458
491
|
sideband.addEventListener("message", (event) => {
|
|
459
492
|
if (!this.#closed && generation === this.#sidebandGeneration) {
|
|
460
|
-
this.#
|
|
461
|
-
await this.#admission;
|
|
462
|
-
if (this.#closed || generation !== this.#sidebandGeneration) return;
|
|
463
|
-
if (await this.#core.requiresAgentAdmission(event.data)) {
|
|
464
|
-
// Only delegations wait for durable admission. Speech deltas and
|
|
465
|
-
// agent output must continue while that independent request waits.
|
|
466
|
-
void this.#enqueue(async () => {
|
|
467
|
-
await this.#options.beforeAgentTurn?.();
|
|
468
|
-
return this.#core.realtimeMessage(event.data);
|
|
469
|
-
}, true).catch(() => {});
|
|
470
|
-
return;
|
|
471
|
-
}
|
|
472
|
-
return this.#core.realtimeMessage(event.data);
|
|
473
|
-
});
|
|
493
|
+
this.#receiveControl(event.data, () => generation === this.#sidebandGeneration);
|
|
474
494
|
}
|
|
475
495
|
});
|
|
476
496
|
sideband.addEventListener("close", () => {
|
|
@@ -492,6 +512,46 @@ export class BrowserVoiceSession {
|
|
|
492
512
|
}
|
|
493
513
|
}
|
|
494
514
|
|
|
515
|
+
#receiveControl(payload, isCurrent) {
|
|
516
|
+
this.#applyLive(async () => {
|
|
517
|
+
if (!this.#directControl) await this.#admission;
|
|
518
|
+
if (this.#closed || !isCurrent()) return;
|
|
519
|
+
if (await this.#core.requiresAgentAdmission(payload)) {
|
|
520
|
+
// Retain accepted requests on close, but keep captions and interruptions
|
|
521
|
+
// independent of the durable route and its ordered task queue.
|
|
522
|
+
void this.#enqueue(async () => {
|
|
523
|
+
await this.#admission;
|
|
524
|
+
await this.#options.beforeAgentTurn?.();
|
|
525
|
+
return this.#core.realtimeMessage(payload);
|
|
526
|
+
}, true).catch(() => {});
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
return this.#core.realtimeMessage(payload);
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
async #openDataChannel() {
|
|
534
|
+
const channel = this.#channel;
|
|
535
|
+
await withStartupDeadline(() => new Promise((resolve, reject) => {
|
|
536
|
+
const cleanup = () => {
|
|
537
|
+
channel.removeEventListener("open", opened);
|
|
538
|
+
channel.removeEventListener("close", closed);
|
|
539
|
+
this.#closing.signal.removeEventListener("abort", closed);
|
|
540
|
+
};
|
|
541
|
+
const opened = () => { cleanup(); resolve(); };
|
|
542
|
+
const closed = () => { cleanup(); reject(new Error("voice data channel closed before opening")); };
|
|
543
|
+
channel.addEventListener("open", opened);
|
|
544
|
+
channel.addEventListener("close", closed);
|
|
545
|
+
this.#closing.signal.addEventListener("abort", closed, { once: true });
|
|
546
|
+
if (channel.readyState === "open") opened();
|
|
547
|
+
else if (channel.readyState === "closed" || this.#closing.signal.aborted) closed();
|
|
548
|
+
}), {
|
|
549
|
+
signal: this.#closing.signal, timeoutMs: SIDEBAND_OPEN_TIMEOUT_MS,
|
|
550
|
+
timeoutError: new VoiceError("data_channel_open_timeout", "The Realtime voice data channel did not open in time."),
|
|
551
|
+
});
|
|
552
|
+
if (!this.#closed && this.#channel === channel) await this.#applyLive(() => this.#core.sidebandOpened());
|
|
553
|
+
}
|
|
554
|
+
|
|
495
555
|
#status(message) {
|
|
496
556
|
if (!this.#closed) this.#options.onStatus(message);
|
|
497
557
|
}
|
|
@@ -504,6 +564,7 @@ export class BrowserVoiceSession {
|
|
|
504
564
|
}
|
|
505
565
|
|
|
506
566
|
#stopBrowserMedia() {
|
|
567
|
+
this.#elevenLabs?.close();
|
|
507
568
|
if (this.#meterTimer !== undefined) window.clearTimeout(this.#meterTimer);
|
|
508
569
|
this.#meterTimer = undefined;
|
|
509
570
|
this.#call?.abort();
|
|
@@ -512,8 +573,13 @@ export class BrowserVoiceSession {
|
|
|
512
573
|
this.#flushTimer = undefined;
|
|
513
574
|
if (this.#reconnectTimer !== undefined) window.clearTimeout(this.#reconnectTimer);
|
|
514
575
|
this.#reconnectTimer = undefined;
|
|
515
|
-
|
|
576
|
+
// Match native close: release provider media before durable tail cleanup.
|
|
577
|
+
if (this.#directControl && this.#channel?.readyState === "open") {
|
|
578
|
+
try { this.#channel.send('{"type":"session.close"}'); } catch {}
|
|
579
|
+
}
|
|
580
|
+
const channel = this.#channel;
|
|
516
581
|
this.#channel = undefined;
|
|
582
|
+
channel?.close();
|
|
517
583
|
this.#peer?.close();
|
|
518
584
|
this.#peer = undefined;
|
|
519
585
|
stopStream(this.#microphone);
|
package/browser/WorkerAgent.mjs
CHANGED
|
@@ -1317,7 +1317,8 @@ function serializeConfig(options) {
|
|
|
1317
1317
|
...connection
|
|
1318
1318
|
} = setup;
|
|
1319
1319
|
if (hostAuth === true) {
|
|
1320
|
-
if (typeof connection.
|
|
1320
|
+
if (typeof connection.createResponse === "function"
|
|
1321
|
+
|| typeof connection.createWebSocket === "function"
|
|
1321
1322
|
|| typeof connection.WebSocketImpl === "function") {
|
|
1322
1323
|
throw new TypeError("Worker Agent host-managed transport callbacks must live inside a custom Worker");
|
|
1323
1324
|
}
|
package/browser/host.d.mts
CHANGED
|
@@ -41,6 +41,12 @@ export type BrowserWebSocketRequest = BrowserWebSocketMetadata & (
|
|
|
41
41
|
}
|
|
42
42
|
);
|
|
43
43
|
|
|
44
|
+
/** One streaming HTTPS Responses request. The host must preserve cancellation. */
|
|
45
|
+
export type BrowserHttpRequest = Exclude<BrowserWebSocketRequest, { authorization: "preconnect" }> & {
|
|
46
|
+
body: string;
|
|
47
|
+
signal: AbortSignal;
|
|
48
|
+
};
|
|
49
|
+
|
|
44
50
|
export type BrowserWebSocketConnection = {
|
|
45
51
|
socket: WebSocket;
|
|
46
52
|
status?: number | undefined;
|
|
@@ -59,6 +65,7 @@ export function createBrowserHost(options?: {
|
|
|
59
65
|
sessionId: string,
|
|
60
66
|
request: BrowserWebSocketRequest,
|
|
61
67
|
) => WebSocket | BrowserWebSocketConnection | Promise<WebSocket | BrowserWebSocketConnection>;
|
|
68
|
+
createResponse?: (endpoint: string, sessionId: string, request: BrowserHttpRequest) => Promise<Response>;
|
|
62
69
|
filesystem?: Workspace;
|
|
63
70
|
filesystemTools?: boolean;
|
|
64
71
|
onEvent?: (eventJson: string) => void;
|
|
@@ -68,12 +75,28 @@ export function createBrowserHost(options?: {
|
|
|
68
75
|
mcp?: McpServers;
|
|
69
76
|
codeEvaluator?: CodeEvaluator;
|
|
70
77
|
toolMode?: "code" | "direct";
|
|
71
|
-
/** @internal
|
|
78
|
+
/** @internal Live host lifecycle for ephemeral Rust-owned subagents. */
|
|
79
|
+
subagentRouting?: Pick<import('../runtime/subagent-routing.mjs').SubagentRouting, 'resolve' | 'bind'>;
|
|
72
80
|
subagentSessions?: {
|
|
73
|
-
|
|
74
|
-
bind(sessionId: string, descriptor: SubagentToolContext): void;
|
|
75
|
-
release(sessionId: string): void;
|
|
81
|
+
bindingDescriptor?(sessionId: string, descriptor: SubagentToolContext, hostContextRef?: string): SubagentToolContext;
|
|
82
|
+
bind(sessionId: string, descriptor: SubagentToolContext, hostContextRef?: string): void;
|
|
83
|
+
release(sessionId: string, hostContextRef?: string): void;
|
|
76
84
|
};
|
|
85
|
+
/** @internal Content-free summary, once when ownership closes; no public transport option. */
|
|
86
|
+
onSocketTiming?: (timing: {
|
|
87
|
+
message_count: number;
|
|
88
|
+
delivered_message_count: number;
|
|
89
|
+
buffered_message_count: number;
|
|
90
|
+
discarded_message_count: number;
|
|
91
|
+
queue_residence_total_ms: number;
|
|
92
|
+
queue_residence_max_ms: number;
|
|
93
|
+
provider_timings: Array<{
|
|
94
|
+
response_id?: string;
|
|
95
|
+
pre_inference_ms?: number;
|
|
96
|
+
engine_queue_max_ms?: number;
|
|
97
|
+
engine_service_ttft_total_ms?: number;
|
|
98
|
+
}>;
|
|
99
|
+
}) => void;
|
|
77
100
|
maxQueuedMessages?: number;
|
|
78
101
|
maxQueuedBytes?: number;
|
|
79
102
|
maxBufferedSendBytes?: number;
|