nanocodex 0.5.0 → 0.6.1

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 (196) hide show
  1. package/README.md +944 -78
  2. package/actions/events.mjs +14 -25
  3. package/actions/index.d.mts +1 -0
  4. package/actions/index.mjs +11 -1
  5. package/actions/session.d.mts +29 -1
  6. package/actions/session.mjs +22 -0
  7. package/actions/turn.d.mts +15 -6
  8. package/actions/turn.mjs +10 -0
  9. package/actions/voice.d.mts +26 -0
  10. package/actions/voice.mjs +54 -0
  11. package/browser/Agent.d.mts +41 -24
  12. package/browser/Agent.mjs +7 -84
  13. package/browser/ChatGptSubscription.d.mts +7 -0
  14. package/browser/ChatGptSubscription.mjs +19 -0
  15. package/browser/InlineAgent.mjs +267 -0
  16. package/browser/Transport.d.mts +80 -0
  17. package/browser/Transport.mjs +63 -0
  18. package/browser/Voice.d.mts +86 -0
  19. package/browser/Voice.mjs +316 -0
  20. package/browser/VoiceSession.mjs +763 -0
  21. package/browser/WorkerAgent.d.mts +65 -0
  22. package/browser/WorkerAgent.mjs +1467 -0
  23. package/browser/agent.worker.mjs +3 -0
  24. package/browser/config.d.mts +36 -0
  25. package/browser/config.mjs +439 -0
  26. package/browser/engine.mjs +13 -0
  27. package/browser/harness.mjs +55 -0
  28. package/browser/host.d.mts +33 -5
  29. package/browser/host.mjs +320 -26
  30. package/browser/hostManagedWebSocket.d.mts +14 -0
  31. package/browser/hostManagedWebSocket.mjs +110 -0
  32. package/browser/index.d.mts +64 -1
  33. package/browser/index.mjs +22 -1
  34. package/browser/indexeddb-durability-store.mjs +210 -0
  35. package/browser/workspace.d.mts +16 -0
  36. package/browser/workspace.mjs +107 -0
  37. package/cloud/Client.d.mts +83 -0
  38. package/cloud/Client.mjs +231 -0
  39. package/cloud/Decorator.d.mts +31 -0
  40. package/cloud/Decorator.mjs +37 -0
  41. package/cloud/Dialog.d.mts +113 -0
  42. package/cloud/Dialog.mjs +366 -0
  43. package/cloud/Errors.d.mts +22 -0
  44. package/cloud/Errors.mjs +39 -0
  45. package/cloud/Principal.d.mts +26 -0
  46. package/cloud/Principal.mjs +88 -0
  47. package/cloud/RemoteProvider.mjs +88 -0
  48. package/cloud/Transport.d.mts +47 -0
  49. package/cloud/Transport.mjs +454 -0
  50. package/cloud/actions/account.d.mts +9 -0
  51. package/cloud/actions/account.mjs +19 -0
  52. package/cloud/actions/agent.d.mts +18 -0
  53. package/cloud/actions/agent.mjs +429 -0
  54. package/cloud/actions/connection.d.mts +133 -0
  55. package/cloud/actions/connection.mjs +580 -0
  56. package/cloud/actions/grant.d.mts +10 -0
  57. package/cloud/actions/grant.mjs +12 -0
  58. package/cloud/actions/index.d.mts +7 -0
  59. package/cloud/actions/index.mjs +7 -0
  60. package/cloud/actions/machineUsd.d.mts +23 -0
  61. package/cloud/actions/machineUsd.mjs +37 -0
  62. package/cloud/actions/model.d.mts +12 -0
  63. package/cloud/actions/model.mjs +50 -0
  64. package/cloud/actions/mpp.d.mts +28 -0
  65. package/cloud/actions/mpp.mjs +39 -0
  66. package/cloud/index.d.mts +30 -0
  67. package/cloud/index.mjs +9 -0
  68. package/cloud/internal.mjs +533 -0
  69. package/cloud/mercator.mjs +108 -0
  70. package/cloud/server/HostPrincipal.d.mts +34 -0
  71. package/cloud/server/HostPrincipal.mjs +202 -0
  72. package/cloud/server/index.d.mts +1 -0
  73. package/cloud/server/index.mjs +1 -0
  74. package/cloud/types.d.mts +212 -0
  75. package/cloudflare/Agent.d.mts +136 -0
  76. package/cloudflare/Agent.mjs +784 -0
  77. package/cloudflare/egress-subject.mjs +19 -0
  78. package/cloudflare/egress.d.mts +41 -0
  79. package/cloudflare/egress.mjs +119 -0
  80. package/cloudflare/event-socket.mjs +316 -0
  81. package/cloudflare/index.d.mts +7 -0
  82. package/cloudflare/index.mjs +6 -0
  83. package/host/Agent.d.mts +55 -0
  84. package/host/Agent.mjs +1 -0
  85. package/host/index.d.mts +71 -0
  86. package/host/index.mjs +10 -0
  87. package/index.d.mts +46 -0
  88. package/index.mjs +11 -0
  89. package/internal.mjs +673 -61
  90. package/managed/Agent.d.mts +459 -0
  91. package/managed/Agent.mjs +1824 -0
  92. package/managed/ManagedError.d.mts +9 -0
  93. package/managed/ManagedError.mjs +8 -0
  94. package/managed/README.md +244 -0
  95. package/managed/Voice.mjs +209 -0
  96. package/managed/index.d.mts +22 -0
  97. package/managed/index.mjs +2 -0
  98. package/managed/internal.mjs +119 -0
  99. package/node/Agent.d.mts +35 -8
  100. package/node/Agent.mjs +116 -25
  101. package/node/ChatGptSubscription.d.mts +9 -0
  102. package/node/ChatGptSubscription.mjs +17 -0
  103. package/node/Transport.d.mts +41 -0
  104. package/node/Transport.mjs +46 -0
  105. package/node/host.mjs +99 -17
  106. package/node/index.d.mts +45 -1
  107. package/node/index.mjs +16 -1
  108. package/node/workspace.d.mts +10 -0
  109. package/node/workspace.mjs +198 -0
  110. package/package.json +145 -11
  111. package/pkg-node/nanocodex.d.ts +523 -11
  112. package/pkg-node/nanocodex.js +2038 -179
  113. package/pkg-node/nanocodex_bg.wasm.d.ts +100 -29
  114. package/pkg-web/.nanocodex-bindgen-stamp +6 -0
  115. package/pkg-web/nanocodex-build.json +1 -0
  116. package/pkg-web/nanocodex.d.ts +623 -40
  117. package/pkg-web/nanocodex.js +2031 -178
  118. package/pkg-web/nanocodex_bg.js +2667 -0
  119. package/pkg-web/nanocodex_bg.wasm +0 -0
  120. package/pkg-web/nanocodex_bg.wasm.d.ts +100 -29
  121. package/pkg-web/nanocodex_worker.js +9 -0
  122. package/runtime/chatgpt-subscription.mjs +212 -0
  123. package/runtime/cloudflare-durability-store.d.mts +26 -0
  124. package/runtime/cloudflare-durability-store.mjs +14 -0
  125. package/runtime/code-evaluator.worker.mjs +137 -0
  126. package/runtime/code-runtime.mjs +1 -250
  127. package/runtime/durability-store.d.mts +85 -0
  128. package/runtime/durability-store.mjs +752 -0
  129. package/runtime/durability.mjs +174 -0
  130. package/runtime/managed-transport.mjs +461 -0
  131. package/runtime/mcp-runtime.mjs +534 -0
  132. package/runtime/postgres-durability-store.d.mts +64 -0
  133. package/runtime/postgres-durability-store.mjs +581 -0
  134. package/runtime/quickjs-evaluator.d.mts +17 -0
  135. package/runtime/quickjs-evaluator.mjs +236 -0
  136. package/runtime/response-controls.mjs +42 -0
  137. package/runtime/response-lanes.mjs +72 -0
  138. package/runtime/responses-transport.mjs +22 -0
  139. package/runtime/subagents.d.mts +107 -0
  140. package/runtime/subagents.mjs +54 -0
  141. package/runtime/subscription-store.d.mts +8 -0
  142. package/runtime/subscription-store.mjs +42 -0
  143. package/runtime/tempo-provider.d.mts +125 -0
  144. package/runtime/tempo-provider.mjs +264 -0
  145. package/runtime/tool-configuration.mjs +1 -0
  146. package/runtime/tool-router.mjs +1 -0
  147. package/runtime/utf8.mjs +1 -0
  148. package/runtime/worker-evaluator.mjs +162 -0
  149. package/runtime/workspace.d.mts +1 -0
  150. package/runtime/workspace.mjs +1 -0
  151. package/tools/Tools.d.mts +56 -0
  152. package/tools/Tools.mjs +136 -0
  153. package/tools/artifact.d.mts +1 -0
  154. package/tools/artifact.mjs +1 -0
  155. package/tools/attachment.mjs +1 -0
  156. package/tools/bash.d.mts +1 -0
  157. package/tools/bash.mjs +1 -0
  158. package/tools/browser/accountInfo.mjs +795 -0
  159. package/tools/browser/browserBuffer.mjs +18 -0
  160. package/tools/browser/browserCompiler.mjs +155 -0
  161. package/tools/browser/browserEgress.mjs +161 -0
  162. package/tools/browser/browserPython.mjs +133 -0
  163. package/tools/browser/browserShell.mjs +1075 -0
  164. package/tools/browser/browserSprintf.mjs +4 -0
  165. package/tools/browser/browserSsh.mjs +201 -0
  166. package/tools/browser/browserZlib.mjs +136 -0
  167. package/tools/browser/compiler.worker.mjs +160 -0
  168. package/tools/browser/devTunnelsSshBrowser.mjs +13 -0
  169. package/tools/browser/index.d.mts +209 -0
  170. package/tools/browser/index.mjs +174 -0
  171. package/tools/browser/opfsGit.mjs +248 -0
  172. package/tools/browser/python.worker.mjs +114 -0
  173. package/tools/browser/threadGit.mjs +197 -0
  174. package/tools/browser/unsupportedNodeRsa.mjs +5 -0
  175. package/tools/browser/workspace.mjs +83 -0
  176. package/tools/dataset.d.mts +1 -0
  177. package/tools/dataset.mjs +1 -0
  178. package/tools/datasetContract.mjs +1 -0
  179. package/tools/datasetEngine.mjs +1 -0
  180. package/tools/hostedCatalog.d.mts +1 -0
  181. package/tools/hostedCatalog.mjs +1 -0
  182. package/tools/index.d.mts +49 -0
  183. package/tools/index.mjs +19 -0
  184. package/tools/namedTool.mjs +1 -0
  185. package/tools/repository-workspace.d.mts +1 -0
  186. package/tools/repository-workspace.mjs +1 -0
  187. package/tools/ssh.d.mts +1 -0
  188. package/tools/ssh.mjs +1 -0
  189. package/tools/standard.mjs +1 -0
  190. package/tools/standardDescriptions.mjs +1 -0
  191. package/types.d.mts +431 -16
  192. package/worker/ChatGptSubscription.d.mts +9 -0
  193. package/worker/ChatGptSubscription.mjs +29 -0
  194. package/worker/index.d.mts +18 -0
  195. package/worker/index.mjs +5 -0
  196. package/pkg-node/nanocodex_bg.wasm +0 -0
