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.
Files changed (95) hide show
  1. package/README.md +180 -16
  2. package/browser/Agent.d.mts +1 -1
  3. package/browser/ElevenLabsPlayback.mjs +119 -0
  4. package/browser/InlineAgent.mjs +24 -17
  5. package/browser/Transport.d.mts +5 -0
  6. package/browser/Transport.mjs +4 -2
  7. package/browser/Voice.d.mts +6 -0
  8. package/browser/Voice.mjs +39 -8
  9. package/browser/VoiceSession.mjs +90 -24
  10. package/browser/WorkerAgent.mjs +2 -1
  11. package/browser/host.d.mts +27 -4
  12. package/browser/host.mjs +175 -18
  13. package/cloud/Decorator.d.mts +9 -0
  14. package/cloud/Decorator.mjs +9 -0
  15. package/cloud/Transport.mjs +6 -0
  16. package/cloud/actions/connection.mjs +3 -0
  17. package/cloud/actions/connector.d.mts +18 -0
  18. package/cloud/actions/connector.mjs +22 -0
  19. package/cloud/actions/index.d.mts +2 -0
  20. package/cloud/actions/index.mjs +2 -0
  21. package/cloud/internal.mjs +5 -0
  22. package/cloud/types.d.mts +2 -0
  23. package/cloudflare/Agent.d.mts +10 -9
  24. package/cloudflare/Agent.mjs +261 -91
  25. package/cloudflare/agent-settings.d.mts +15 -0
  26. package/cloudflare/agent-settings.mjs +135 -0
  27. package/cloudflare/chat-reasoning.mjs +20 -0
  28. package/cloudflare/durable-placement.d.mts +12 -0
  29. package/cloudflare/durable-placement.mjs +82 -0
  30. package/cloudflare/egress.d.mts +34 -1
  31. package/cloudflare/egress.mjs +14 -0
  32. package/cloudflare/gateway-binding-responses.mjs +84 -0
  33. package/cloudflare/gateway-responses.d.mts +48 -0
  34. package/cloudflare/gateway-responses.mjs +191 -0
  35. package/cloudflare/index.d.mts +6 -0
  36. package/cloudflare/index.mjs +3 -0
  37. package/cloudflare/managed-access.d.mts +23 -0
  38. package/cloudflare/managed-access.mjs +120 -0
  39. package/cloudflare/managed-auth.d.mts +15 -0
  40. package/cloudflare/managed-auth.mjs +113 -0
  41. package/cloudflare/managed-live.d.mts +7 -0
  42. package/cloudflare/managed-live.mjs +56 -0
  43. package/cloudflare/prepared-connection.mjs +49 -0
  44. package/cloudflare/provider-stream.mjs +353 -0
  45. package/cloudflare/rpc.d.mts +9 -0
  46. package/cloudflare/rpc.mjs +14 -0
  47. package/cloudflare/tool-alias-diagnostic.mjs +21 -0
  48. package/cloudflare/workers-ai-responses.d.mts +28 -0
  49. package/cloudflare/workers-ai-responses.mjs +398 -0
  50. package/host/Agent.d.mts +1 -1
  51. package/index.d.mts +2 -0
  52. package/internal.mjs +75 -14
  53. package/managed/Access.d.mts +5 -0
  54. package/managed/Access.mjs +66 -0
  55. package/managed/Agent.d.mts +29 -67
  56. package/managed/Agent.mjs +74 -183
  57. package/managed/README.md +9 -38
  58. package/managed/Voice.mjs +3 -1
  59. package/managed/index.d.mts +1 -3
  60. package/managed/index.mjs +1 -0
  61. package/node/Agent.d.mts +1 -1
  62. package/node/Agent.mjs +9 -3
  63. package/node/host.mjs +17 -0
  64. package/package.json +39 -2
  65. package/pkg-node/nanocodex.d.ts +8 -16
  66. package/pkg-node/nanocodex.js +107 -89
  67. package/pkg-node/nanocodex_bg.wasm.d.ts +4 -5
  68. package/pkg-web/.nanocodex-bindgen-stamp +7 -1
  69. package/pkg-web/nanocodex-build.json +1 -1
  70. package/pkg-web/nanocodex.d.ts +12 -21
  71. package/pkg-web/nanocodex.js +107 -88
  72. package/pkg-web/nanocodex_bg.js +107 -88
  73. package/pkg-web/nanocodex_bg.wasm +0 -0
  74. package/pkg-web/nanocodex_bg.wasm.d.ts +4 -5
  75. package/pkg-web/nanocodex_worker.js +1 -1
  76. package/runtime/before-compaction.mjs +49 -0
  77. package/runtime/code-evaluator.worker.mjs +30 -31
  78. package/runtime/managed-transport.mjs +11 -2
  79. package/runtime/mcp-runtime.mjs +15 -1
  80. package/runtime/quickjs-evaluator.mjs +69 -21
  81. package/runtime/response-controls.mjs +94 -25
  82. package/runtime/responses-http.mjs +69 -0
  83. package/runtime/steer-receipt.mjs +13 -0
  84. package/runtime/subagent-routing.d.mts +38 -0
  85. package/runtime/subagent-routing.mjs +80 -0
  86. package/runtime/subagents.d.mts +1 -1
  87. package/runtime/tempo-provider.d.mts +1 -1
  88. package/runtime/tempo-provider.mjs +1 -1
  89. package/tools/browser/accountInfo.mjs +53 -8
  90. package/tools/browser/browserShell.mjs +2 -2
  91. package/tools/browser/index.mjs +3 -3
  92. package/tools/environment.d.mts +40 -0
  93. package/tools/environment.mjs +73 -0
  94. package/tools/index.d.mts +2 -0
  95. 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
- settings: voiceSettings({ ...options, ...parameters, voice: selectedVoice }),
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
- startPromise = next.start().then(() => {
179
- if (destroyed || session !== next || generation !== current) return;
180
- publish({ ...snapshot, error: undefined, status: "active", statusText: `Voice active (${selectedVoice})` });
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
+ }
@@ -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
- // Managed Rust can deliver authoritative context after SDP negotiation.
138
- // Start both immediately, but fence incoming control events on admission.
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 core?.start();
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.#sideband?.readyState === WebSocket.OPEN) {
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(true);
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.#applyLive(async () => {
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
- this.#channel?.close();
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);
@@ -1317,7 +1317,8 @@ function serializeConfig(options) {
1317
1317
  ...connection
1318
1318
  } = setup;
1319
1319
  if (hostAuth === true) {
1320
- if (typeof connection.createWebSocket === "function"
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
  }
@@ -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 Durable host lifecycle for Rust-owned subagent descriptors. */
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
- restore(): readonly SubagentToolContext[];
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;