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.
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 -243
  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/browser/host.mjs CHANGED
@@ -1,31 +1,161 @@
1
- import { createCodeRuntime } from "../runtime/code-runtime.mjs";
1
+ import { createCodeRuntime, toolResult } from "../runtime/code-runtime.mjs";
2
+ import {
3
+ toolRouterBrand,
4
+ toolRouterRuntime,
5
+ toolRuntimeLifecycle,
6
+ } from "../runtime/tool-router.mjs";
7
+ import { utf8ByteLength } from "../runtime/utf8.mjs";
8
+ import { createWorkerEvaluator } from "../runtime/worker-evaluator.mjs";
9
+ import { openHostManagedWebSocket } from "./hostManagedWebSocket.mjs";
2
10
 
3
11
  const DEFAULT_MAX_QUEUED_MESSAGES = 4_096;
4
12
  const DEFAULT_MAX_QUEUED_BYTES = 32 * 1024 * 1024;
5
13
  const DEFAULT_MAX_BUFFERED_SEND_BYTES = 16 * 1024 * 1024;
14
+ const MPP_CLIENT_PROTOCOL_ERROR_CLOSE_CODE = 3008;
6
15
  const WEBSOCKET_OPEN = 1;
7
16
 
8
17
  export function createBrowserHost(options = {}) {
18
+ const toolMode = options.toolMode ?? "code";
19
+ if (toolMode !== "code" && toolMode !== "direct") {
20
+ throw new TypeError("toolMode must be code or direct");
21
+ }
22
+ const toolsRouter = options.tools?.[toolRouterBrand]
23
+ ? options.tools[toolRouterRuntime]
24
+ : undefined;
25
+ const toolsMcp = toolsRouter?.hasSourceKind("mcp") === true;
26
+ if (toolsRouter?.hasSource("workspace") && options.filesystem) {
27
+ throw new TypeError("workspace is already configured in Tools");
28
+ }
29
+ if (toolsMcp && options.mcp) {
30
+ throw new TypeError("MCP is already configured in Tools");
31
+ }
32
+ if ((toolsMcp || options.mcp) && toolMode !== "code") {
33
+ throw new TypeError("remote MCP requires Code Mode");
34
+ }
35
+ const toolsLifecycle = options.tools?.[toolRuntimeLifecycle];
36
+ toolsLifecycle?.available();
9
37
  const WebSocketImpl = options.WebSocketImpl ?? globalThis.WebSocket;
10
38
  const createWebSocket = options.createWebSocket
39
+ ?? (options.hostManagedProtocol && ((endpoint, sessionId, request) =>
40
+ openHostManagedWebSocket(endpoint, sessionId, {
41
+ WebSocketImpl,
42
+ threadId: request.threadId,
43
+ })))
11
44
  ?? (WebSocketImpl && ((endpoint) => new WebSocketImpl(endpoint)));
12
45
  if (!options.mpp && !createWebSocket) {
13
46
  throw new Error("WebSocket is unavailable in this runtime");
14
47
  }
15
48
  const connections = new Map();
16
- const code = createCodeRuntime(options.tools);
17
- const toolMode = options.toolMode ?? "code";
18
- if (toolMode !== "code" && toolMode !== "direct") {
19
- throw new TypeError("toolMode must be code or direct");
49
+ const openingAttempts = new Set();
50
+ const connectingConnections = new Set();
51
+ const codeEvaluator = options.codeEvaluator
52
+ ?? (typeof globalThis.Worker === "function"
53
+ ? createWorkerEvaluator()
54
+ : () => Promise.reject(new Error(
55
+ "browser Code Mode requires a child Worker or an explicit codeEvaluator",
56
+ )));
57
+ const code = createCodeRuntime(options.tools, {
58
+ evaluate: codeEvaluator,
59
+ subagentSessions: options.subagentSessions,
60
+ });
61
+ const toolProviders = options.toolProviders ?? [];
62
+ if (!Array.isArray(toolProviders)) {
63
+ throw new TypeError("toolProviders must be an array");
64
+ }
65
+ for (const [index, provider] of toolProviders.entries()) {
66
+ const sourceOptions = {
67
+ id: provider.sourceId ?? `attached:${String(index).padStart(8, "0")}`,
68
+ kind: "attached",
69
+ mode: "attached-over-cloud",
70
+ deferred: true,
71
+ };
72
+ const sourceId = code.addProvider(provider, sourceOptions);
73
+ provider.setCatalogValidator?.((definitions) =>
74
+ code.validateProviderDefinitions(sourceId, definitions, sourceOptions));
75
+ }
76
+ const toolProvidersReady = Promise.all(
77
+ toolProviders.map((provider) => provider.settled?.()),
78
+ );
79
+ if (options.filesystem && options.filesystemTools === false) {
80
+ code.addTools({
81
+ apply_patch: {
82
+ description: "Apply a Rust-verified patch to the browser workspace.",
83
+ parameters: { type: "object", additionalProperties: false },
84
+ async handler(input, context) {
85
+ if (typeof options.applyPatch !== "function") {
86
+ throw new Error("the Rust browser apply_patch planner is unavailable");
87
+ }
88
+ const summary = await options.applyPatch(input, context.sessionId);
89
+ return toolResult(summary, {});
90
+ },
91
+ },
92
+ });
20
93
  }
94
+ const filesystemReady = options.filesystem && options.filesystemTools !== false
95
+ ? import("../runtime/workspace.mjs")
96
+ .then(({ tools }) => code.addTools(tools(options.filesystem)))
97
+ : undefined;
98
+ const mcp = options.mcp
99
+ ? import("../runtime/mcp-runtime.mjs").then(({ createMcpRuntime }) =>
100
+ createMcpRuntime(options.mcp, { clientName: "nanocodex-browser" }))
101
+ : undefined;
102
+ const mcpInstalled = mcp?.then(async (provider) => {
103
+ if (disposal) return provider.close();
104
+ try { code.addProvider(provider, { id: "mcp", kind: "mcp" }); }
105
+ catch (error) {
106
+ try { await provider.close(); }
107
+ catch (cleanupError) {
108
+ throw new AggregateError([error, cleanupError], "MCP installation and cleanup failed");
109
+ }
110
+ throw error;
111
+ }
112
+ return provider;
113
+ });
21
114
  const onEvent = options.onEvent || (() => {});
22
115
  const maxQueuedMessages = options.maxQueuedMessages ?? DEFAULT_MAX_QUEUED_MESSAGES;
23
116
  const maxQueuedBytes = options.maxQueuedBytes ?? DEFAULT_MAX_QUEUED_BYTES;
24
117
  const maxBufferedSendBytes = options.maxBufferedSendBytes ?? DEFAULT_MAX_BUFFERED_SEND_BYTES;
25
- const encoder = new TextEncoder();
26
118
  let nextHandle = 1;
119
+ let references = 0;
120
+ let disposal;
121
+ let disposalError;
122
+ let preconnected;
123
+
124
+ function preconnect(endpoint, sessionId) {
125
+ if (disposal) return Promise.reject(new Error("Nanocodex host is already disposed"));
126
+ if (preconnected?.endpoint === endpoint && preconnected.sessionId === sessionId) {
127
+ return preconnected.ownership.promise.then(() => undefined);
128
+ }
129
+ void closePreconnected().catch(() => {});
130
+ const ownership = openOwned(() => createWebSocket(endpoint, sessionId, {
131
+ authorization: "preconnect",
132
+ }));
133
+ const entry = { endpoint, sessionId, ownership };
134
+ preconnected = entry;
135
+ void ownership.promise.catch(() => {
136
+ if (preconnected === entry) preconnected = undefined;
137
+ });
138
+ return ownership.promise.then(() => undefined);
139
+ }
140
+
141
+ function takePreconnected(endpoint, sessionId) {
142
+ if (preconnected?.endpoint !== endpoint || preconnected.sessionId !== sessionId) {
143
+ return undefined;
144
+ }
145
+ const ownership = preconnected.ownership;
146
+ preconnected = undefined;
147
+ return ownership;
148
+ }
149
+
150
+ function closePreconnected(error = new Error("WebSocket preconnection was closed")) {
151
+ const entry = preconnected;
152
+ preconnected = undefined;
153
+ if (!entry) return Promise.resolve();
154
+ return entry.ownership.dispose(error);
155
+ }
27
156
 
28
157
  async function connect(endpoint, apiKey, sessionId, metadata = {}) {
158
+ if (disposal) throw new Error("Nanocodex host is already disposed");
29
159
  if (options.mpp) return connectMpp(endpoint);
30
160
  const authorization = options.hostAuth
31
161
  ? { authorization: "host_managed" }
@@ -34,7 +164,13 @@ export function createBrowserHost(options = {}) {
34
164
  delete request.authorization;
35
165
  delete request.bearerToken;
36
166
  Object.assign(request, authorization);
37
- const opened = await createWebSocket(endpoint, sessionId, request);
167
+ const threadId = metadata.threadId ?? sessionId;
168
+ let ownership = threadId === sessionId ? takePreconnected(endpoint, sessionId) : undefined;
169
+ if (ownership === undefined) {
170
+ if (preconnected !== undefined) await closePreconnected();
171
+ ownership = openOwned(() => createWebSocket(endpoint, sessionId, request));
172
+ }
173
+ const opened = await ownership.promise;
38
174
  const { socket, ...handshake } = normalizeWebSocketConnection(opened);
39
175
  return new Promise((resolve, reject) => {
40
176
  let settled = false;
@@ -46,9 +182,26 @@ export function createBrowserHost(options = {}) {
46
182
  intentionallyClosed: false,
47
183
  overflowed: false,
48
184
  };
185
+ const rejectConnection = (error) => {
186
+ if (settled) return;
187
+ settled = true;
188
+ connectingConnections.delete(connection);
189
+ reject(error);
190
+ };
191
+ connection.reject = rejectConnection;
192
+ ownership.transfer();
193
+ connectingConnections.add(connection);
49
194
  const resolveOpen = () => {
50
195
  if (settled) return;
196
+ if (disposal) {
197
+ connection.intentionallyClosed = true;
198
+ settled = true;
199
+ reject(disposalError);
200
+ return;
201
+ }
51
202
  settled = true;
203
+ connectingConnections.delete(connection);
204
+ delete connection.reject;
52
205
  const handle = nextHandle++;
53
206
  connections.set(handle, connection);
54
207
  resolve(JSON.stringify({
@@ -68,33 +221,32 @@ export function createBrowserHost(options = {}) {
68
221
  });
69
222
  socket.addEventListener("close", (event) => {
70
223
  if (!settled) {
71
- settled = true;
72
- reject(new Error(`WebSocket closed during connection with code ${event.code}`));
224
+ rejectConnection(new Error(`WebSocket closed during connection with code ${event.code}`));
73
225
  } else if (!connection.intentionallyClosed && !connection.overflowed) {
74
226
  enqueue(connection, { kind: "closed", detail: `with code ${event.code}` });
75
227
  }
76
228
  });
77
229
  socket.addEventListener("error", () => {
78
230
  if (!settled) {
79
- settled = true;
80
- reject(new Error("WebSocket connection failed"));
231
+ rejectConnection(new Error("WebSocket connection failed"));
81
232
  } else {
82
233
  enqueue(connection, { kind: "error", detail: "WebSocket connection failed" });
83
234
  }
84
235
  });
85
236
  if (socket.readyState === WEBSOCKET_OPEN) resolveOpen();
86
237
  else if (socket.readyState > WEBSOCKET_OPEN) {
87
- settled = true;
88
- reject(new Error("WebSocket closed during connection"));
238
+ rejectConnection(new Error("WebSocket closed during connection"));
89
239
  }
90
240
  });
91
241
  }
92
242
 
93
243
  async function connectMpp(endpoint) {
244
+ if (disposal) throw new Error("Nanocodex host is already disposed");
94
245
  if (typeof options.mpp.ws !== "function") {
95
246
  throw new TypeError("mpp must provide ws(endpoint)");
96
247
  }
97
- const socket = await options.mpp.ws(endpoint);
248
+ const ownership = openOwned(() => options.mpp.ws(endpoint));
249
+ const socket = await ownership.promise;
98
250
  if (!socket || typeof socket.addEventListener !== "function") {
99
251
  throw new TypeError("mpp.ws(endpoint) must return a WebSocket");
100
252
  }
@@ -108,6 +260,7 @@ export function createBrowserHost(options = {}) {
108
260
  overflowed: false,
109
261
  managed: true,
110
262
  };
263
+ ownership.transfer();
111
264
  connections.set(handle, connection);
112
265
  socket.addEventListener("message", (event) => {
113
266
  enqueue(connection, typeof event.data === "string"
@@ -116,7 +269,15 @@ export function createBrowserHost(options = {}) {
116
269
  });
117
270
  socket.addEventListener("close", (event) => {
118
271
  if (!connection.intentionallyClosed && !connection.overflowed) {
119
- enqueue(connection, { kind: "closed", detail: `with code ${event.code ?? 1000}` });
272
+ const code = event.code ?? 1000;
273
+ const suffix = event.reason ? `: ${event.reason}` : "";
274
+ enqueue(connection, code === MPP_CLIENT_PROTOCOL_ERROR_CLOSE_CODE
275
+ ? {
276
+ kind: "error",
277
+ detail: `MPP WebSocket payment flow failed with code ${code}${suffix}`,
278
+ reconnectable: false,
279
+ }
280
+ : { kind: "closed", detail: `with code ${code}${suffix}` });
120
281
  }
121
282
  });
122
283
  socket.addEventListener("error", () => {
@@ -139,7 +300,7 @@ export function createBrowserHost(options = {}) {
139
300
  connection.socket.send(JSON.stringify({ mpp: "message", data: message }));
140
301
  return Promise.resolve(JSON.stringify({ ok: true }));
141
302
  }
142
- const frameBytes = encoder.encode(message).byteLength;
303
+ const frameBytes = utf8ByteLength(message);
143
304
  if (frameBytes > maxBufferedSendBytes
144
305
  || connection.socket.bufferedAmount + frameBytes > maxBufferedSendBytes) {
145
306
  return Promise.resolve(JSON.stringify({
@@ -159,7 +320,7 @@ export function createBrowserHost(options = {}) {
159
320
  }
160
321
  }
161
322
 
162
- function next(handle, timeoutMs) {
323
+ function next(handle) {
163
324
  const connection = connections.get(handle);
164
325
  if (!connection) {
165
326
  return Promise.resolve(JSON.stringify({ kind: "closed", detail: "before the next frame" }));
@@ -171,12 +332,7 @@ export function createBrowserHost(options = {}) {
171
332
  }
172
333
  if (connection.waiter) return Promise.reject(new Error("concurrent reads are unsupported"));
173
334
  return new Promise((resolve) => {
174
- const timer = setTimeout(() => {
175
- connection.waiter = undefined;
176
- resolve(JSON.stringify({ kind: "timeout" }));
177
- }, timeoutMs);
178
335
  connection.waiter = (message) => {
179
- clearTimeout(timer);
180
336
  connection.waiter = undefined;
181
337
  resolve(JSON.stringify(message));
182
338
  };
@@ -189,7 +345,7 @@ export function createBrowserHost(options = {}) {
189
345
  connections.delete(handle);
190
346
  connection.intentionallyClosed = true;
191
347
  connection.waiter?.({ kind: "closed", detail: "by the WASM runtime" });
192
- connection.socket.close();
348
+ return connection.socket.close();
193
349
  }
194
350
 
195
351
  function enqueue(connection, message) {
@@ -198,7 +354,7 @@ export function createBrowserHost(options = {}) {
198
354
  connection.waiter(message);
199
355
  return;
200
356
  }
201
- const bytes = encoder.encode(message.kind === "text" ? message.text : JSON.stringify(message)).byteLength;
357
+ const bytes = utf8ByteLength(message.kind === "text" ? message.text : JSON.stringify(message));
202
358
  if (connection.queue.length >= maxQueuedMessages || connection.queuedBytes + bytes > maxQueuedBytes) {
203
359
  connection.queue.length = 0;
204
360
  connection.queuedBytes = 0;
@@ -207,7 +363,7 @@ export function createBrowserHost(options = {}) {
207
363
  kind: "error",
208
364
  detail: `receive queue exceeded ${maxQueuedMessages} messages or ${maxQueuedBytes} bytes`,
209
365
  };
210
- const errorBytes = encoder.encode(JSON.stringify(error)).byteLength;
366
+ const errorBytes = utf8ByteLength(JSON.stringify(error));
211
367
  connection.queue.push({ message: error, bytes: errorBytes });
212
368
  connection.queuedBytes = errorBytes;
213
369
  connection.socket.close(1009, "receive queue exceeded configured bounds");
@@ -217,18 +373,156 @@ export function createBrowserHost(options = {}) {
217
373
  connection.queuedBytes += bytes;
218
374
  }
219
375
 
376
+ function dispose() {
377
+ if (disposal) return disposal;
378
+ disposalError = new Error("Nanocodex host was disposed during WebSocket connection");
379
+ disposal = Promise.resolve().then(async () => {
380
+ const cleanups = [];
381
+ const cleanup = (action) => {
382
+ try { cleanups.push(Promise.resolve(action())); }
383
+ catch (failure) { cleanups.push(Promise.reject(failure)); }
384
+ };
385
+ const preconnectedOwnership = preconnected?.ownership;
386
+ for (const attempt of [...openingAttempts]) {
387
+ if (attempt !== preconnectedOwnership) cleanup(() => attempt.dispose(disposalError));
388
+ }
389
+ for (const connection of [...connectingConnections]) {
390
+ connection.intentionallyClosed = true;
391
+ cleanup(() => connection.reject(disposalError));
392
+ cleanup(() => connection.socket.close());
393
+ }
394
+ for (const handle of [...connections.keys()]) cleanup(() => close(handle));
395
+ cleanup(() => closePreconnected(disposalError));
396
+ cleanup(() => code.reset());
397
+ cleanup(() => mcpInstalled?.then(() => {}));
398
+ cleanup(() => toolsLifecycle?.close());
399
+ cleanup(() => options.onDispose?.());
400
+ const settled = await Promise.allSettled(cleanups);
401
+ const errors = settled
402
+ .filter((result) => result.status === "rejected")
403
+ .map((result) => result.reason);
404
+ if (errors.length === 1) throw errors[0];
405
+ if (errors.length > 1) {
406
+ throw new AggregateError(errors, "Nanocodex host disposal failed");
407
+ }
408
+ });
409
+ return disposal;
410
+ }
411
+
412
+ function openOwned(factory) {
413
+ if (disposal) throw new Error("Nanocodex host is already disposed");
414
+ let rejectDisposed;
415
+ const disposed = new Promise((_, reject) => { rejectDisposed = reject; });
416
+ const ownership = {
417
+ disposed: false,
418
+ opened: undefined,
419
+ closePromise: undefined,
420
+ error: undefined,
421
+ dispose(error) {
422
+ if (ownership.disposed) return ownership.closePromise ?? Promise.resolve();
423
+ ownership.disposed = true;
424
+ ownership.error = error;
425
+ openingAttempts.delete(ownership);
426
+ rejectDisposed(error);
427
+ return closeOpened();
428
+ },
429
+ transfer() {
430
+ if (disposal || ownership.disposed) {
431
+ throw disposalError ?? ownership.error
432
+ ?? new Error("Nanocodex host was disposed during WebSocket connection");
433
+ }
434
+ openingAttempts.delete(ownership);
435
+ },
436
+ };
437
+ openingAttempts.add(ownership);
438
+ let opening;
439
+ try {
440
+ opening = Promise.resolve(factory());
441
+ } catch (error) {
442
+ opening = Promise.reject(error);
443
+ }
444
+ opening = opening.then((opened) => {
445
+ ownership.opened = opened;
446
+ if (!ownership.disposed) return opened;
447
+ void closeOpened().catch(() => {});
448
+ throw ownership.error;
449
+ });
450
+ ownership.promise = Promise.race([opening, disposed]);
451
+ void ownership.promise.catch(() => {
452
+ openingAttempts.delete(ownership);
453
+ });
454
+ return ownership;
455
+
456
+ function closeOpened() {
457
+ if (ownership.closePromise) return ownership.closePromise;
458
+ if (ownership.opened === undefined) return Promise.resolve();
459
+ try {
460
+ ownership.closePromise = Promise.resolve(
461
+ normalizeWebSocketConnection(ownership.opened).socket.close(),
462
+ );
463
+ } catch (error) {
464
+ ownership.closePromise = Promise.reject(error);
465
+ }
466
+ return ownership.closePromise;
467
+ }
468
+ }
469
+
470
+ toolsLifecycle?.claim();
220
471
  return Object.freeze({
472
+ ready: async () => {
473
+ await Promise.all([filesystemReady, mcpInstalled, toolProvidersReady]);
474
+ },
475
+ retain() {
476
+ if (disposal) throw new Error("Nanocodex host is already disposed");
477
+ references += 1;
478
+ },
479
+ release() {
480
+ if (references > 0) references -= 1;
481
+ return references === 0 ? dispose() : Promise.resolve();
482
+ },
221
483
  connect,
484
+ preconnect,
222
485
  send,
223
486
  next,
224
487
  close,
225
488
  sleep: (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)),
226
- executeCode: code.executeCode,
489
+ executeCode: code.executeCodeObserved,
490
+ waitCode: code.waitCodeObserved,
491
+ beginCodeTurn: code.beginTurn,
492
+ cancelCodeTurn: code.cancelTurn,
493
+ nextCodeUpdate: code.nextCodeUpdate,
227
494
  executeTool: code.executeTool,
495
+ bindSubagentSession: code.bindSubagentSession,
496
+ cancelCode: code.cancel,
497
+ readWorkspaceFile: async (path) => {
498
+ if (!options.filesystem) throw new Error("browser workspace is unavailable");
499
+ const contents = await options.filesystem.readFile(path);
500
+ if (!(contents instanceof Uint8Array)) {
501
+ throw new TypeError("browser workspace readFile() must return Uint8Array");
502
+ }
503
+ return contents;
504
+ },
505
+ listWorkspace: async (path) => {
506
+ if (!options.filesystem) throw new Error("browser workspace is unavailable");
507
+ if (typeof options.filesystem.list !== "function") {
508
+ throw new Error("browser workspace does not expose list()");
509
+ }
510
+ return options.filesystem.list(path, { maxEntries: 2_000 });
511
+ },
512
+ writeWorkspaceFile: async (path, contents) => {
513
+ if (!options.filesystem) throw new Error("browser workspace is unavailable");
514
+ await options.filesystem.writeFile(path, contents);
515
+ },
516
+ removeWorkspaceFile: async (path) => {
517
+ if (!options.filesystem) throw new Error("browser workspace is unavailable");
518
+ await options.filesystem.remove(path);
519
+ },
228
520
  toolMode: () => toolMode,
229
521
  toolDefinitions: code.toolDefinitions,
522
+ releaseSession: code.releaseSession,
230
523
  emitEvent: onEvent,
231
524
  reset: code.reset,
525
+ dispose,
232
526
  });
233
527
  }
234
528
 
@@ -0,0 +1,14 @@
1
+ export type HostManagedWebSocketOptions = Readonly<{
2
+ WebSocketImpl?: typeof WebSocket | undefined;
3
+ timeoutMs?: number | undefined;
4
+ }>;
5
+
6
+ export function openHostManagedWebSocket(
7
+ endpoint: string | URL,
8
+ sessionId: string,
9
+ options?: HostManagedWebSocketOptions,
10
+ ): Promise<WebSocket>;
11
+
12
+ export function defaultHostManagedWebSocketUrl(
13
+ location?: Pick<Location, "href">,
14
+ ): string;
@@ -0,0 +1,110 @@
1
+ const DEFAULT_TIMEOUT_MS = 10_000;
2
+
3
+ /** Open Nanocodex's same-origin Responses proxy and consume its setup frame. */
4
+ export function openHostManagedWebSocket(endpoint, sessionId, options = {}) {
5
+ if (typeof sessionId !== "string" || !sessionId) {
6
+ throw new TypeError("host-managed WebSocket requires a session ID");
7
+ }
8
+ const WebSocketImpl = options.WebSocketImpl ?? globalThis.WebSocket;
9
+ if (typeof WebSocketImpl !== "function") {
10
+ throw new Error("WebSocket is unavailable in this runtime");
11
+ }
12
+ const socketUrl = resolveWebSocketUrl(endpoint);
13
+ socketUrl.searchParams.set("session_id", sessionId);
14
+ socketUrl.searchParams.set("thread_id", options.threadId ?? sessionId);
15
+ return waitForProxyHandshake(
16
+ new WebSocketImpl(socketUrl),
17
+ options.timeoutMs ?? DEFAULT_TIMEOUT_MS,
18
+ );
19
+ }
20
+
21
+ export function defaultHostManagedWebSocketUrl(location = globalThis.location) {
22
+ if (!location?.href) {
23
+ throw new Error("host-managed transport requires websocketUrl outside a browser location");
24
+ }
25
+ const url = new URL("/api/responses", location.href);
26
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
27
+ return url.href;
28
+ }
29
+
30
+ function resolveWebSocketUrl(endpoint) {
31
+ const base = globalThis.location?.href;
32
+ const url = base === undefined ? new URL(endpoint) : new URL(endpoint, base);
33
+ if (url.protocol !== "ws:" && url.protocol !== "wss:") {
34
+ throw new TypeError("host-managed websocketUrl must use ws: or wss:");
35
+ }
36
+ return url;
37
+ }
38
+
39
+ function waitForProxyHandshake(socket, timeoutMs) {
40
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) {
41
+ socket.close();
42
+ throw new TypeError("host-managed WebSocket timeout must be a positive integer");
43
+ }
44
+ return new Promise((resolve, reject) => {
45
+ let settled = false;
46
+ const timer = setTimeout(
47
+ () => fail(new Error("Agent connection timed out")),
48
+ timeoutMs,
49
+ );
50
+ const cleanup = () => {
51
+ clearTimeout(timer);
52
+ socket.removeEventListener("message", onMessage);
53
+ socket.removeEventListener("error", onError);
54
+ socket.removeEventListener("close", onClose);
55
+ };
56
+ const fail = (error) => {
57
+ if (settled) return;
58
+ settled = true;
59
+ cleanup();
60
+ socket.close();
61
+ reject(error);
62
+ };
63
+ const onMessage = (event) => {
64
+ const message = parseHandshake(event.data);
65
+ if (message?.type === "nanocodex.proxy.ready") {
66
+ settled = true;
67
+ cleanup();
68
+ resolve(socket);
69
+ return;
70
+ }
71
+ if (message?.type === "nanocodex.proxy.rejected"
72
+ && Number.isInteger(message.status)
73
+ && message.status >= 100
74
+ && message.status <= 599) {
75
+ const status = message.status;
76
+ const body = typeof message.error === "string" ? message.error : `HTTP ${status}`;
77
+ const retryAfter = Number(message.retryAfter);
78
+ fail(Object.assign(
79
+ new Error(`Agent connection rejected with HTTP ${status}: ${body}`),
80
+ {
81
+ status,
82
+ body,
83
+ ...(Number.isFinite(retryAfter) && retryAfter >= 0 ? { retryAfter } : {}),
84
+ },
85
+ ));
86
+ return;
87
+ }
88
+ fail(new Error("Agent connection returned an invalid handshake"));
89
+ };
90
+ const onError = () => fail(new Error("WebSocket connection failed"));
91
+ const onClose = (event) => fail(
92
+ new Error(`WebSocket closed during connection with code ${event.code}`),
93
+ );
94
+ socket.addEventListener("message", onMessage);
95
+ socket.addEventListener("error", onError);
96
+ socket.addEventListener("close", onClose);
97
+ });
98
+ }
99
+
100
+ function parseHandshake(data) {
101
+ if (typeof data !== "string") return undefined;
102
+ try {
103
+ const value = JSON.parse(data);
104
+ return typeof value === "object" && value !== null && !Array.isArray(value)
105
+ ? value
106
+ : undefined;
107
+ } catch {
108
+ return undefined;
109
+ }
110
+ }
@@ -1,18 +1,81 @@
1
- export { Actions } from "../index.mjs";
1
+ export * as Actions from "../actions/index.mjs";
2
+ export {
3
+ createMemoryChatGptSubscriptionStore,
4
+ subscriptionRevision,
5
+ } from "../runtime/subscription-store.mjs";
6
+ export { createQuickJsEvaluator } from "../runtime/quickjs-evaluator.mjs";
7
+ export {
8
+ createTempoProvider,
9
+ createTempoProviderFromAccounts,
10
+ DEFAULT_MERCATOR_MCP_URL,
11
+ } from "../runtime/tempo-provider.mjs";
12
+ export type {
13
+ AccountsTempoProviderOptions,
14
+ AccountsWallet,
15
+ TempoProvider,
16
+ } from "../runtime/tempo-provider.mjs";
2
17
  export type {
3
18
  AgentEvent,
19
+ AgentLifecycle,
20
+ AgentSessionContext,
21
+ ChatGptCredential,
22
+ ChatGptCredentialSeed,
23
+ ChatGptLoginStatus,
24
+ ChatGptSubscriptionHandle,
25
+ ChatGptSubscriptionOptions,
26
+ ChatGptSubscriptionStore,
4
27
  CostStatus,
28
+ CodeEvaluator,
29
+ CodeEvaluatorEnvironment,
5
30
  EstimatedUsdCost,
31
+ ExecutionEnvironment,
6
32
  PromptInput,
7
33
  PromptItem,
34
+ LifecycleTurn,
35
+ LifecycleTurnResult,
8
36
  ReasoningMode,
9
37
  SessionSnapshot,
10
38
  Thinking,
11
39
  Tool,
40
+ NamedTool,
12
41
  ToolContext,
42
+ SubagentToolContext,
43
+ ToolConfiguration,
13
44
  ToolMap,
14
45
  Turn,
15
46
  TurnResult,
16
47
  TurnUsage,
48
+ McpPayment,
49
+ McpServer,
50
+ McpServers,
51
+ MemoryChatGptSubscriptionStore,
52
+ MppSession,
53
+ SubscriptionCommitRequest,
54
+ SubscriptionCommitResult,
55
+ SubscriptionRevision,
56
+ SubscriptionStoredValue,
17
57
  } from "../types.mjs";
18
58
  export * as Agent from "./Agent.mjs";
59
+ export * as ChatGptSubscription from "./ChatGptSubscription.mjs";
60
+ export * as Subagents from "../runtime/subagents.mjs";
61
+ export * as Transport from "./Transport.mjs";
62
+ export * as Voice from "./Voice.mjs";
63
+ export * as Workspace from "./workspace.mjs";
64
+ export * as Tools from "../tools/index.mjs";
65
+ export {
66
+ createConfig,
67
+ type AgentParameters,
68
+ type AgentSnapshot,
69
+ type AgentStatus,
70
+ type Config,
71
+ type CreateConfigParameters,
72
+ } from "./config.mjs";
73
+ export {
74
+ defaultHostManagedWebSocketUrl,
75
+ openHostManagedWebSocket,
76
+ type HostManagedWebSocketOptions,
77
+ } from "./hostManagedWebSocket.mjs";
78
+ export type {
79
+ BrowserWebSocketConnection,
80
+ BrowserWebSocketRequest,
81
+ } from "./host.mjs";