package/README.md CHANGED
@@ -1,14 +1,15 @@
1
1
  # Nanocodex for JavaScript
2
2
 
3
- The Node and browser entrypoints expose the same viem-v3-style API over the
4
- same Rust/WASM agent. Runtime-specific host options are flattened into
5
- `Agent.create(...)`; generated WASM handles and host routing remain private.
3
+ The Node, browser, and Web API host entrypoints expose the same viem-v3-style
4
+ API. A `Transport` owns authentication, placement, and socket setup;
5
+ `Agent.create(...)` owns tools and the common Agent/Turn lifecycle. Generated
6
+ WASM handles, managed control-plane handles, and host routing remain private.
6
7
 
7
8
  ```js
8
- import { Actions, Agent } from "nanocodex/node";
9
+ import { Actions, Agent, Transport } from "nanocodex/node";
9
10
 
10
11
  const agent = await Agent.create({
11
- apiKey: process.env.OPENAI_API_KEY,
12
+ transport: Transport.openAi({ apiKey: process.env.OPENAI_API_KEY }),
12
13
  model: "gpt-5.6-luna",
13
14
  instructions: "You are a Rust coding agent. Preserve unrelated work and run relevant tests.",
14
15
  reasoningMode: "pro",
@@ -21,9 +22,10 @@ const turn = agent.turn.prompt({ input: "Build the thing." });
21
22
  const result = await turn.result();
22
23
  turn.dispose();
23
24
  console.log(result.finalMessage);
24
- console.log(result.usage);
25
- console.log(result.usage.estimated_cost?.usd);
26
- console.log(result.usage.cost_status);
25
+ const usage = await result.usage();
26
+ console.log(usage);
27
+ console.log(usage.estimated_cost?.usd);
28
+ console.log(usage.cost_status);
27
29
 
28
30
  await agent.session.setThinking("high");
29
31
  await agent.session.setFastMode(true);
@@ -34,24 +36,595 @@ const branchTurn = branch.turn.prompt({ input: "Try another approach." });
34
36
  const branchResult = await branchTurn.result();
35
37
  branchTurn.dispose();
36
38
  console.log(branchResult.finalMessage);
39
+ branchResult.dispose();
37
40
 
38
41
  const followOn = Actions.turn.prompt(agent, { input: "Now explain it." });
39
- console.log((await Actions.turn.getResult(followOn)).finalMessage);
42
+ const followResult = await Actions.turn.getResult(followOn);
43
+ console.log(followResult.finalMessage);
40
44
  followOn.dispose();
45
+ followResult.dispose();
46
+ result.dispose();
41
47
  await branch.session.shutdown();
42
48
  await agent.session.shutdown();
43
49
  ```
44
50
 
