nanocodex 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +944 -78
- package/actions/events.mjs +14 -25
- package/actions/index.d.mts +1 -0
- package/actions/index.mjs +11 -1
- package/actions/session.d.mts +29 -1
- package/actions/session.mjs +22 -0
- package/actions/turn.d.mts +15 -6
- package/actions/turn.mjs +10 -0
- package/actions/voice.d.mts +26 -0
- package/actions/voice.mjs +54 -0
- package/browser/Agent.d.mts +41 -24
- package/browser/Agent.mjs +7 -84
- package/browser/ChatGptSubscription.d.mts +7 -0
- package/browser/ChatGptSubscription.mjs +19 -0
- package/browser/InlineAgent.mjs +267 -0
- package/browser/Transport.d.mts +80 -0
- package/browser/Transport.mjs +63 -0
- package/browser/Voice.d.mts +86 -0
- package/browser/Voice.mjs +316 -0
- package/browser/VoiceSession.mjs +763 -0
- package/browser/WorkerAgent.d.mts +65 -0
- package/browser/WorkerAgent.mjs +1467 -0
- package/browser/agent.worker.mjs +3 -0
- package/browser/config.d.mts +36 -0
- package/browser/config.mjs +439 -0
- package/browser/engine.mjs +13 -0
- package/browser/harness.mjs +55 -0
- package/browser/host.d.mts +33 -5
- package/browser/host.mjs +320 -26
- package/browser/hostManagedWebSocket.d.mts +14 -0
- package/browser/hostManagedWebSocket.mjs +110 -0
- package/browser/index.d.mts +64 -1
- package/browser/index.mjs +22 -1
- package/browser/indexeddb-durability-store.mjs +210 -0
- package/browser/workspace.d.mts +16 -0
- package/browser/workspace.mjs +107 -0
- package/cloud/Client.d.mts +83 -0
- package/cloud/Client.mjs +231 -0
- package/cloud/Decorator.d.mts +31 -0
- package/cloud/Decorator.mjs +37 -0
- package/cloud/Dialog.d.mts +113 -0
- package/cloud/Dialog.mjs +366 -0
- package/cloud/Errors.d.mts +22 -0
- package/cloud/Errors.mjs +39 -0
- package/cloud/Principal.d.mts +26 -0
- package/cloud/Principal.mjs +88 -0
- package/cloud/RemoteProvider.mjs +88 -0
- package/cloud/Transport.d.mts +47 -0
- package/cloud/Transport.mjs +454 -0
- package/cloud/actions/account.d.mts +9 -0
- package/cloud/actions/account.mjs +19 -0
- package/cloud/actions/agent.d.mts +18 -0
- package/cloud/actions/agent.mjs +429 -0
- package/cloud/actions/connection.d.mts +133 -0
- package/cloud/actions/connection.mjs +580 -0
- package/cloud/actions/grant.d.mts +10 -0
- package/cloud/actions/grant.mjs +12 -0
- package/cloud/actions/index.d.mts +7 -0
- package/cloud/actions/index.mjs +7 -0
- package/cloud/actions/machineUsd.d.mts +23 -0
- package/cloud/actions/machineUsd.mjs +37 -0
- package/cloud/actions/model.d.mts +12 -0
- package/cloud/actions/model.mjs +50 -0
- package/cloud/actions/mpp.d.mts +28 -0
- package/cloud/actions/mpp.mjs +39 -0
- package/cloud/index.d.mts +30 -0
- package/cloud/index.mjs +9 -0
- package/cloud/internal.mjs +533 -0
- package/cloud/mercator.mjs +108 -0
- package/cloud/server/HostPrincipal.d.mts +34 -0
- package/cloud/server/HostPrincipal.mjs +202 -0
- package/cloud/server/index.d.mts +1 -0
- package/cloud/server/index.mjs +1 -0
- package/cloud/types.d.mts +212 -0
- package/cloudflare/Agent.d.mts +136 -0
- package/cloudflare/Agent.mjs +784 -0
- package/cloudflare/egress-subject.mjs +19 -0
- package/cloudflare/egress.d.mts +41 -0
- package/cloudflare/egress.mjs +119 -0
- package/cloudflare/event-socket.mjs +316 -0
- package/cloudflare/index.d.mts +7 -0
- package/cloudflare/index.mjs +6 -0
- package/host/Agent.d.mts +55 -0
- package/host/Agent.mjs +1 -0
- package/host/index.d.mts +71 -0
- package/host/index.mjs +10 -0
- package/index.d.mts +46 -0
- package/index.mjs +11 -0
- package/internal.mjs +673 -61
- package/managed/Agent.d.mts +459 -0
- package/managed/Agent.mjs +1824 -0
- package/managed/ManagedError.d.mts +9 -0
- package/managed/ManagedError.mjs +8 -0
- package/managed/README.md +244 -0
- package/managed/Voice.mjs +209 -0
- package/managed/index.d.mts +22 -0
- package/managed/index.mjs +2 -0
- package/managed/internal.mjs +119 -0
- package/node/Agent.d.mts +35 -8
- package/node/Agent.mjs +116 -25
- package/node/ChatGptSubscription.d.mts +9 -0
- package/node/ChatGptSubscription.mjs +17 -0
- package/node/Transport.d.mts +41 -0
- package/node/Transport.mjs +46 -0
- package/node/host.mjs +99 -17
- package/node/index.d.mts +45 -1
- package/node/index.mjs +16 -1
- package/node/workspace.d.mts +10 -0
- package/node/workspace.mjs +198 -0
- package/package.json +145 -11
- package/pkg-node/nanocodex.d.ts +523 -11
- package/pkg-node/nanocodex.js +2038 -179
- package/pkg-node/nanocodex_bg.wasm.d.ts +100 -29
- package/pkg-web/.nanocodex-bindgen-stamp +6 -0
- package/pkg-web/nanocodex-build.json +1 -0
- package/pkg-web/nanocodex.d.ts +623 -40
- package/pkg-web/nanocodex.js +2031 -178
- package/pkg-web/nanocodex_bg.js +2667 -0
- package/pkg-web/nanocodex_bg.wasm +0 -0
- package/pkg-web/nanocodex_bg.wasm.d.ts +100 -29
- package/pkg-web/nanocodex_worker.js +9 -0
- package/runtime/chatgpt-subscription.mjs +212 -0
- package/runtime/cloudflare-durability-store.d.mts +26 -0
- package/runtime/cloudflare-durability-store.mjs +14 -0
- package/runtime/code-evaluator.worker.mjs +137 -0
- package/runtime/code-runtime.mjs +1 -243
- package/runtime/durability-store.d.mts +85 -0
- package/runtime/durability-store.mjs +752 -0
- package/runtime/durability.mjs +174 -0
- package/runtime/managed-transport.mjs +461 -0
- package/runtime/mcp-runtime.mjs +534 -0
- package/runtime/postgres-durability-store.d.mts +64 -0
- package/runtime/postgres-durability-store.mjs +581 -0
- package/runtime/quickjs-evaluator.d.mts +17 -0
- package/runtime/quickjs-evaluator.mjs +236 -0
- package/runtime/response-controls.mjs +42 -0
- package/runtime/response-lanes.mjs +72 -0
- package/runtime/responses-transport.mjs +22 -0
- package/runtime/subagents.d.mts +107 -0
- package/runtime/subagents.mjs +54 -0
- package/runtime/subscription-store.d.mts +8 -0
- package/runtime/subscription-store.mjs +42 -0
- package/runtime/tempo-provider.d.mts +125 -0
- package/runtime/tempo-provider.mjs +264 -0
- package/runtime/tool-configuration.mjs +1 -0
- package/runtime/tool-router.mjs +1 -0
- package/runtime/utf8.mjs +1 -0
- package/runtime/worker-evaluator.mjs +162 -0
- package/runtime/workspace.d.mts +1 -0
- package/runtime/workspace.mjs +1 -0
- package/tools/Tools.d.mts +56 -0
- package/tools/Tools.mjs +136 -0
- package/tools/artifact.d.mts +1 -0
- package/tools/artifact.mjs +1 -0
- package/tools/attachment.mjs +1 -0
- package/tools/bash.d.mts +1 -0
- package/tools/bash.mjs +1 -0
- package/tools/browser/accountInfo.mjs +795 -0
- package/tools/browser/browserBuffer.mjs +18 -0
- package/tools/browser/browserCompiler.mjs +155 -0
- package/tools/browser/browserEgress.mjs +161 -0
- package/tools/browser/browserPython.mjs +133 -0
- package/tools/browser/browserShell.mjs +1075 -0
- package/tools/browser/browserSprintf.mjs +4 -0
- package/tools/browser/browserSsh.mjs +201 -0
- package/tools/browser/browserZlib.mjs +136 -0
- package/tools/browser/compiler.worker.mjs +160 -0
- package/tools/browser/devTunnelsSshBrowser.mjs +13 -0
- package/tools/browser/index.d.mts +209 -0
- package/tools/browser/index.mjs +174 -0
- package/tools/browser/opfsGit.mjs +248 -0
- package/tools/browser/python.worker.mjs +114 -0
- package/tools/browser/threadGit.mjs +197 -0
- package/tools/browser/unsupportedNodeRsa.mjs +5 -0
- package/tools/browser/workspace.mjs +83 -0
- package/tools/dataset.d.mts +1 -0
- package/tools/dataset.mjs +1 -0
- package/tools/datasetContract.mjs +1 -0
- package/tools/datasetEngine.mjs +1 -0
- package/tools/hostedCatalog.d.mts +1 -0
- package/tools/hostedCatalog.mjs +1 -0
- package/tools/index.d.mts +49 -0
- package/tools/index.mjs +19 -0
- package/tools/namedTool.mjs +1 -0
- package/tools/repository-workspace.d.mts +1 -0
- package/tools/repository-workspace.mjs +1 -0
- package/tools/ssh.d.mts +1 -0
- package/tools/ssh.mjs +1 -0
- package/tools/standard.mjs +1 -0
- package/tools/standardDescriptions.mjs +1 -0
- package/types.d.mts +431 -16
- package/worker/ChatGptSubscription.d.mts +9 -0
- package/worker/ChatGptSubscription.mjs +29 -0
- package/worker/index.d.mts +18 -0
- package/worker/index.mjs +5 -0
- 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
|
|
4
|
-
|
|
5
|
-
`Agent.create(...)
|
|
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
|
-
|
|
25
|
-
console.log(
|
|
26
|
-
console.log(
|
|
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
|
-
|
|
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
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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({
|
|
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
|
-
|
|
679
|
+
result = await turn.result();
|
|
99
680
|
console.error(result.finalMessage);
|
|
100
681
|
} finally {
|
|
101
|
-
|
|
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.
|
|
126
|
-
|
|
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.
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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/
|
|
1099
|
+
import { Agent, Transport } from "nanocodex/host";
|
|
255
1100
|
import module from "nanocodex/wasm";
|
|
256
1101
|
|
|
257
1102
|
const agent = await Agent.create({
|
|
258
|
-
|
|
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
|
-
`
|
|
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.
|
|
282
|
-
|
|
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
|
|
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.
|
|
290
|
-
const agent = await Agent.create({
|
|
1136
|
+
import { Agent, Transport } from "https://cdn.jsdelivr.net/npm/nanocodex@0.6.0/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
|
-
|
|
1146
|
+
result = await turn.result();
|
|
294
1147
|
console.log(result.finalMessage);
|
|
295
1148
|
} finally {
|
|
296
|
-
|
|
297
|
-
|
|
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
|
|
304
|
-
|
|
305
|
-
|
|
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.
|