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/README.md CHANGED
@@ -10,7 +10,7 @@ import { Actions, Agent, Transport } from "nanocodex/node";
10
10
 
11
11
  const agent = await Agent.create({
12
12
  transport: Transport.openAi({ apiKey: process.env.OPENAI_API_KEY }),
13
- model: "gpt-5.6-luna",
13
+ model: "gpt-6-luna",
14
14
  instructions: "You are a Rust coding agent. Preserve unrelated work and run relevant tests.",
15
15
  reasoningMode: "pro",
16
16
  thinking: "high",
@@ -80,6 +80,23 @@ Worker, Durable Object, or application proxy that owns rotating credentials.
80
80
  Authentication modes are constructors rather than a union of mutually
81
81
  exclusive fields on `Agent.create`.
82
82
 
83
+ ### Personal memories and caller context
84
+
85
+ Managed agents can keep user preferences separate from team knowledge:
86
+
87
+ ```js
88
+ import { Agent } from "nanocodex/managed";
89
+
90
+ await Agent.memory({ operation: "scan", query: "my preferences" }, { scope: "personal" });
91
+ await Agent.memory({ operation: "put", content: "I prefer concise replies." }, { scope: "personal" });
92
+ ```
93
+
94
+ The default scope remains `team`. Personal memories belong to the authenticated
95
+ user within their organization, across teams; use the same scope when reading,
96
+ replacing, or deleting a memory. Managed clients also accept descriptive
97
+ `requestOrigin: { client: "desktop", hand: "user:HAND_ID", cwd: "/HAND_ID" }`.
98
+ Caller context is included once at startup and never grants execution authority.
99
+
83
100
  ### Compose and place tools
84
101
 
85
102
  `createTools` owns one deterministic tool recipe. Custom functions, a portable
@@ -158,8 +175,11 @@ workspace or MCP configuration to an Agent that already receives them through
158
175
  `Tools`.
159
176
 
160
177
  Browser consumers can attach Codex's ChatGPT Realtime voice lifecycle to the
161
- same retained Agent. The resource owns microphone, speaker, WebRTC, sideband,
178
+ same retained Agent. The resource owns microphone, speaker, WebRTC control,
162
179
  and delegation cleanup; stopping voice does not cancel an active coding turn.
180
+ Managed and Connect voice use the WebRTC data channel for live events and
181
+ commands. Speech and captions can flow while durable startup completes;
182
+ delegated work still waits for admission. Local Agents retain the sideband path.
163
183
  Snapshots update each speaker's transcript row as speech arrives, using a stable
164
184
  `id` and `isPartial` flag. Completion replaces that row. `transcript.delta` events
165
185
  carry the current partial text; `transcript` events retain completed-turn semantics.
@@ -214,15 +234,22 @@ subscription adapter (which treats all roles as context), and
214
234
  `appendContext(voice, text)` adds background commentary without
215
235
  requesting speech. Context and speech are split into provider-sized messages.
216
236
  These commands retain frames until sent and preserve them
217
- across a sideband reconnect. They are also methods on the resource and on
218
- `useVoice` from `nanocodex-react`. These settings use ChatGPT subscription voice;
219
- custom voices and Platform audio configuration are not accepted.
237
+ across reconnects when using the sideband transport. They are also methods on
238
+ the resource and on
239
+ `useVoice` from `nanocodex-react`. Choose `outputProvider: "elevenlabs"` and `elevenLabsVoiceId` to synthesize
240
+ spoken output with an ElevenLabs account voice, including an instant clone.
241
+ The account voice settings panel connects the API key, lists voices, and uploads
242
+ cloning samples after explicit consent. Keys are encrypted on the server and
243
+ never included in saved voice settings. ChatGPT still owns live input and agent
244
+ handoffs; `voice` continues to select its built-in voice. The default output
245
+ provider is `openai`. Platform audio configuration is not accepted.
220
246
 
221
247
  `Voice.create(...)` remains the equivalent namespaced resource constructor, and
222
248
  `Voice.voices` is the exact ChatGPT V3 voice catalog. The constructor accepts a
223
249
  normal browser Agent, an account-owned managed Agent, or a grant-scoped
224
- `ConnectAgent`. Authentication stays in the owning host routes; Connect uses a
225
- fresh one-use sideband ticket, and the browser binding never receives ChatGPT
250
+ `ConnectAgent`. Authentication stays in the owning host routes. An explicit
251
+ `sidebandUrl` override selects the sideband transport; Connect uses a fresh
252
+ one-use ticket for that path. The browser binding never receives ChatGPT
226
253
  credentials or places its reusable grant bearer in a WebSocket URL.
227
254
 
228
255
  ### Durable Cloudflare Agent
@@ -280,10 +307,12 @@ Cloudflare Agents default to direct tool mode because Workers prohibit dynamic
280
307
  `eval`/`new Function`. Caller-defined tools therefore work without a code
281
308
  evaluator. Select `toolMode: "code"` only when also supplying an evaluator that
282
309
  is explicitly compatible with the deployed Worker runtime. Runtime-owned
283
- Subagents are installed by default, including on a durable root. Clean children
284
- persist independent execution state under their own agent session IDs. The
285
- Rust task-tree registry remains in memory and is closed with the live root, so
286
- tree-local IDs and topology are not reconstructed from those agent states. Use
310
+ Subagents are installed by default, including on a durable root. All children are
311
+ ephemeral: their identities, topology, conversations, results, and routing exist
312
+ only for the lifetime of the root runtime. Root shutdown or restart discards the
313
+ entire child tree; only the root's own durable history resumes. Existing child
314
+ checkpoints from older versions are discarded. Within a live runtime, completed
315
+ children remain available for follow-up messages until closed. Use
287
316
  `Subagents.create({ maxConcurrency })` in `tools` to set an explicit finite
288
317
  concurrency limit. Active subagent turns are unlimited by default.
289
318
 
@@ -327,6 +356,11 @@ native and WASM consumers run the same Rust implementation and receive the
327
356
  same seven tools: `spawn_agent`, `submit_result`, `send_agent_message`,
328
357
  `list_agents`, `wait_agent`, `interrupt_agent`, and `close_agent`.
329
358
 
359
+ Children call `submit_result({output})`; the runtime supplies the trusted
360
+ instruction revision. The response is `{accepted: true, status: "accepted"}` or
361
+ `{accepted: false, status: "superseded"}`. Superseded submissions are normal
362
+ continuations: incorporate the updated instructions and submit again.
363
+
330
364
  Inside a caller-owned Worker or server isolate, host capabilities stay as
331
365
  ordinary functions without crossing another compatibility protocol:
332
366
 
@@ -500,7 +534,7 @@ use the individual factories in server-side Cloudflare Workers. Vite integration
500
534
  is provided separately by `nanocodex-vite`.
501
535
 
502
536
  The browser composition includes native `browseX` public X browsing, advertised
503
- by `accountInfo().apis` without an X connector. The embedding app serves
537
+ by `environment().apis` without an X connector. The embedding app serves
504
538
  `/api/tools/x/browse` and `/api/tools/x/convert`; Nanocodex's account app forwards
505
539
  these requests to the private X Worker.
506
540
 
@@ -664,7 +698,7 @@ const mpp = tempoProvider.session;
664
698
 
665
699
  const agent = await Agent.create({
666
700
  transport: Transport.mpp({ session: tempoProvider }),
667
- thinking: "none",
701
+ thinking: "low",
668
702
  fastMode: true,
669
703
  tools,
670
704
  });
@@ -714,8 +748,8 @@ of its wallet adapter, and constructs both payment paths from that provider's
714
748
  adapter-neutral `getMppxParameters()` contract. The lower-level
715
749
  `createTempoProvider({ session, payment })` remains available when the
716
750
  application constructs MPPx itself. Both explicitly select Tempo provider mode.
717
- In that mode Nanocodex automatically adds its built-in Mercator MCP and wraps it
718
- with the same wallet and payment policy. The provider also exposes an MPP-aware
751
+ In that mode Nanocodex automatically adds its built-in Mercator MCP at
752
+ `https://mercator.sh/mcp` and wraps it with the same wallet and payment policy. The provider also exposes an MPP-aware
719
753
  `fetch`; Mercator's paid REST handoffs use that same method rather than a second
720
754
  wallet or payment configuration. Its MCP transport remains wrapped at the MCP
721
755
  protocol layer, so browser requests do not need an `Accept-Payment` CORS header.
@@ -1133,7 +1167,7 @@ package manager or build step:
1133
1167
 
1134
1168
  ```html
1135
1169
  <script type="module">
1136
- import { Agent, Transport } from "https://cdn.jsdelivr.net/npm/nanocodex@0.6.1/host/index.mjs";
1170
+ import { Agent, Transport } from "https://cdn.jsdelivr.net/npm/nanocodex@0.6.5/host/index.mjs";
1137
1171
  const agent = await Agent.create({
1138
1172
  transport: Transport.hostManaged({
1139
1173
  websocketUrl: "/api/responses",
@@ -1183,3 +1217,133 @@ inspect `agent.configuration()`, `environment()`, `usage()`, `requests()`,
1183
1217
  `artifacts`, `webhook`, and `requiredActions`. See the
1184
1218
  [managed configuration and operations guide](../../docs/MANAGED_AGENT_CONFIGURATION.md)
1185
1219
  for examples, authorization, delivery semantics, and runtime limits.
1220
+
1221
+
1222
+ ## Call connected services from your app
1223
+
1224
+ After Nanocodex Connect login, the app and its agents share the same approved
1225
+ service capabilities and exact account selections. Request the services during
1226
+ login and call them directly without creating an agent or running a turn:
1227
+
1228
+ ```js
1229
+ const connection = await client.connection.connect({
1230
+ capabilities: { cloudAccounts: { spotify: true, soundcloud: true } },
1231
+ });
1232
+ const response = await client.connectors.spotify.request({
1233
+ path: "/v1/me/playlists?limit=20",
1234
+ // Select an approved account when more than one is connected:
1235
+ connectionId: connection.grant.connectorConnections.spotify[0],
1236
+ });
1237
+ if (!response.ok) throw new Error(`Spotify returned ${response.status}`);
1238
+ const playlists = await response.json();
1239
+ ```
1240
+
1241
+ `client` is a `Client.create(...)` instance from `nanocodex/connect`. For a dynamic service, use
1242
+ `client.connectors.request({ connector: "spotify", path: "/v1/me/playlists" })`.
1243
+ The standalone form is `Actions.connectors.request(client, options)`.
1244
+ Every API service has a scoped entry point, including `client.connectors.gmail`
1245
+ and `client.connectors.soundcloud`. The original singular `client.connector.request`
1246
+ and `Actions.connector.request` remain supported. Requests support a provider
1247
+ path, HTTP method, optional `connectionId`, JSON object `body`, and abort `signal`.
1248
+ The response preserves provider HTTP status, body, and pagination/rate-limit
1249
+ headers. Writes are never automatically retried.
1250
+
1251
+ The underlying API is `POST /v1/connectors/:connector/request` at the configured
1252
+ Connect API origin, authenticated with the app's Connect grant bearer token and
1253
+ `X-Nanocodex-App-Id`. Browser calls must come from the grant's approved app origin.
1254
+ Its JSON body uses `path`, `method`, optional `connection_id`, and optional `body`.
1255
+ It requires no conversation or thread ID. Both this endpoint and agent egress
1256
+ check current grant revocation, expiry, approved service, and exact account IDs.
1257
+ Connecting an account alone does not grant it to every app; the user must approve
1258
+ that service in the app's Connect flow. Provider tokens remain in the broker.
1259
+ ChatGPT is a model connector and has no generic HTTP endpoint here.
1260
+
1261
+
1262
+ ### Use a provider SDK with a custom base URL
1263
+
1264
+ Native SDKs can use normal HTTP through
1265
+ `https://nanocodex.gakonst.workers.dev/connectors/<service>/<provider-path>`.
1266
+ All 13 API capabilities share this route. Append the provider's existing path:
1267
+
1268
+ | Connector | Example route |
1269
+ | --- | --- |
1270
+ | Spotify | `/connectors/spotify/v1/me/playlists` |
1271
+ | SoundCloud | `/connectors/soundcloud/me/playlists` |
1272
+ | Gmail | `/connectors/gmail/gmail/v1/users/me/messages` |
1273
+ | Google Drive | `/connectors/gdrive/drive/v3/files` |
1274
+ | Google Calendar | `/connectors/gcalendar/calendar/v3/calendars/primary/events` |
1275
+ | Google Tasks | `/connectors/gtasks/tasks/v1/users/@me/lists` |
1276
+ | Google Docs | `/connectors/gdocs/v1/documents/{id}` |
1277
+ | Google Sheets | `/connectors/gsheets/v4/spreadsheets/{id}` |
1278
+ | Google Slides | `/connectors/gslides/v1/presentations/{id}` |
1279
+ | Google Contacts | `/connectors/gcontacts/v1/people/me/connections?personFields=names` |
1280
+ | GitHub | `/connectors/github/user` |
1281
+ | Slack | `/connectors/slack/api/auth.test` |
1282
+ | X | `/connectors/x/2/users/me` |
1283
+
1284
+ Provider path restrictions and upstream OAuth permissions still apply. Google
1285
+ services are individually opted in even though they share a Google login.
1286
+ ChatGPT and remote MCP connections use their existing separate protocols.
1287
+ The same routes are available on the configured Connect API origin.
1288
+
1289
+ Send the app's **Nanocodex Connect grant token** as the bearer token. Native
1290
+ requests need no extra app identity headers: the token identifies its approved
1291
+ app and user. SDK routes also accept `Authorization: OAuth <grant-token>`
1292
+ (SoundCloud) and `Authorization: token <grant-token>` (GitHub). Tokens must stay in
1293
+ headers, not query parameters. Browser Origins and any explicit app ID must match that grant.
1294
+ The grant must already include the requested service; this route cannot connect
1295
+ accounts or enlarge the grant. If multiple accounts were approved, set
1296
+ `X-Nanocodex-Connector-Connection` to the selected approved connection ID.
1297
+
1298
+ For example, with [Spotipy](https://github.com/spotipy-dev/spotipy):
1299
+
1300
+ ```python
1301
+ import spotipy
1302
+
1303
+ spotify = spotipy.Spotify(auth=nanocodex_grant_token, retries=0, status_retries=0)
1304
+ spotify.prefix = "https://nanocodex.gakonst.workers.dev/connectors/spotify/v1/"
1305
+ page = spotify.current_user_playlists(limit=20)
1306
+ next_page = spotify.next(page)
1307
+ ```
1308
+
1309
+ For Gmail with Google's [Python API client](https://github.com/googleapis/google-api-python-client):
1310
+
1311
+ ```python
1312
+ from google.oauth2.credentials import Credentials
1313
+ from googleapiclient.discovery import build
1314
+
1315
+ gmail = build(
1316
+ "gmail", "v1",
1317
+ credentials=Credentials(token=nanocodex_grant_token),
1318
+ client_options={
1319
+ "api_endpoint": "https://nanocodex.gakonst.workers.dev/connectors/gmail/",
1320
+ },
1321
+ cache_discovery=False,
1322
+ )
1323
+ messages = gmail.users().messages().list(userId="me").execute(num_retries=0)
1324
+ ```
1325
+
1326
+ Use `userId="me"`; Gmail routes are restricted to the connected user. These
1327
+ credentials hold only the Connect grant; renew an expired grant through Nanocodex
1328
+ Connect instead of asking the provider SDK to refresh it with Google or Spotify.
1329
+ SoundCloud clients use `/connectors/soundcloud/` as their base URL and the same
1330
+ Connect grant, using [SoundCloud's OAuth header format](https://developers.soundcloud.com/docs).
1331
+
1332
+ The proxy preserves request methods, query parameters, and body bytes. Provider
1333
+ status codes, JSON errors, rate-limit headers, and non-JSON bodies are preserved.
1334
+ API links in JSON, pagination Link headers, and same-provider redirects are
1335
+ rewritten through the proxy; artwork and public share URLs stay unchanged.
1336
+ Cross-provider redirects are rejected. JSON link rewriting is bounded to 16 MiB
1337
+ per response; use provider pagination for larger collections. Spotify GETs share
1338
+ one-second read results and the broker's registration-wide rate-limit cooldown.
1339
+ The broker retries a rate-limited Spotify GET at most twice within a ten-second
1340
+ cooldown-wait budget; longer limits return 429 with the remaining `Retry-After`.
1341
+ No writes are automatically retried. Disable SDK write retries to avoid repeating
1342
+ a successful write after an ambiguous network failure.
1343
+
1344
+ See the runnable [Python example](../../examples/python/spotify_proxy.py).
1345
+ The integration test uses real Spotipy 2.26.0 and google-api-python-client 2.192.0
1346
+ against the Connect Worker with fixture provider responses, covering reads,
1347
+ writes, pagination, rate limits, and revoked grants. Worker tests cover routing
1348
+ and service denial for all 13 API connectors. SDKs that hardcode their API host need a custom transport instead of only
1349
+ a base URL setting.
@@ -33,7 +33,7 @@ export declare namespace create {
33
33
  transport: ManagedTransport;
34
34
  tools?: Tools | undefined;
35
35
  }>;
36
- type Options = AgentOptions & WorkerToolExposureOptions & {
36
+ type Options = Omit<AgentOptions, "beforeCompaction"> & WorkerToolExposureOptions & {
37
37
  /** Precompiled browser module; WebAssembly modules are structured-clone-safe. */
38
38
  module?: WebAssembly.Module | undefined;
39
39
  /** Fixed browser workspace facts, including its AGENTS.md snapshot. */
@@ -0,0 +1,119 @@
1
+ /** Plays account-proxied PCM without exposing provider credentials to the browser. */
2
+ export class ElevenLabsPlayback {
3
+ #synthesize;
4
+ #onError;
5
+ #context;
6
+ #abort = new AbortController();
7
+ #queue = Promise.resolve();
8
+ #sources = new Set();
9
+ #nextTime = 0;
10
+ #caption;
11
+ #offset = 0;
12
+ #suppressedThrough = -1;
13
+ #pending = 0;
14
+ #pendingChars = 0;
15
+ #closed = false;
16
+
17
+ constructor(synthesize, onError) {
18
+ this.#synthesize = synthesize;
19
+ this.#onError = onError;
20
+ }
21
+
22
+ prime() {
23
+ if (this.#closed) return;
24
+ this.#context ??= new AudioContext({ sampleRate: 24000 });
25
+ return this.#context.resume();
26
+ }
27
+
28
+ transcript(entry) {
29
+ if (this.#closed || entry.speaker !== "assistant" || entry.id <= this.#suppressedThrough) return;
30
+ if (this.#caption !== undefined && entry.id < this.#caption) return;
31
+ if (entry.id !== this.#caption) { this.#caption = entry.id; this.#offset = 0; }
32
+ const remaining = entry.text.slice(this.#offset);
33
+ // Buffer partial words; emit complete sentences before the turn finishes.
34
+ const boundary = entry.is_partial ? [...remaining.matchAll(/[.!?\n](?:\s|$)/g)].at(-1) : undefined;
35
+ const length = entry.is_partial ? (boundary ? boundary.index + boundary[0].length : 0) : remaining.length;
36
+ const text = remaining.slice(0, length).trim();
37
+ this.#offset += length;
38
+ if (!text) return;
39
+ if (this.#pending >= 32 || this.#pendingChars + text.length > 32768) {
40
+ this.interrupt();
41
+ this.#onError(new Error("ElevenLabs playback queue exceeded its limit"));
42
+ return;
43
+ }
44
+ this.#pending++;
45
+ this.#pendingChars += text.length;
46
+ const signal = this.#abort.signal;
47
+ this.#queue = this.#queue.then(async () => {
48
+ if (signal.aborted) return;
49
+ this.#context ??= new AudioContext({ sampleRate: 24000 });
50
+ await this.#context.resume();
51
+ if (signal.aborted) return;
52
+ const response = await this.#synthesize(text, signal);
53
+ if (!response.ok) throw new Error(`ElevenLabs synthesis failed (${response.status})`);
54
+ const reader = response.body?.getReader();
55
+ if (!reader) throw new Error("ElevenLabs returned no audio stream");
56
+ const cancel = () => { void reader.cancel().catch(() => {}); };
57
+ signal.addEventListener("abort", cancel, { once: true });
58
+ let carry;
59
+ try {
60
+ while (!signal.aborted) {
61
+ const { value, done } = await reader.read();
62
+ if (done) break;
63
+ if (signal.aborted) break;
64
+ if (value.byteLength > 1024 * 1024) throw new Error("ElevenLabs audio chunk exceeded its limit");
65
+ const bytes = carry === undefined ? value : Uint8Array.from([carry, ...value]);
66
+ const size = bytes.length - bytes.length % 2;
67
+ carry = size < bytes.length ? bytes[size] : undefined;
68
+ if (!size) continue;
69
+ // Keep decoded/scheduled audio bounded even when synthesis outruns playback.
70
+ for (let offset = 0; offset < size && !signal.aborted; offset += 9600) {
71
+ while (!signal.aborted && (this.#nextTime - this.#context.currentTime > 2 || this.#sources.size >= 128)) {
72
+ await new Promise((resolve) => {
73
+ const done = () => { clearTimeout(timer); signal.removeEventListener("abort", done); resolve(); };
74
+ const timer = setTimeout(done, 25);
75
+ signal.addEventListener("abort", done, { once: true });
76
+ });
77
+ }
78
+ if (signal.aborted) break;
79
+ const length = Math.min(9600, size - offset);
80
+ const buffer = this.#context.createBuffer(1, length / 2, 24000);
81
+ const output = buffer.getChannelData(0);
82
+ const view = new DataView(bytes.buffer, bytes.byteOffset + offset, length);
83
+ for (let i = 0; i < output.length; i++) output[i] = view.getInt16(i * 2, true) / 32768;
84
+ const source = this.#context.createBufferSource();
85
+ source.buffer = buffer;
86
+ source.connect(this.#context.destination);
87
+ this.#sources.add(source);
88
+ source.onended = () => { this.#sources.delete(source); source.disconnect(); };
89
+ this.#nextTime = Math.max(this.#nextTime, this.#context.currentTime + 0.02);
90
+ source.start(this.#nextTime);
91
+ this.#nextTime += buffer.duration;
92
+ }
93
+ }
94
+ } finally { signal.removeEventListener("abort", cancel); await reader.cancel().catch(() => {}); reader.releaseLock(); }
95
+ }).catch((error) => { if (!signal.aborted) this.#onError(error); }).finally(() => {
96
+ if (!signal.aborted) { this.#pending--; this.#pendingChars -= text.length; }
97
+ });
98
+ }
99
+
100
+ interrupt() {
101
+ this.#suppressedThrough = Math.max(this.#suppressedThrough, this.#caption ?? -1);
102
+ this.#pending = 0;
103
+ this.#pendingChars = 0;
104
+ this.#abort.abort();
105
+ this.#abort = new AbortController();
106
+ this.#queue = Promise.resolve();
107
+ for (const source of this.#sources) { try { source.stop(); } catch {} source.disconnect(); }
108
+ this.#sources.clear();
109
+ this.#nextTime = 0;
110
+ // Caption IDs are monotonic: every late delta/final for this epoch stays silent.
111
+ }
112
+
113
+ close() {
114
+ this.#closed = true;
115
+ this.interrupt();
116
+ void this.#context?.close();
117
+ this.#context = undefined;
118
+ }
119
+ }
@@ -16,6 +16,7 @@ import {
16
16
  registerDefinitionHost,
17
17
  releaseDefinitionHost,
18
18
  releaseHostSession,
19
+ releaseHostSessions,
19
20
  toWasmConfig,
20
21
  } from "../internal.mjs";
21
22
  import { createBrowserHost } from "./host.mjs";
@@ -45,8 +46,10 @@ export async function create(options = {}) {
45
46
  thinking,
46
47
  reasoningMode,
47
48
  fastMode,
49
+ rawApiEvents,
48
50
  instructions,
49
51
  additionalInstructions,
52
+ beforeCompaction,
50
53
  sessionId,
51
54
  workspace,
52
55
  resume,
@@ -75,8 +78,10 @@ export async function create(options = {}) {
75
78
  websocketPreconnect,
76
79
  apiBaseUrl,
77
80
  websocketWarmup,
81
+ stateless,
78
82
  WebSocketImpl,
79
83
  createWebSocket,
84
+ createResponse,
80
85
  } = resolveResponsesTransport(transport ?? defaultHostManagedTransport());
81
86
  const subagentsEnabled = internalRuntime?.subagentsEnabled;
82
87
  if (subagentsEnabled !== undefined && typeof subagentsEnabled !== "boolean") {
@@ -109,16 +114,20 @@ export async function create(options = {}) {
109
114
  const host = createBrowserHost({
110
115
  WebSocketImpl,
111
116
  createWebSocket,
117
+ createResponse,
112
118
  hostAuth: hostAuth === true
113
119
  || (apiKey === undefined && mpp === undefined && subscription === undefined),
114
120
  hostManagedProtocol,
115
121
  mpp,
116
122
  onEvent: events.emit,
123
+ beforeCompaction,
117
124
  filesystem,
118
125
  filesystemTools,
119
126
  tools: hostTools,
120
127
  toolProviders,
121
128
  subagentSessions,
129
+ subagentRouting: internalRuntime?.subagentRouting,
130
+ onSocketTiming: internalRuntime?.onSocketTiming,
122
131
  toolMode,
123
132
  mcp: mcp === false
124
133
  ? undefined
@@ -161,8 +170,11 @@ export async function create(options = {}) {
161
170
  : "wss://openai.mpp.tempo.xyz/v1/responses"),
162
171
  apiBaseUrl,
163
172
  websocketWarmup,
173
+ stateless,
164
174
  subagents: subagentConfig,
175
+ subagentRouting: internalRuntime?.subagentRouting !== undefined,
165
176
  hostDefinitionId,
177
+ beforeCompaction: beforeCompaction !== undefined,
166
178
  ...config,
167
179
  durabilityHostId: durabilityOwner?.id,
168
180
  }));
@@ -174,29 +186,19 @@ export async function create(options = {}) {
174
186
  );
175
187
  if (cloudflareReservation !== undefined) {
176
188
  activateCloudflareAgentSession(cloudflareReservation);
177
- const restoredSubagents = subagentSessions?.restore?.() ?? [];
178
- if (restoredSubagents.length > 0) {
179
- const restoredHostContextRefs = Object.fromEntries(
180
- restoredSubagents.map((descriptor) => {
181
- const hostContextRef = subagentSessions?.hostContextRef?.(descriptor.sessionId);
182
- return [descriptor.sessionId, hostContextRef ?? null];
183
- }),
184
- );
185
- await raw.restoreSubagents(
186
- JSON.stringify(restoredSubagents),
187
- JSON.stringify(restoredHostContextRefs),
188
- );
189
- }
190
189
  }
191
190
  return raw;
192
191
  } catch (error) {
193
192
  const cleanupErrors = [];
194
193
  if (raw !== undefined) {
195
- try { await raw.shutdown(); }
194
+ try {
195
+ await raw.shutdown();
196
+ }
196
197
  catch (cleanupError) { cleanupErrors.push(cleanupError); }
197
198
  try { raw.free(); }
198
199
  catch (cleanupError) { cleanupErrors.push(cleanupError); }
199
200
  }
201
+ releaseHostSessions(host);
200
202
  try { durabilityOwner?.abandon(); }
201
203
  catch (cleanupError) { cleanupErrors.push(cleanupError); }
202
204
  try { await host.dispose(); }
@@ -210,16 +212,20 @@ export async function create(options = {}) {
210
212
  throw error;
211
213
  }
212
214
  },
215
+ async shutdown(raw) {
216
+ await raw.shutdown();
217
+ },
213
218
  subscribe: events.subscribe,
214
219
  adopt(raw) {
215
220
  host.retain();
216
221
  try {
217
- durabilityOwner?.retain();
222
+ // Adopted child handles are ephemeral and do not own the root store.
223
+ if (raw.sessionId === stableSessionId) durabilityOwner?.retain();
218
224
  bindHostSession(host, raw.sessionId, cloudflareReservation);
219
225
  events.addSource(raw);
220
226
  } catch (error) {
221
227
  events.removeSource(raw);
222
- durabilityOwner?.release();
228
+ if (raw.sessionId === stableSessionId) durabilityOwner?.release();
223
229
  releaseHost(host);
224
230
  throw error;
225
231
  }
@@ -228,7 +234,7 @@ export async function create(options = {}) {
228
234
  events.removeSource(raw);
229
235
  host.releaseSession(raw.sessionId);
230
236
  releaseHostSession(host, raw.sessionId);
231
- durabilityOwner?.release();
237
+ if (raw.sessionId === stableSessionId) durabilityOwner?.release();
232
238
  releaseHost(host);
233
239
  },
234
240
  decorate: (agent) => agent.extend(agentActions()),
@@ -240,6 +246,7 @@ export async function create(options = {}) {
240
246
  thinking,
241
247
  reasoningMode,
242
248
  fastMode,
249
+ rawApiEvents,
243
250
  instructions,
244
251
  additionalInstructions,
245
252
  sessionId: stableSessionId,
@@ -4,6 +4,7 @@ import type {
4
4
  } from "../types.mjs";
5
5
  import type { Options as ManagedClientOptions } from "../managed/Agent.mjs";
6
6
  import type {
7
+ BrowserHttpRequest,
7
8
  BrowserWebSocketConnection,
8
9
  BrowserWebSocketRequest,
9
10
  } from "./host.mjs";
@@ -25,6 +26,8 @@ export type WorkerTransport = ResponsesTransport & Readonly<{
25
26
 
26
27
  type SharedEndpointOptions = Readonly<{
27
28
  apiBaseUrl?: string | undefined;
29
+ /** Use HTTPS with full history replay, no stored responses, and no WebSocket warmup or preconnect. */
30
+ stateless?: boolean | undefined;
28
31
  websocketUrl?: string | undefined;
29
32
  /** Open the persistent socket as soon as Agent.create returns. Defaults to true for hostManaged. */
30
33
  websocketPreconnect?: boolean | undefined;
@@ -34,9 +37,11 @@ type SharedEndpointOptions = Readonly<{
34
37
  type WorkerEndpointOptions = SharedEndpointOptions & Readonly<{
35
38
  WebSocketImpl?: never;
36
39
  createWebSocket?: never;
40
+ createResponse?: never;
37
41
  }>;
38
42
 
39
43
  type EndpointOptions = SharedEndpointOptions & Readonly<{
44
+ createResponse?(endpoint: string, sessionId: string, request: BrowserHttpRequest): Promise<Response>;
40
45
  WebSocketImpl?: typeof WebSocket | undefined;
41
46
  createWebSocket?(
42
47
  endpoint: string,
@@ -25,12 +25,12 @@ export function chatGpt(options) {
25
25
 
26
26
  export function hostManaged(options = {}) {
27
27
  const websocketUrl = options.websocketUrl
28
- ?? (options.createWebSocket ? undefined : defaultHostManagedWebSocketUrl());
28
+ ?? (options.stateless === true || options.createWebSocket ? undefined : defaultHostManagedWebSocketUrl());
29
29
  return createResponsesTransport({
30
30
  hostAuth: true,
31
31
  hostManagedProtocol: true,
32
32
  ...connection({ ...options, websocketUrl }),
33
- websocketPreconnect: options.websocketPreconnect ?? true,
33
+ websocketPreconnect: options.stateless === true ? false : options.websocketPreconnect ?? true,
34
34
  });
35
35
  }
36
36
 
@@ -53,7 +53,9 @@ function connection(options = {}) {
53
53
  return {
54
54
  WebSocketImpl: options.WebSocketImpl,
55
55
  apiBaseUrl: options.apiBaseUrl,
56
+ stateless: options.stateless,
56
57
  createWebSocket: options.createWebSocket,
58
+ createResponse: options.createResponse,
57
59
  websocketUrl: options.websocketUrl,
58
60
  websocketPreconnect: options.websocketPreconnect,
59
61
  websocketWarmup: options.websocketWarmup,
@@ -34,6 +34,7 @@ export type Snapshot = Readonly<{
34
34
  microphoneLevel: number;
35
35
  speakerLevel: number;
36
36
  error: Error | undefined;
37
+ /** Active means media and live control are ready; task admission may still be pending. */
37
38
  status: "idle" | "connecting" | "active" | "error";
38
39
  statusText: string | undefined;
39
40
  /** Latest 200 rows, retained across stop/start. Subscribe to events for longer history. */
@@ -50,6 +51,8 @@ export type Event =
50
51
  | Readonly<{ type: "stopped" }>;
51
52
  export type Settings = Readonly<{
52
53
  voice?: VoiceName | undefined;
54
+ outputProvider?: "openai" | "elevenlabs" | undefined;
55
+ elevenLabsVoiceId?: string | undefined;
53
56
  /** Additional speaking preferences; base assistant instructions are retained. */
54
57
  instructions?: string | undefined;
55
58
  pace?: "slow" | "natural" | "fast" | undefined;
@@ -58,6 +61,8 @@ export type Settings = Readonly<{
58
61
  acknowledgements?: boolean | undefined;
59
62
  }>;
60
63
  export type Options = Settings & Readonly<{
64
+ /** Authenticated synthesis transport returning mono signed 16-bit little-endian PCM at 24 kHz. */
65
+ synthesize?(text: string, signal: AbortSignal): Promise<Response>;
61
66
  callUrl?: string | URL | undefined;
62
67
  sidebandUrl?(callId: string, sessionId: string): string | URL | Promise<string | URL>;
63
68
  captureMicrophone?(): Promise<MediaStream>;
@@ -78,6 +83,7 @@ export type Voice = Readonly<{
78
83
  destroy(): Promise<void>;
79
84
  getSnapshot(): Snapshot;
80
85
  onEvent(listener: (event: Event) => void): () => void;
86
+ /** Resolves after media and task admission are ready. The active snapshot may arrive first. */
81
87
  start(options?: Settings): Promise<void>;
82
88
  stop(): Promise<void>;
83
89
  subscribe(listener: () => void): () => void;