51
+ Transports are explicit, immutable configurations, like viem v3 transports:
52
+
53
+ ```js
54
+ Transport.openAi({ apiKey, websocketUrl });
55
+ Transport.chatGpt({ subscription });
56
+ Transport.mpp({ session: paymentSession });
57
+ Transport.managed({ agent: { create: true } });
58
+ Transport.managed({ agent: { id: retainedAgentId } });
59
+ ```
60
+
61
+ Managed identity is always explicit. `{ create: true }` provisions one new
62
+ account-owned durable Agent; `{ id }` eagerly verifies and opens that existing
63
+ Agent. Omitting `agent` never creates a durable resource. Both return the same
64
+ `sessionId`, `events.watch()`, `turn.prompt()` / Turn, `dispose()`, and
65
+ `session.shutdown()` lifecycle used by local transports. Managed shutdown
66
+ closes this client and any reverse tool attachment; it does not delete the
67
+ durable Agent.
68
+
69
+ Choose the entrypoint by execution owner:
70
+
71
+ - `nanocodex/browser` creates and owns a package module Worker. Its options are
72
+ structured-clone-safe and its default harness includes the browser workspace.
73
+ - `nanocodex/host` runs in the current Web API isolate. Use it inside a
74
+ caller-owned browser Worker, Cloudflare Worker, Vercel Function, or similar
75
+ host when transports, tools, filesystems, or durability contain functions.
76
+ - `nanocodex/node` runs in the current Node process with Node host adapters.
77
+
78
+ The browser transports additionally expose `Transport.hostManaged(...)` for a
79
+ Worker, Durable Object, or application proxy that owns rotating credentials.
80
+ Authentication modes are constructors rather than a union of mutually
81
+ exclusive fields on `Agent.create`.
82
+
83
+ ### Compose and place tools
84
+
85
+ `createTools` owns one deterministic tool recipe. Custom functions, a portable
86
+ workspace, and MCP are composed once; placement is selected afterward. Pass the
87
+ recipe to an in-process Node or Web API host, or reverse-attach it to a managed
88
+ agent target:
89
+
90
+ For a reverse machine attachment, `attachmentId` is its stable safe-ASCII source
91
+ identity (at most 123 bytes), and must equal the `id` of its sole non-secret
92
+ `machines` entry. Multiple machines may stay attached through independent
93
+ `Tools` runtimes; reconnect one runtime to replace that machine route while the
94
+ durable managed agent stays alive. Generic attachments may omit machine metadata.
95
+
96
+ ```js
97
+ import { createTools } from "nanocodex";
98
+ import { Agent, Transport, Workspace } from "nanocodex/node";
99
+ import WebSocket from "ws";
100
+
101
+ const workspace = await Workspace.open({ path: process.cwd() });
102
+ const tools = await createTools({
103
+ attachmentId: "laptop",
104
+ machines: [{
105
+ id: "laptop",
106
+ name: "My laptop",
107
+ workspace: process.cwd(),
108
+ capabilities: ["filesystem", "native-shell"],
109
+ }],
110
+ workspace,
111
+ tools: {
112
+ lookup_issue: {
113
+ description: "Read one issue from the application database.",
114
+ parameters: {
115
+ type: "object",
116
+ properties: { id: { type: "string" } },
117
+ required: ["id"],
118
+ additionalProperties: false,
119
+ },
120
+ handler: ({ id }) => issues.get(id),
121
+ },
122
+ },
123
+ mcp: {
124
+ docs: { url: "https://mcp.example.test" },
125
+ },
126
+ });
127
+
128
+ const agent = await Agent.create({
129
+ transport: Transport.managed({
130
+ agent: { id: agentId },
131
+ baseUrl: managedOrigin,
132
+ apiKey,
133
+ toolsTransport: (target, options) => new WebSocket(target, {
134
+ headers: options.headers,
135
+ }),
136
+ }),
137
+ tools,
138
+ });
139
+
140
+ // On shutdown:
141
+ await agent.session.shutdown();
142
+ ```
143
+
144
+ The managed target retains credentials in a private transport closure; the API
145
+ key is not embedded in the endpoint or serializable target data. While the
146
+ attachment is live, an exact same-name attached tool wins over the cloud tool.
147
+ After detach, the cloud definition is immediately eligible again. Definition
148
+ parity is validated before the attached catalog becomes active, and calls
149
+ already admitted retain their pinned placement.
150
+
151
+ `Tools` has one Agent owner and owns the lifecycle of its MCP runtime and
152
+ reverse attachments. Local transports host the recipe in process; a managed
153
+ transport starts a bounded reverse-attachment supervisor while the durable
154
+ Agent remains available through its cloud tools. A successful catalog
155
+ acknowledgement upgrades later admissions to the attached placement. A second
156
+ Agent host rejects the same value. Do not also supply legacy top-level
157
+ workspace or MCP configuration to an Agent that already receives them through
158
+ `Tools`.
159
+
160
+ Browser consumers can attach Codex's ChatGPT Realtime voice lifecycle to the
161
+ same retained Agent. The resource owns microphone, speaker, WebRTC, sideband,
162
+ and delegation cleanup; stopping voice does not cancel an active coding turn.
163
+ Snapshots update each speaker's transcript row as speech arrives, using a stable
164
+ `id` and `isPartial` flag. Completion replaces that row. `transcript.delta` events
165
+ carry the current partial text; `transcript` events retain completed-turn semantics.
166
+ Internal Realtime envelopes are projected into spoken text before publication.
167
+ Transcript updates continue while a delegation waits for durable admission.
168
+ Snapshots retain the latest 200 rows across stop/start. Subscribe to events if
169
+ an application needs its own longer transcript history.
170
+
171
+ Both local and managed browser Agents use the shared Rust/WASM client-managed
172
+ handoff policy. Only a completed final answer from the current spoken request
173
+ is submitted for speech. Commentary stays private; superseded, oversized, or
174
+ unconfirmed answers remain visible as `recovered` transcript rows and
175
+ `answer.recovered` events. Workspace and conversation history are not injected
176
+ into call startup. Browser media uses WebRTC echo cancellation, noise suppression,
177
+ and gain control; the native audio helper is used by native clients.
178
+
179
+ `start()` resolves after the media peer and backend session are ready. A media
180
+ connection timeout gets one retry after the first call has been closed.
181
+
182
+ The one-operation-at-a-time action surface is the canonical imperative API:
183
+
184
+ ```js
185
+ import { Actions } from "nanocodex/browser";
186
+
187
+ const voice = Actions.voice.create(agent);
188
+
189
+ await Actions.voice.start(voice); // defaults to Codex's `cove` voice
190
+ Actions.voice.setMuted(voice, true); // also works while connecting
191
+ Actions.voice.toggleMuted(voice);
192
+ const { microphoneLevel, speakerLevel } = Actions.voice.getSnapshot(voice);
193
+
194
+ // Fence old speech before submitting typed input. The shared terminal does this.
195
+ await Actions.voice.noteTypedInput(voice);
196
+ await agent.turn.prompt("Check the tests.");
197
+ await Actions.voice.stop(voice);
198
+ await Actions.voice.destroy(voice);
199
+ ```
200
+
201
+ Subscription voice preferences use the same Rust policy in browsers and native
202
+ apps. `start` and `create` accept `voice`, `instructions`, `pace` (`slow`,
203
+ `natural`, `fast`), `updates` (`auto`, `results`, `silent`), and optional
204
+ `acknowledgements`. Pace and style are speaking instructions.
205
+ `updates: "silent"` retains coding results as text without automatic speech.
206
+ `handoffMode` remains accepted for compatibility; browser client-managed
207
+ handoffs deliver completed finals and do not stream intermediate commentary.
208
+ Apply changed settings by stopping and starting a call. The shared terminal provides a saved
209
+ Voice settings panel with an Apply and reconnect action.
210
+
211
+ During an active call, `Actions.voice.speak(voice, text)` queues explicit speech,
212
+ `appendText(voice, text, { role: "developer" })` adds text using Codex's
213
+ subscription adapter (which treats all roles as context), and
214
+ `appendContext(voice, text)` adds background commentary without
215
+ requesting speech. Context and speech are split into provider-sized messages.
216
+ 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.
220
+
221
+ `Voice.create(...)` remains the equivalent namespaced resource constructor, and
222
+ `Voice.voices` is the exact ChatGPT V3 voice catalog. The constructor accepts a
223
+ 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
226
+ credentials or places its reusable grant bearer in a WebSocket URL.
227
+
228
+ ### Durable Cloudflare Agent
229
+
230
+ `nanocodex/cloudflare` is the standard Durable Object consumer. It keeps the
231
+ host transport, SQLite durable state, private runtime identity, event persistence,
232
+ hibernatable socket fan-out, and cursor replay inside the adapter:
233
+
234
+ ```js
235
+ import { DurableObject } from "cloudflare:workers";
236
+ import { Agent } from "nanocodex/cloudflare";
237
+
238
+ export class CodingAgent extends DurableObject {
239
+ #ready;
240
+
241
+ constructor(context, env) {
242
+ super(context, env);
243
+ this.#ready = Agent.create(this, {
244
+ instructions: "You are a focused coding agent.",
245
+ });
246
+ }
247
+
248
+ async prompt(input) {
249
+ const agent = await this.#ready;
250
+ const turn = agent.turn.prompt({ input });
251
+ let result;
252
+ try {
253
+ result = await turn.result();
254
+ return result.finalMessage;
255
+ } finally {
256
+ try {
257
+ result?.dispose();
258
+ } finally {
259
+ turn.dispose();
260
+ }
261
+ }
262
+ }
263
+
264
+ async fetch(request) {
265
+ return (await this.#ready).events.connect(request);
266
+ }
267
+ }
268
+ ```
269
+
270
+ The returned value is the normal typed Agent: follow-on prompts reuse its owned
271
+ history, and results remain independently awaitable. `events.connect(request)`
272
+ is only a read-only AgentEvent WebSocket surface; it does not define prompt,
273
+ membership, room, quota, or application routing policy. Event frames are
274
+ `{ cursor, event }`. Replay is bounded; a far-behind client can receive
275
+ `{ type: "replay_paused", cursor, latest_cursor }` followed by close code
276
+ `1013`, then continues by reconnecting with that pause cursor as
277
+ `?cursor=<decimal>`.
278
+
279
+ Cloudflare Agents default to direct tool mode because Workers prohibit dynamic
280
+ `eval`/`new Function`. Caller-defined tools therefore work without a code
281
+ evaluator. Select `toolMode: "code"` only when also supplying an evaluator that
282
+ 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
287
+ `Subagents.create({ maxConcurrency })` in `tools` to set an explicit finite
288
+ concurrency limit. Active subagent turns are unlimited by default.
289
+
290
+ Each Durable Object persists a private runtime identity in its own SQLite
291
+ storage and derives its state identity from it, so multiple objects in one
292
+ isolate remain independent and eviction reuses the same identity. Before
293
+ replacing an Agent inside a still-live object, await `agent.session.shutdown()`;
294
+ deleting the Durable Object and its retained event/state rows remains an
295
+ application-owned lifecycle operation.
296
+
297
+ Internally this constructor uses `Transport.hostManaged` and an exact brokered
298
+ Responses WebSocket. `authMode` is required and accepts only `"api_key"` or
299
+ `"chatgpt"`; URLs and non-secret placeholders are fixed. `Agent.create` awaits
300
+ the private binding's WebSocket upgrade, so a missing binding or a broker whose
301
+ single policy does not match the selected mode rejects startup. The managed
302
+ Worker API deliberately has no provider-key, token, transport, or durability
303
+ option.
304
+
305
+ The managed Worker needs only the Durable Object and private broker bindings;
306
+ the broker's separate Wrangler configuration owns the real provider secret:
307
+
308
+ ```jsonc
309
+ {
310
+ "services": [{ "binding": "EGRESS", "service": "my-private-egress-broker" }],
311
+ "durable_objects": {
312
+ "bindings": [{ "name": "AGENTS", "class_name": "CodingAgent" }]
313
+ },
314
+ "migrations": [{ "tag": "v1", "new_sqlite_classes": ["CodingAgent"] }],
315
+ "vars": { "NANOCODEX_AUTH_MODE": "chatgpt" }
316
+ }
317
+ ```
318
+
319
+ Do not put `OPENAI_API_KEY`, OAuth material, account IDs, or relay capabilities
320
+ in this managed Worker configuration. A private Service Binding is a
321
+ controlled-code boundary, so the separately deployed broker must still enforce
322
+ one exact destination, one matching credential policy, placeholder replacement,
323
+ header allowlisting, and no public route.
324
+
325
+ Task-tree orchestration is an optional extension over the core agent. Both
326
+ native and WASM consumers run the same Rust implementation and receive the
327
+ same seven tools: `spawn_agent`, `submit_result`, `send_agent_message`,
328
+ `list_agents`, `wait_agent`, `interrupt_agent`, and `close_agent`.
329
+
330
+ Inside a caller-owned Worker or server isolate, host capabilities stay as
331
+ ordinary functions without crossing another compatibility protocol:
332
+
333
+ ```js
334
+ import { Agent, Transport } from "nanocodex/host";
335
+ import nanocodexWasm from "./nanocodex.wasm";
336
+
337
+ const myApplicationTool = {
338
+ name: "lookup_order",
339
+ description: "Look up one order.",
340
+ parameters: {
341
+ type: "object",
342
+ properties: { id: { type: "string" } },
343
+ required: ["id"],
344
+ additionalProperties: false,
345
+ },
346
+ handler: ({ id }) => orders.get(id),
347
+ };
348
+
349
+ const agent = await Agent.create({
350
+ module: nanocodexWasm,
351
+ transport: Transport.hostManaged({
352
+ websocketUrl: "/api/responses",
353
+ createWebSocket: (endpoint) => new WebSocket(endpoint),
354
+ }),
355
+ tools: [myApplicationTool],
356
+ });
357
+ ```
358
+
359
+ `parameters` is optional and defaults to an open object. TypeScript types are
360
+ erased at runtime, so provide JSON Schema only when the model needs a precise
361
+ argument contract, as `lookup_order` does above.
362
+
363
+ ## Standard web and browser tools
364
+
365
+ `nanocodex/tools` contains composable named tools rather than another agent or
366
+ runtime. Each factory returns an entry that can sit beside application tools
367
+ and Rust/WASM extensions in the same array:
368
+
369
+ ```js
370
+ import { Agent, Transport } from "nanocodex/host";
371
+ import {
372
+ dataset,
373
+ imageGeneration,
374
+ updatePlan,
375
+ web,
376
+ } from "nanocodex/tools";
377
+
378
+ const agent = await Agent.create({
379
+ transport: Transport.hostManaged({
380
+ websocketUrl: "/api/responses",
381
+ createWebSocket: (endpoint) => new WebSocket(endpoint),
382
+ }),
383
+ tools: [
384
+ web(),
385
+ dataset(),
386
+ imageGeneration({
387
+ recentImages: (sessionId, count) => images.get(sessionId).slice(-count),
388
+ rememberImage: (sessionId, imageUrl) => images.get(sessionId).push(imageUrl),
389
+ }),
390
+ updatePlan(),
391
+ myApplicationTool,
392
+ ],
393
+ });
394
+ ```
395
+
396
+ The web and image factories use the canonical OpenAI/Codex tool names, argument
397
+ schemas, bounds, and image-edit modes, and normalize common malformed model
398
+ arguments before dispatch. In a browser, they default to the same-origin
399
+ `/api/tools/web-search` and `/api/tools/image-generation` routes. The host owns
400
+ only a bounded JSON endpoint, credentials, authorization, and persistence.
401
+ `web(...)` posts `{ commands, session_id, model }`, where `model` is the
402
+ effective model of the invoking root or subagent; `imageGeneration(...)` posts
403
+ `{ images, prompt }`. The host owns model authorization and may ignore or
404
+ override this value. Pass `url` when the host route lives elsewhere.
405
+
406
+ `dataset()` runs entirely in the caller and inspects public HTTPS Parquet,
407
+ uncompressed JSONL, and Hugging Face datasets. It opens a session-scoped handle,
408
+ returns schema metadata, and supports projection and filtering queries without
409
+ hard row or offset ceilings. Input and output bytes remain bounded; partial
410
+ results return an opaque `nextCursor` that retains the query and resumes from a
411
+ physical Parquet row batch or JSONL byte position. Parquet uses HTTP range reads
412
+ and predicate pushdown where possible; JSONL scans incrementally and requires
413
+ byte-range support for cursor continuation. The implementation, Parquet reader,
414
+ and non-Snappy codecs load only after the model first calls the tool. Direct URLs
415
+ must allow browser CORS, and Parquet servers must support byte ranges.
416
+ Consumers that only need this capability can import `dataset` from the smaller
417
+ `nanocodex/tools/dataset` leaf entry.
418
+
419
+ ```js
420
+ const datasets = dataset();
421
+ const opened = await datasets.handler({
422
+ operation: "open",
423
+ source: {
424
+ kind: "huggingface",
425
+ dataset: "openai/gsm8k",
426
+ config: "main",
427
+ split: "train",
428
+ },
429
+ }, { sessionId: "thread-1" });
430
+
431
+ const page = await datasets.handler({
432
+ operation: "query",
433
+ dataset_id: opened.datasetId,
434
+ columns: ["question", "answer"],
435
+ filters: [{ column: "question", op: "contains", value: "how many" }],
436
+ limit: 5,
437
+ }, { sessionId: "thread-1" });
438
+
439
+ if (page.nextCursor) {
440
+ await datasets.handler({
441
+ operation: "query",
442
+ dataset_id: opened.datasetId,
443
+ cursor: page.nextCursor,
444
+ limit: 5,
445
+ }, { sessionId: "thread-1" });
446
+ }
447
+ ```
448
+
449
+ This same adapter works inside a Cloudflare Worker or Durable Object:
450
+
451
+ ```js
452
+ import { Agent, Transport } from "nanocodex/host";
453
+ import { web } from "nanocodex/tools";
454
+
455
+ const agent = await Agent.create({
456
+ module: env.NANOCODEX_WASM,
457
+ transport: Transport.hostManaged({
458
+ websocketUrl: env.RESPONSES_WEBSOCKET_URL,
459
+ createWebSocket: (endpoint) => new WebSocket(endpoint),
460
+ }),
461
+ toolMode: "direct",
462
+ tools: [
463
+ web({
464
+ url: env.WEB_TOOL_URL,
465
+ headers: { authorization: `Bearer ${env.WEB_TOOL_TOKEN}` },
466
+ }),
467
+ ],
468
+ });
469
+ ```
470
+
471
+ For a caller-owned browser Worker, `browser(...)` composes the same tools with
472
+ one persistent OPFS workspace and a lazy WASM-backed shell (Python through
473
+ Pyodide, C/C++ through wasm-clang, plus browser Git and bounded commands):
474
+
475
+ ```js
476
+ import { Agent } from "nanocodex/host";
477
+ import { browser } from "nanocodex/tools/browser";
478
+
479
+ const runtime = await browser({
480
+ threadId,
481
+ recentImages,
482
+ rememberImage,
483
+ });
484
+
485
+ const agent = await Agent.create({
486
+ transport,
487
+ filesystem: runtime.filesystem,
488
+ instructions: runtime.instructions,
489
+ executionEnvironment: {
490
+ currentDate,
491
+ timezone,
492
+ projectInstructions: runtime.projectInstructions,
493
+ },
494
+ tools: runtime.tools,
495
+ });
496
+ ```
497
+
498
+ `browser(...)` runs in a browser Worker because OPFS is a browser capability;
499
+ use the individual factories in server-side Cloudflare Workers. Vite integration
500
+ is provided separately by `nanocodex-vite`.
501
+
502
+ The browser composition includes native `browseX` public X browsing, advertised
503
+ by `accountInfo().apis` without an X connector. The embedding app serves
504
+ `/api/tools/x/browse` and `/api/tools/x/convert`; Nanocodex's account app forwards
505
+ these requests to the private X Worker.
506
+
507
+ The browser composition includes `render_artifact` as a normal typed tool. For
508
+ other hosts, compose the same factory with any workspace implementing the
509
+ Nanocodex workspace contract:
510
+
511
+ ```js
512
+ import { artifact, web } from "nanocodex/tools";
513
+
514
+ const tools = [
515
+ web({ url: env.WEB_TOOL_URL }),
516
+ artifact({ workspace }),
517
+ ];
518
+ ```
519
+
520
+ The artifact factory performs no dynamic evaluation and is safe to load in a
521
+ Cloudflare Worker. Browser hosts additionally install the exact iframe syntax
522
+ validator. The model calls `tools.render_artifact({ id, title, source })` from
523
+ Code Mode, or `render_artifact` directly when the host selects direct mode; no
524
+ artifact CLI is installed. Artifact capacity is host-owned: the binding adds no
525
+ byte, source-length, ID-length, or document-count policy limits.
526
+
527
+ Application tools may provide `outputSchema` alongside `parameters`. The
528
+ binding serializes it to Rust's `output_schema`, so Code Mode receives the same
529
+ generated TypeScript return declaration as native Codex tools instead of
530
+ guessing result fields:
531
+
532
+ ```js
533
+ const execCommand = {
534
+ name: "exec_command",
535
+ description: "Run a command.",
536
+ parameters: { type: "object", properties: { cmd: { type: "string" } }, required: ["cmd"] },
537
+ outputSchema: {
538
+ type: "object",
539
+ properties: { output: { type: "string" }, wall_time_seconds: { type: "number" } },
540
+ required: ["output", "wall_time_seconds"],
541
+ additionalProperties: false,
542
+ },
543
+ handler: runCommand,
544
+ };
545
+ ```
546
+
547
+ This is what loading a Rust-written tool from JavaScript looks like here.
548
+ `nanocodex-subagents` is statically linked into `nanocodex.wasm`; every JS
549
+ `Agent.create(...)` installs it by default. Spreading `Subagents.create()` into
550
+ `tools` overrides its maximum concurrency and contributes one opaque extension
551
+ entry, not seven JavaScript handlers. Inside the binding, Rust creates one
552
+ shared registry and installs fresh tools for every root, spawn, and fork:
553
+
554
+ ```rust,ignore
555
+ let (registry, control, updates) = nanocodex_subagents::channel(max_concurrency);
556
+ let tools = Tools::builder().without_defaults().build()?;
557
+ let tools = nanocodex_tools::embedded::bind_host(tools, javascript_host);
558
+ let (agent, events) = Nanocodex::builder(openai)
559
+ .tools_factory(move |handle| {
560
+ nanocodex_subagents::install_tools(tools.clone(), handle, registry.clone())
561
+ })
562
+ .build()?;
563
+ ```
564
+
565
+ This is deliberately static composition, not a generic runtime loader for an
566
+ arbitrary second `.wasm` plugin. A custom Rust extension is linked into the
567
+ binding crate at build time and exposed by a small branded JS configuration;
568
+ adding a dynamic component ABI would be a separate feature with a much larger
569
+ contract and runtime cost.
570
+
571
+ The root owns the task tree. `agent.session.shutdown()` closes every child
572
+ before stopping the root driver; applications do not maintain a parallel JS
573
+ scheduler or reimplement the communication tools.
574
+
575
+ ## Persistent workspaces
576
+
577
+ Runtime-specific `Workspace` adapters give an embedding application one file
578
+ contract for both local browser kernels and Node kernels. The browser adapter
579
+ uses the origin-private file system (OPFS), so reopening the same stable name
580
+ after a Worker, page, or agent-session restart reuses its files. The Node
581
+ adapter roots the same operations in an ordinary directory and refuses path
582
+ traversal and symbolic-link escapes.
583
+
584
+ ```js
585
+ import { Workspace } from "nanocodex/browser/workspace";
586
+ import { Agent, Transport } from "nanocodex/host";
587
+
588
+ const workspace = await Workspace.open({ name: "my-notebook" });
589
+ const agent = await Agent.create({
590
+ transport: Transport.hostManaged({
591
+ websocketUrl: "/api/responses",
592
+ createWebSocket: (endpoint) => new WebSocket(endpoint),
593
+ }),
594
+ filesystem: workspace,
595
+ });
596
+
597
+ await workspace.writeFile("README.md", "# Durable browser workspace\n");
598
+ console.log(await workspace.list(".", { recursive: true }));
599
+ ```
600
+
601
+ The returned handle is application-owned and remains usable by a file browser,
602
+ editor, upload/download surface, or another agent session. `Workspace.tools`
603
+ exposes bounded `list_files`, `read_file`, `write_file`, `make_directory`, and
604
+ `delete_file` operations through the normal caller-defined tool boundary. It
605
+ does not add a fake browser shell.
606
+
607
+ Node uses the same shape with a real directory:
608
+
609
+ ```js
610
+ import { Agent, Transport, Workspace } from "nanocodex/node";
611
+
612
+ const workspace = await Workspace.open({ path: process.cwd() });
613
+ const agent = await Agent.create({
614
+ transport: Transport.openAi({ apiKey: process.env.OPENAI_API_KEY }),
615
+ filesystem: workspace,
616
+ });
617
+ ```
618
+
45
619
  Node and browser applications can instead pay through MPP without an OpenAI
46
620
  API key. Pass an MPP session with a `ws(endpoint)` method; an `mppx` Tempo
47
621
  session manager has this shape. Nanocodex defaults the socket to
48
622
  `wss://openai.mpp.tempo.xyz/v1/responses` when `mpp` is present.
49
623
 
50
624
  ```js
51
- import { Agent } from "nanocodex/node";
625
+ import { Agent, createTempoProviderFromAccounts, Transport } from "nanocodex/node";
52
626
  import { Expiry } from "accounts";
53
627
  import { Provider } from "accounts/cli";
54
- import { tempo } from "mppx/client";
55
628
  import { parseUnits } from "viem";
56
629
  import { connect } from "viem/experimental/erc7846";
57
630
  import WebSocket from "ws";
@@ -77,28 +650,40 @@ const account = await provider.store.accessKeys.select({
77
650
  });
78
651
  if (!account) throw new Error("Tempo account has no usable access key");
79
652
  console.error(`Tempo access-key signer: ${account.accessKeyAddress}`);
80
- const mpp = tempo.session.manager({
81
- account,
82
- autoSwap: { tokenIn: [pathUsd], slippage: 1 },
83
- bootstrap: true,
84
- client: provider.getClient(),
85
- webSocket: WebSocket,
86
- maxDeposit: "0.05",
87
- topUpAmount: "0.05",
653
+ const tempoProvider = await createTempoProviderFromAccounts({
654
+ wallet: provider,
655
+ accessKey: account.accessKeyAddress,
656
+ policy: {
657
+ autoSwap: { tokenIn: [pathUsd], slippage: 1 },
658
+ maxDeposit: "0.05",
659
+ topUpAmount: "0.05",
660
+ },
661
+ session: { bootstrap: true, webSocket: WebSocket },
88
662
  });
663
+ const mpp = tempoProvider.session;
89
664
 
90
- const agent = await Agent.create({ mpp, thinking: "none", fastMode: true, tools });
665
+ const agent = await Agent.create({
666
+ transport: Transport.mpp({ session: tempoProvider }),
667
+ thinking: "none",
668
+ fastMode: true,
669
+ tools,
670
+ });
91
671
  const events = agent.events.watch();
92
672
  const unwatch = events.onEvent((event) => {
93
673
  process.stdout.write(`${JSON.stringify(event)}\n`);
94
674
  });
95
675
  let turn;
676
+ let result;
96
677
  try {
97
678
  turn = agent.turn.prompt({ input: "Build the thing." });
98
- const result = await turn.result();
679
+ result = await turn.result();
99
680
  console.error(result.finalMessage);
100
681
  } finally {
101
- turn?.dispose();
682
+ try {
683
+ result?.dispose();
684
+ } finally {
685
+ turn?.dispose();
686
+ }
102
687
  unwatch();
103
688
  events.off();
104
689
  const cleanupErrors = [];
@@ -122,18 +707,114 @@ try {
122
707
  The application still owns its wallet, deposit policy, persisted payment
123
708
  channel store, and final settlement. Keep the manager alive to reuse its channel
124
709
  across agents, and supply mppx `channelStore` for reuse after a process or page
125
- restart. Nanocodex never closes a caller-owned MPP session. `apiKey` and `mpp`
126
- are mutually exclusive.
710
+ restart. Nanocodex never closes a caller-owned MPP session.
711
+ `createTempoProviderFromAccounts({ wallet, ... })`
712
+ accepts any provider returned by Accounts SDK `Provider.create(...)`, regardless
713
+ of its wallet adapter, and constructs both payment paths from that provider's
714
+ adapter-neutral `getMppxParameters()` contract. The lower-level
715
+ `createTempoProvider({ session, payment })` remains available when the
716
+ 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
719
+ `fetch`; Mercator's paid REST handoffs use that same method rather than a second
720
+ wallet or payment configuration. Its MCP transport remains wrapped at the MCP
721
+ protocol layer, so browser requests do not need an `Accept-Payment` CORS header.
722
+ Browser Connect consumers send paid REST handoffs through the Connect API's
723
+ fixed Mercator relay because Mercator's job endpoint is not itself CORS-enabled;
724
+ the relay preserves MPP challenges, credentials, and receipts but never signs.
725
+ Passing a generic `MppSession`, an OpenAI key, or ChatGPT host auth does not
726
+ initialize Mercator. Pass `mcp: false` to opt out explicitly.
727
+
728
+ Remote Streamable HTTP MCP servers are configured directly on the agent. The
729
+ JavaScript binding uses the official MCP SDK transport, keeps remote tools
730
+ deferred, and mirrors native Nanocodex exposure: the initial Responses request
731
+ contains provider-native `tool_search`, while canonical `mcp__<server>__<tool>`
732
+ functions are callable only below Code Mode. Code Mode also exposes
733
+ `tools.tool_search`, so one cell can discover a deferred tool and invoke the
734
+ returned canonical name. Search results return loadable namespaces for the next
735
+ model request; remote tools never become a flat set of top-level model-visible
736
+ calls.
737
+
738
+ MPP-enabled MCP uses MPPx's in-place `McpClient.wrap`. Ordinary paid HTTP uses
739
+ `Mppx.create(...).fetch`. The public `tempo()` method is installed in both and
740
+ supports Tempo charge and session challenges, so paid services composed behind
741
+ Mercator use the same signer and spending policy as the model:
742
+
743
+ ```js
744
+ const mcpMethod = tempo({
745
+ account,
746
+ channelStore,
747
+ getClient: () => provider.getClient(),
748
+ maxDeposit: "0.05",
749
+ topUpAmount: "0.05",
750
+ });
751
+
752
+ const agent = await Agent.create({
753
+ transport: Transport.mpp({
754
+ session: createTempoProvider({
755
+ session: mpp,
756
+ payment: { methods: [mcpMethod] },
757
+ }),
758
+ }),
759
+ });
760
+ ```
761
+
762
+ Explicit `mcp` entries are merged over the Tempo defaults, so an application
763
+ can replace `mercator` or add other servers without rebuilding the provider.
764
+
765
+ Each server also accepts `headers`, `fetch`, allow/deny tool lists, a timeout,
766
+ or an already initialized MCP SDK-compatible `client`. Nanocodex closes clients
767
+ it creates and leaves caller-owned clients open. Connection failures are
768
+ reported by `tool_search` so one unavailable server does not prevent the agent
769
+ from starting.
770
+
771
+ Code Mode is the default. Model-facing `exec` cells can yield with a first-line
772
+ `// @exec: {"yield_time_ms": 1000, "max_output_tokens": 1000}` directive or
773
+ `yield_control()`. The model resumes the returned cell ID through `wait`, which
774
+ returns only new output and can terminate the cell. Cells belong to their agent
775
+ session and are invalidated when the host shuts down; a persisted `wait` never
776
+ restarts missing work. Embedded cells retain ownership of all nested tool calls
777
+ until they finish or are cancelled.
778
+
779
+ Custom evaluators receive `audio`, `notify`, `yield_control`, `setTimeout`, and
780
+ `clearTimeout` alongside the existing globals in `CodeEvaluatorEnvironment`.
781
+ Forward those helpers into the guest environment to preserve the model-visible
782
+ contract. `image` accepts individual MCP image blocks and honors explicit detail
783
+ before MCP metadata; `audio` accepts MCP audio blocks. Both accept data URLs.
784
+
785
+ Runtimes whose content-security policy rejects `eval`/`new Function` can supply
786
+ a Code Mode evaluator. `createQuickJsEvaluator` accepts an asyncified
787
+ `quickjs-emscripten-core` module, serializes Asyncify execution, and exposes only
788
+ the standard Nanocodex Code Mode globals across the interpreter boundary. This
789
+ keeps deferred MCP plus Code Mode functional in Cloudflare Workers:
790
+
791
+ ```js
792
+ import asyncVariant from "@jitl/quickjs-wasmfile-release-asyncify";
793
+ import { Agent, createQuickJsEvaluator, createTempoProvider, Transport } from "nanocodex/host";
794
+ import { newQuickJSAsyncWASMModuleFromVariant } from "quickjs-emscripten-core";
795
+
796
+ const quickJs = await newQuickJSAsyncWASMModuleFromVariant(asyncVariant);
797
+ const agent = await Agent.create({
798
+ transport: Transport.mpp({ session: tempoProvider }),
799
+ // module and mcp omitted here
800
+ codeEvaluator: createQuickJsEvaluator(quickJs),
801
+ });
802
+ ```
803
+
804
+ Cloudflare requires the QuickJS `.wasm` file to be statically imported and
805
+ passed with `newVariant(..., { wasmModule })`; the complete deployment is in
806
+ `examples/cloudflare-fetch-mcp`.
127
807
 
128
808
  Completed results can be persisted and resumed by a fresh Node or browser
129
809
  agent:
130
810
 
131
811
  ```js
132
- const snapshot = result.snapshot;
812
+ const snapshot = await result.snapshot();
813
+ result.dispose();
133
814
  await agent.session.shutdown();
134
815
 
135
816
  const resumed = await Agent.create({
136
- apiKey: process.env.OPENAI_API_KEY,
817
+ transport: Transport.openAi({ apiKey: process.env.OPENAI_API_KEY }),
137
818
  resume: snapshot,
138
819
  tools,
139
820
  });
@@ -145,13 +826,158 @@ so the first resumed request safely replays the committed conversation. Resume
145
826
  with the same instructions and tool definitions, and release the original
146
827
  agent before handing its snapshot to another writer.
147
828
 
829
+ For crash recovery inside a turn, provide the generic durability host instead
830
+ of manually persisting snapshots. The host stores one opaque Rust state value;
831
+ model replay, tool ambiguity, operation deduplication, and checkpoint recovery
832
+ remain in Rust/WASM:
833
+
834
+ ```js
835
+ import { Agent, Transport } from "nanocodex/host";
836
+
837
+ const agent = await Agent.create({
838
+ transport: Transport.openAi({ apiKey: process.env.OPENAI_API_KEY }),
839
+ durability: {
840
+ async load(stateId) {
841
+ return database.loadState(stateId);
842
+ },
843
+ async acquire(stateId, { ownerId }) {
844
+ return database.acquireState(stateId, ownerId);
845
+ },
846
+ async replace(stateId, { ownerId, fence, expectedRevision, payload }) {
847
+ return database.compareAndReplace(
848
+ stateId,
849
+ ownerId,
850
+ fence,
851
+ expectedRevision,
852
+ payload,
853
+ );
854
+ // { status: "replaced", revision: "8" }
855
+ // or { status: "conflict", actualRevision: "8" }
856
+ // or { status: "not_committed", message: "transaction rolled back" }
857
+ },
858
+ },
859
+ durabilityId: "customer-agent-123",
860
+ });
861
+
862
+ // Every prompt is durable because the state store is configured. Supply `id`
863
+ // only when an external retry must identify the same logical operation.
864
+ const turn = agent.turn.prompt({ input: "Build the thing." });
865
+ // const turn = agent.turn.prompt({ id: "request-7", input: "Build the thing." });
866
+ let result;
867
+ try {
868
+ result = await turn.result();
869
+ console.log(result.finalMessage);
870
+ } finally {
871
+ try {
872
+ result?.dispose();
873
+ } finally {
874
+ turn.dispose();
875
+ await agent.session.shutdown();
876
+ }
877
+ }
878
+ ```
879
+
880
+ Revisions are unsigned decimal strings so JavaScript preserves Rust's full
881
+ `u64` range. Import `durabilityRevision`, `createMemoryDurabilityStore`,
882
+ `createSqliteDurabilityStore`, and `sqliteDurabilitySchema` from the small
883
+ `nanocodex/durability` leaf. Durable step hosts can carry the memory store's
884
+ `snapshot()` into the next step. SQLite hosts provide one transaction query
885
+ adapter and execute the canonical schema; the platform never interprets the
886
+ opaque Rust state. See `js/managed`,
887
+ `examples/vercel-workflows`, and `examples/rivet-actors` for all three host
888
+ shapes.
889
+
890
+ Cloudflare Durable Objects can bind their colocated SQLite and initialize the
891
+ canonical schema in one call. The adapter is structural and adds no Workers
892
+ runtime dependency:
893
+
894
+ ```js
895
+ import { createCloudflareDurabilityStore } from "nanocodex/durability/cloudflare";
896
+
897
+ const durability = createCloudflareDurabilityStore(this.ctx.storage);
898
+ const agent = await Agent.create({
899
+ module: env.NANOCODEX_WASM,
900
+ transport,
901
+ durability,
902
+ durabilityId: sessionId,
903
+ });
904
+ ```
905
+
906
+ Vercel and other PostgreSQL hosts use `createPostgresDurabilityStore(pool)`
907
+ from `nanocodex/durability/postgres`; connection ownership and secret policy
908
+ remain in the application.
909
+
910
+ The built-in stores can move one stopped agent across providers without
911
+ decoding or rebasing its Rust state. Cloudflare owners should use the adapter's
912
+ lifecycle-safe export instead of reconstructing its private state ID:
913
+
914
+ ```js
915
+ import { Agent as CloudflareAgent } from "nanocodex/cloudflare";
916
+ import { importDurabilityStatePages } from "nanocodex/durability";
917
+ import { createPostgresDurabilityStore } from "nanocodex/durability/postgres";
918
+
919
+ await cloudflareAgent.session.shutdown();
920
+ const pages = [];
921
+ let cursor;
922
+ let to;
923
+ do {
924
+ const page = await CloudflareAgent.exportDurabilityState(durableObjectOwner, {
925
+ from: "0", // exclusive destination revision
926
+ to, // omit once, then repeat the selected inclusive source revision
927
+ cursor,
928
+ });
929
+ pages.push(page);
930
+ to = page.to;
931
+ cursor = page.nextCursor ?? undefined;
932
+ } while (cursor !== undefined);
933
+
934
+ // Send the pages through an authenticated, encrypted operator path.
935
+ const destination = createPostgresDurabilityStore(vercelPostgresPool);
936
+ await importDurabilityStatePages(destination, JSON.parse(JSON.stringify(pages)));
937
+
938
+ const vercelAgent = await Agent.create({
939
+ module: wasmModule,
940
+ transport,
941
+ durability: destination,
942
+ durabilityId: pages[0].stateId,
943
+ });
944
+ ```
945
+
946
+ `from` is exclusive and `to` is inclusive. For a nonzero `from`, load the
947
+ destination once, hash that exact state with `durabilityStateDigest`, and repeat
948
+ the short `fromDigest` on every page request; revision zero's null-state digest
949
+ is implied. Each page carries that SHA-256 lineage digest, so import atomically
950
+ succeeds only if the destination still has the exact revision and payload
951
+ selected at `from`.
952
+ Because `to` is one complete Rust state, no intermediate revision log is
953
+ needed. Export fences the old source owner, and PostgreSQL reconciles lost
954
+ COMMIT responses internally by retrying the identical idempotent request, so
955
+ the API never reports an ambiguous write outcome. Stop source admission before
956
+ the first page and never resume it after cutover begins. Pages can contain
957
+ conversation and tool state, so handle them as secrets. The Vercel example
958
+ includes a WASM integration test that executes the
959
+ same agent Cloudflare → PostgreSQL → Cloudflare, replays committed turn IDs
960
+ without model calls, rebuilds the first new provider request from committed
961
+ history without a previous-response handle, and then continues with new turns
962
+ on each destination.
963
+
964
+ The managed Cloudflare service exposes the same offline cutover at `POST
965
+ /v1/agents/<agent-id>/durability`; the call permanently closes source admission.
966
+ Create a destination with `POST /v1/agents`, an `Idempotency-Key` header, and
967
+ `{ "durability": <archive> }`. The stable key owns resumable receipt adoption.
968
+ The Vercel example accepts that same body at `POST /api/sessions` and exports a
969
+ stopped PostgreSQL state through `POST /api/durability/export` with
970
+ `{ "state_id": <durability-id>, "from": <revision>,
971
+ "fromDigest": <required-for-nonzero-from>, "to": <optional-revision>,
972
+ "cursor": <optional-cursor> }`.
973
+
148
974
  Node embedders whose bundler relocates package assets may compile and pass the
149
975
  web-target artifact explicitly. The runtime still uses the Node host for
150
976
  WebSockets and Code Mode:
151
977
 
152
978
  ```js
153
979
  const module = await WebAssembly.compile(await readFile(wasmAssetPath));
154
- const agent = await Agent.create({ apiKey, module });
980
+ const agent = await Agent.create({ transport: Transport.openAi({ apiKey }), module });
155
981
  ```
156
982
 
157
983
  A Codex-compatible rollout can also be resumed by materializing its committed
@@ -164,9 +990,10 @@ context, and typed history.
164
990
  an owned client decorated with matching domain actions:
165
991
 
166
992
  - `agent.turn.prompt(...)` / `Actions.turn.prompt(agent, ...)`
993
+ - `turn.accepted()` / `Actions.turn.accepted(turn)`
167
994
  - `turn.result()` / `Actions.turn.getResult(turn)`
168
- - `result.snapshot` / `Actions.turn.getSnapshot(result)`
169
- - `result.usage` / `Actions.turn.getUsage(result)`
995
+ - `result.snapshot()` / `Actions.turn.getSnapshot(result)`
996
+ - `result.usage()` / `Actions.turn.getUsage(result)`
170
997
  - `agent.session.fork(...)` / `Actions.session.fork(agent, ...)`
171
998
  - `agent.session.compact()` / `Actions.session.compact(agent)`
172
999
  - `agent.session.setThinking(...)` / `Actions.session.setThinking(agent, ...)`
@@ -175,10 +1002,27 @@ an owned client decorated with matching domain actions:
175
1002
  - `agent.session.spawn()` / `Actions.session.spawn(agent)`
176
1003
  - `agent.events.watch(...)` / `Actions.events.watch(agent, ...)`
177
1004
 
178
- `turn.result()` resolves to a frozen completed `TurnResult`. Its
179
- `finalMessage` is eager; `usage` and `snapshot` cross the WASM boundary lazily
180
- once and are then cached. Historical `fork({ at })` accepts this completed
181
- result, never an unfinished turn or a provider response ID.
1005
+ `turn.accepted()` resolves when Rust has admitted the prompt. A durable agent
1006
+ returns its stable request ID; a custom runtime without durable admission
1007
+ returns `undefined`. Managed HTTP hosts can await this narrow boundary before
1008
+ acknowledging a request without waiting for model execution or materializing a
1009
+ result.
1010
+
1011
+ `turn.result()` resolves to a frozen, opaque completed `TurnResult` handle. Its
1012
+ `finalMessage` is eager. The async `usage()` and `snapshot()` actions materialize
1013
+ immutable values once and cache their promises. A package Worker completes a
1014
+ turn with only the message and hidden result identity; Rust-produced snapshot
1015
+ JSON crosses the Worker boundary only on first demand and is parsed once in the
1016
+ calling isolate. Historical `fork({ at })` consumes the hidden identity directly,
1017
+ never an unfinished turn, clone, snapshot, or provider response ID.
1018
+
1019
+ The completed result owns its identity independently from the `Turn`, so
1020
+ `turn.dispose()` does not invalidate a successful result. Call `result.dispose()`
1021
+ after its last fork/materialization; this releases the retained Worker/native
1022
+ checkpoint and invalidates future `snapshot()`, `usage()`, and historical forks.
1023
+ An undisposed result intentionally keeps its package Worker alive after the last
1024
+ Agent shuts down so its lazy values remain available. Garbage collection is only
1025
+ a fallback for forgotten handles, not deterministic cleanup.
182
1026
 
183
1027
  `turn.dispose()` only releases the JavaScript/WASM handle; like dropping the
184
1028
  Rust `Turn`, it does not cancel accepted work. Await `turn.cancel()` before
@@ -227,82 +1071,97 @@ const extended = agent.extend((client) => ({
227
1071
  extended.inspect.session();
228
1072
  ```
229
1073
 
230
- Browser Workers use the identical shape:
1074
+ The package-owned browser Worker accepts the same transport policy without
1075
+ function-valued callbacks:
231
1076
 
232
1077
  ```js
233
- import { Agent } from "nanocodex/browser";
1078
+ import { Agent, Transport } from "nanocodex/browser";
234
1079
 
235
1080
  const agent = await Agent.create({
236
- websocketUrl: signedOrCookieAuthorizedEndpoint,
237
- createWebSocket(endpoint, sessionId) {
238
- const url = new URL(endpoint);
239
- url.searchParams.set("session_id", sessionId);
240
- return new WebSocket(url);
241
- },
242
- tools,
1081
+ transport: Transport.hostManaged({
1082
+ websocketUrl: signedOrCookieAuthorizedEndpoint,
1083
+ }),
1084
+ threadId,
243
1085
  });
244
1086
  ```
245
1087
 
246
- Server-side Worker runtimes can await a `fetch()`-based WebSocket upgrade. The
247
- third callback argument is a discriminated authorization request plus connection
248
- metadata. With `apiKey`, `authorization` is `"bearer"` and `bearerToken` is
249
- present. With `hostAuth: true`, it is `"host_managed"`; the host must resolve
250
- credentials without exposing them to WASM. Do not retain or log bearer tokens.
251
- Return the socket alone or a descriptor containing response metadata:
1088
+ Caller-owned browser Workers and server isolates import `nanocodex/host` when
1089
+ they need function-valued tools or socket construction. Server-side runtimes
1090
+ can await a `fetch()`-based WebSocket upgrade. The third callback argument is a
1091
+ discriminated authorization request plus connection metadata, including the
1092
+ eager `preconnect` request. With `Transport.openAi`, `authorization` is
1093
+ `"bearer"` and `bearerToken` is present. With `Transport.hostManaged`, it is
1094
+ `"host_managed"`; the host must resolve credentials without exposing them to
1095
+ WASM. Do not retain or log bearer tokens. Return the socket alone or a
1096
+ descriptor containing response metadata:
252
1097
 
253
1098
  ```js
254
- import { Agent } from "nanocodex/browser";
1099
+ import { Agent, Transport } from "nanocodex/host";
255
1100
  import module from "nanocodex/wasm";
256
1101
 
257
1102
  const agent = await Agent.create({
258
- apiKey,
1103
+ transport: Transport.openAi({
1104
+ apiKey,
1105
+ async createWebSocket(endpoint, sessionId, request) {
1106
+ if (request.authorization !== "bearer") {
1107
+ throw new Error("this host requires Nanocodex bearer authorization");
1108
+ }
1109
+ const response = await fetch(endpoint.replace("wss:", "https:"), {
1110
+ headers: {
1111
+ Authorization: `Bearer ${request.bearerToken}`,
1112
+ Upgrade: "websocket",
1113
+ "session-id": sessionId,
1114
+ },
1115
+ });
1116
+ if (!response.webSocket) throw new Error(`upgrade failed: ${response.status}`);
1117
+ response.webSocket.accept();
1118
+ return { socket: response.webSocket, status: response.status };
1119
+ },
1120
+ }),
259
1121
  module,
260
- async createWebSocket(endpoint, sessionId, request) {
261
- if (request.authorization !== "bearer") {
262
- throw new Error("this host requires Nanocodex bearer authorization");
263
- }
264
- const response = await fetch(endpoint.replace("wss:", "https:"), {
265
- headers: {
266
- Authorization: `Bearer ${request.bearerToken}`,
267
- Upgrade: "websocket",
268
- "session-id": sessionId,
269
- },
270
- });
271
- if (!response.webSocket) throw new Error(`upgrade failed: ${response.status}`);
272
- response.webSocket.accept();
273
- return { socket: response.webSocket, status: response.status };
274
- },
275
1122
  });
276
1123
  ```
277
1124
 
278
- `hostAuth` is useful when the embedding runtime owns rotating credentials. The
1125
+ `Transport.hostManaged` is useful when the embedding runtime owns rotating credentials. The
279
1126
  callback can acquire a fresh token, attempt the upgrade, and refresh-and-retry
280
1127
  on 401. Bound and reject upgrade work in the callback: until it returns a
281
- socket, there is no connection handle for Nanocodex to close. `apiKey`,
282
- `hostAuth`, and `mpp` are mutually exclusive.
1128
+ socket, there is no connection handle for Nanocodex to close. Selecting one
1129
+ transport makes authentication modes mutually exclusive by construction.
283
1130
 
284
- After publication, a browser can load the same entrypoint without a package
285
- manager or build step:
1131
+ After publication, a browser can load the current-isolate host without a
1132
+ package manager or build step:
286
1133
 
287
1134
  ```html
288
1135
  <script type="module">
289
- import { Agent } from "https://cdn.jsdelivr.net/npm/nanocodex@0.5.0/browser/index.mjs";
290
- const agent = await Agent.create({ websocketUrl: "/api/responses" });
1136
+ import { Agent, Transport } from "https://cdn.jsdelivr.net/npm/nanocodex@0.6.1/host/index.mjs";
1137
+ const agent = await Agent.create({
1138
+ transport: Transport.hostManaged({
1139
+ websocketUrl: "/api/responses",
1140
+ createWebSocket: (endpoint) => new WebSocket(endpoint),
1141
+ }),
1142
+ });
291
1143
  const turn = agent.turn.prompt({ input: "Hello." });
1144
+ let result;
292
1145
  try {
293
- const result = await turn.result();
1146
+ result = await turn.result();
294
1147
  console.log(result.finalMessage);
295
1148
  } finally {
296
- turn.dispose();
297
- await agent.session.shutdown();
1149
+ try {
1150
+ result?.dispose();
1151
+ } finally {
1152
+ turn.dispose();
1153
+ await agent.session.shutdown();
1154
+ }
298
1155
  }
299
1156
  </script>
300
1157
  ```
301
1158
 
302
1159
  Pin the package version in production. The adjacent WASM file is part of the
303
- npm package and is resolved relative to the browser module. The endpoint must
304
- be authorized by the embedding application because browser WebSockets cannot
305
- attach OpenAI's upgrade authorization header.
1160
+ npm package and is resolved relative to the host module. This no-build path
1161
+ runs in the current page isolate; bundled applications should prefer the
1162
+ package-owned Worker from `nanocodex/browser`. The endpoint must be authorized
1163
+ by the embedding application because browser WebSockets cannot attach OpenAI's
1164
+ upgrade authorization header.
306
1165
 
307
1166
  The owned Rust session retains follow-on history, response state, tool output,
308
1167
  its WebSocket, and stable prompt-cache identity. Typed browser content accepts
@@ -317,3 +1176,10 @@ cd examples/node
317
1176
  npm install
318
1177
  OPENAI_API_KEY=... npm start
319
1178
  ```
1179
+
1180
+ Managed clients can save `Agent.definitions` and `Agent.environments`, select them
1181
+ with `Agent.create({ definitionId, environmentTemplateId, configuration })`, and
1182
+ inspect `agent.configuration()`, `environment()`, `usage()`, `requests()`,
1183
+ `artifacts`, `webhook`, and `requiredActions`. See the
1184
+ [managed configuration and operations guide](../../docs/MANAGED_AGENT_CONFIGURATION.md)
1185
+ for examples, authorization, delivery semantics, and runtime limits.