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/internal.mjs CHANGED
@@ -1,12 +1,24 @@
1
1
  const agentStates = new WeakMap();
2
2
  const turnStates = new WeakMap();
3
3
  const resultStates = new WeakMap();
4
+ const resultFinalizer = typeof FinalizationRegistry === "function"
5
+ ? new FinalizationRegistry((raw) => {
6
+ try { raw.free(); } catch (error) { reportError(error); }
7
+ })
8
+ : undefined;
4
9
  const hostSessions = new Map();
10
+ const cloudflareHostReservations = new WeakMap();
11
+ const activeAgentSessions = new Map();
12
+ const pendingCloudflareAgentSessions = new Map();
13
+ const cloudflareAgentSessions = new WeakSet();
5
14
  const hostConnections = new Map();
6
- let activeHost;
15
+ const definitionHosts = new Map();
7
16
  let nextHostConnection = 1;
17
+ let nextDefinitionHost = 1;
8
18
  let nextAgentUid = 1;
9
19
 
20
+ export const CLOUDFLARE_SESSION_RESERVATION = Symbol("nanocodex.cloudflare.sessionReservation");
21
+
10
22
  export function defineRuntime(definition) {
11
23
  if (!definition || typeof definition.create !== "function") {
12
24
  throw new TypeError("a Nanocodex runtime must define create(options)");
@@ -21,33 +33,107 @@ export function defineRuntime(definition) {
21
33
  adopt: definition.adopt,
22
34
  release: definition.release,
23
35
  decorate: definition.decorate,
36
+ reserveSessions: definition.reserveSessions !== false,
24
37
  });
25
38
  }
26
39
 
27
- export async function createAgentClient(runtime, options = {}) {
40
+ export async function createAgentClient(runtime, options = {}, requestedReservation) {
28
41
  if (!runtime || typeof runtime.create !== "function") {
29
42
  throw new TypeError("createAgent requires a Nanocodex runtime");
30
43
  }
31
- return createAgent(await runtime.create(options), runtime);
44
+ const reserveSession = runtime.reserveSessions !== false;
45
+ const reservation = requestedReservation ?? (
46
+ !reserveSession || options.sessionId === undefined
47
+ ? undefined
48
+ : reserveAgentSession(options.sessionId)
49
+ );
50
+ let raw;
51
+ try {
52
+ raw = await runtime.create(options);
53
+ } catch (error) {
54
+ releaseAgentSession(reservation);
55
+ throw error;
56
+ }
57
+ return createAgent(raw, runtime, reservation, reserveSession);
58
+ }
59
+
60
+ /** Internal adapter seam: observes completion of the owned Agent release path. */
61
+ export function observeAgentRelease(agent, listener) {
62
+ if (typeof listener !== "function") throw new TypeError("Agent release observer must be a function");
63
+ const state = knownAgentState(agent);
64
+ if (state.released) {
65
+ listener({ graceful: state.shutdownPromise !== undefined });
66
+ return () => {};
67
+ }
68
+ state.releaseObservers.add(listener);
69
+ return () => state.releaseObservers.delete(listener);
70
+ }
71
+
72
+ /** Creates the stable UUIDv7 identity reserved before a WASM Agent is constructed. */
73
+ export function createSessionId() {
74
+ if (typeof globalThis.crypto?.getRandomValues !== "function") {
75
+ throw new Error("Nanocodex Agent creation requires crypto.getRandomValues()");
76
+ }
77
+ const bytes = globalThis.crypto.getRandomValues(new Uint8Array(16));
78
+ let timestamp = Date.now();
79
+ for (let index = 5; index >= 0; index -= 1) {
80
+ bytes[index] = timestamp % 256;
81
+ timestamp = Math.floor(timestamp / 256);
82
+ }
83
+ bytes[6] = (bytes[6] & 0x0f) | 0x70;
84
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
85
+ const encoded = [...bytes].map((byte) => byte.toString(16).padStart(2, "0"));
86
+ return `${encoded.slice(0, 4).join("")}-${encoded.slice(4, 6).join("")}-${encoded.slice(6, 8).join("")}-${encoded.slice(8, 10).join("")}-${encoded.slice(10).join("")}`;
32
87
  }
33
88
 
34
89
  export function prompt(agent, options) {
35
90
  const state = agentState(agent);
36
91
  const input = actionInput(options);
92
+ const operationId = options?.id;
93
+ const cancelOnAdmission = options?.cancelOnAdmission === true;
37
94
  const raw = typeof input === "string"
38
- ? state.raw.prompt(input)
39
- : state.raw.promptContent(JSON.stringify(input));
95
+ ? state.raw.prompt(input, operationId, cancelOnAdmission)
96
+ : state.raw.promptContent(JSON.stringify(input), operationId, cancelOnAdmission);
40
97
  return createTurn(raw, agent);
41
98
  }
42
99
 
100
+ /** Internal live-input seam: atomically steers the active turn or starts one. */
101
+ export async function routePrompt(agent, options) {
102
+ const state = agentState(agent);
103
+ const input = actionInput(options);
104
+ if (typeof input !== "string") {
105
+ throw new TypeError("live routed input must be text");
106
+ }
107
+ const raw = await state.raw.routePrompt(input);
108
+ return raw === undefined ? undefined : createTurn(raw, agent);
109
+ }
110
+
43
111
  export function getTurnResult(turn) {
44
112
  const state = turnState(turn);
45
- state.result ||= Promise.resolve()
46
- .then(() => state.raw.result())
47
- .then(createTurnResult);
113
+ if (!state.result) {
114
+ try {
115
+ state.result = Promise.resolve(state.raw.result()).then(createTurnResult);
116
+ } catch (error) {
117
+ state.result = Promise.reject(error);
118
+ }
119
+ }
48
120
  return state.result;
49
121
  }
50
122
 
123
+ export function awaitTurnAcceptance(turn) {
124
+ const state = turnState(turn);
125
+ if (!state.acceptance) {
126
+ try {
127
+ state.acceptance = typeof state.raw.accepted === "function"
128
+ ? Promise.resolve(state.raw.accepted())
129
+ : Promise.resolve(undefined);
130
+ } catch (error) {
131
+ state.acceptance = Promise.reject(error);
132
+ }
133
+ }
134
+ return state.acceptance;
135
+ }
136
+
51
137
  export function getTurnSnapshot(result) {
52
138
  return resultState(result).snapshot();
53
139
  }
@@ -56,12 +142,27 @@ export function getTurnUsage(result) {
56
142
  return resultState(result).usage();
57
143
  }
58
144
 
145
+ /** Internal Worker seam: preserves the Rust-owned encoding until it reaches its consumer. */
146
+ export function getEncodedTurnSnapshot(result) {
147
+ return encodedTurnResultValue(resultState(result), "snapshot");
148
+ }
149
+
150
+ /** Internal Worker seam: preserves the Rust-owned encoding until it reaches its consumer. */
151
+ export function getEncodedTurnUsage(result) {
152
+ return encodedTurnResultValue(resultState(result), "usage");
153
+ }
154
+
59
155
  export function steer(turn, options) {
60
156
  const state = turnState(turn);
61
157
  const input = actionInput(options);
62
158
  return typeof input === "string"
63
- ? state.raw.steer(input)
64
- : state.raw.steerContent(JSON.stringify(input));
159
+ ? state.raw.steer(input, options.messageId)
160
+ : state.raw.steerContent(JSON.stringify(input), options.messageId);
161
+ }
162
+
163
+ export function withdrawSteer(turn, { messageId }) {
164
+ if (typeof messageId !== "string" || !messageId) throw new TypeError("messageId must be a non-empty string");
165
+ return turnState(turn).raw.withdrawSteer(messageId);
65
166
  }
66
167
 
67
168
  export function cancel(turn) {
@@ -82,10 +183,42 @@ export async function spawn(agent) {
82
183
  return createAgent(await state.raw.spawn(), state.runtime);
83
184
  }
84
185
 
186
+ export async function spawnSubagent(agent, options) {
187
+ return JSON.parse(await agentState(agent).raw.spawnSubagent(JSON.stringify(options)));
188
+ }
189
+
190
+ export async function spawnSubagents(agent, options) {
191
+ return JSON.parse(await agentState(agent).raw.spawnSubagents(JSON.stringify(options)));
192
+ }
193
+
194
+ export async function waitSubagents(agent, options) {
195
+ return JSON.parse(await agentState(agent).raw.waitSubagents(JSON.stringify(options)));
196
+ }
197
+
198
+ export async function listSubagents(agent, options) {
199
+ return JSON.parse(await agentState(agent).raw.listSubagents(JSON.stringify(options ?? {})));
200
+ }
201
+
202
+ export async function sendSubagentMessage(agent, options) {
203
+ return JSON.parse(await agentState(agent).raw.sendSubagentMessage(JSON.stringify(options)));
204
+ }
205
+
206
+ export async function interruptSubagent(agent, agentId) {
207
+ return JSON.parse(await agentState(agent).raw.interruptSubagent(JSON.stringify({ agentId })));
208
+ }
209
+
210
+ export async function closeSubagent(agent, agentId) {
211
+ return JSON.parse(await agentState(agent).raw.closeSubagent(JSON.stringify({ agentId })));
212
+ }
213
+
85
214
  export function setThinking(agent, thinking) {
86
215
  return agentState(agent).raw.setThinking(thinking);
87
216
  }
88
217
 
218
+ export function setModel(agent, model) {
219
+ return agentState(agent).raw.setModel(model);
220
+ }
221
+
89
222
  export function setFastMode(agent, enabled) {
90
223
  return agentState(agent).raw.setFastMode(enabled);
91
224
  }
@@ -94,6 +227,38 @@ export function compact(agent) {
94
227
  return agentState(agent).raw.compact();
95
228
  }
96
229
 
230
+ export async function context(agent) {
231
+ return parseSessionContext(await agentState(agent).raw.context());
232
+ }
233
+
234
+ export async function appendDeveloperMessage(agent, text) {
235
+ if (typeof text !== "string" || !text.trim()) {
236
+ throw new TypeError("non-empty string");
237
+ }
238
+ return parseSessionContext(await agentState(agent).raw.appendDeveloperMessage(text));
239
+ }
240
+
241
+ export async function startRealtimeConversation(agent) {
242
+ return parseSessionContext(await agentState(agent).raw.startRealtimeConversation());
243
+ }
244
+
245
+ export async function endRealtimeConversation(agent) {
246
+ return parseSessionContext(await agentState(agent).raw.endRealtimeConversation());
247
+ }
248
+
249
+ export async function realtimeDelegation(agent, input, transcript = []) {
250
+ return agentState(agent).raw.realtimeDelegation(input, JSON.stringify(transcript));
251
+ }
252
+
253
+ export async function realtimeTailDelegation(agent, transcript) {
254
+ return agentState(agent).raw.realtimeTailDelegation(JSON.stringify(transcript));
255
+ }
256
+
257
+ /** Internal browser seam over the Rust-owned voice controller. */
258
+ export function createBrowserVoice(agent, voice) {
259
+ return agentState(agent).raw.browserVoice(voice);
260
+ }
261
+
97
262
  export async function shutdown(agent) {
98
263
  const state = knownAgentState(agent);
99
264
  if (state.shutdownPromise) return state.shutdownPromise;
@@ -106,6 +271,10 @@ export async function shutdown(agent) {
106
271
  return state.shutdownPromise;
107
272
  }
108
273
 
274
+ function parseSessionContext(context) {
275
+ return JSON.parse(context);
276
+ }
277
+
109
278
  export function subscribeAgentEvents(agent, listener, options = {}, onRelease) {
110
279
  const state = agentState(agent);
111
280
  if (typeof state.runtime.subscribe !== "function") {
@@ -114,9 +283,9 @@ export function subscribeAgentEvents(agent, listener, options = {}, onRelease) {
114
283
  if (typeof listener !== "function") {
115
284
  throw new TypeError("watchAgentEvents requires a listener");
116
285
  }
117
- const unsubscribe = state.runtime.subscribe((event, encodedLength) => {
286
+ const unsubscribe = state.runtime.subscribe((event, encodedLength, encodedEvent, agentId) => {
118
287
  if (options.includeAllSessions || !event?.request_id || event.request_id === agent.sessionId) {
119
- listener(event, encodedLength);
288
+ listener(event, encodedLength, encodedEvent, agentId);
120
289
  }
121
290
  });
122
291
  let active = true;
@@ -145,27 +314,72 @@ export function toWasmConfig(options = {}) {
145
314
  copy(config, "thinking", options.thinking);
146
315
  copy(config, "reasoning_mode", options.reasoningMode);
147
316
  copy(config, "fast_mode", options.fastMode);
317
+ copy(config, "websocket_warmup", options.websocketWarmup);
148
318
  copy(config, "websocket_url", options.websocketUrl);
149
319
  copy(config, "api_base_url", options.apiBaseUrl);
150
320
  copy(config, "instructions", options.instructions);
321
+ copy(config, "additional_instructions", options.additionalInstructions);
151
322
  copy(config, "session_id", options.sessionId);
152
323
  copy(config, "workspace", options.workspace);
324
+ if (options.executionEnvironment !== undefined) {
325
+ const environment = options.executionEnvironment;
326
+ if (!environment || typeof environment !== "object" || Array.isArray(environment)) {
327
+ throw new TypeError("executionEnvironment must be an object");
328
+ }
329
+ config.execution_environment = {
330
+ current_date: environment.currentDate,
331
+ timezone: environment.timezone,
332
+ };
333
+ copy(
334
+ config.execution_environment,
335
+ "project_instructions",
336
+ environment.projectInstructions,
337
+ );
338
+ }
153
339
  copy(config, "resume", options.resume);
340
+ copy(config, "durability_id", options.durabilityId);
341
+ copy(config, "durability_host_id", options.durabilityHostId);
342
+ copy(config, "terminal_receipt_retention", options.terminalReceiptRetention);
343
+ copy(config, "subagents", options.subagents);
344
+ copy(config, "host_definition_id", options.hostDefinitionId);
154
345
  return config;
155
346
  }
156
347
 
157
348
  export function createEventChannel() {
158
349
  const listeners = new Set();
350
+ const sources = new Set();
159
351
  return Object.freeze({
160
- emit(eventJson) {
352
+ emit(eventJson, encodedBytes, agentId) {
161
353
  if (!listeners.size) return;
162
- const event = typeof eventJson === "string" ? JSON.parse(eventJson) : eventJson;
163
- const encodedLength = typeof eventJson === "string" ? eventJson.length : undefined;
164
- for (const listener of listeners) listener(event, encodedLength);
354
+ const event = freezeJson(typeof eventJson === "string" ? JSON.parse(eventJson) : eventJson);
355
+ const encodedLength = Number.isSafeInteger(encodedBytes) && encodedBytes >= 0
356
+ ? encodedBytes
357
+ : undefined;
358
+ for (const listener of listeners) listener(event, encodedLength, eventJson, agentId);
165
359
  },
166
360
  subscribe(listener) {
361
+ const activate = listeners.size === 0;
167
362
  listeners.add(listener);
168
- return () => listeners.delete(listener);
363
+ if (activate) {
364
+ for (const source of sources) source.setEventForwarding?.(true);
365
+ }
366
+ let active = true;
367
+ return () => {
368
+ if (!active) return;
369
+ active = false;
370
+ listeners.delete(listener);
371
+ if (listeners.size === 0) {
372
+ for (const source of sources) source.setEventForwarding?.(false);
373
+ }
374
+ };
375
+ },
376
+ addSource(source) {
377
+ sources.add(source);
378
+ if (listeners.size) source.setEventForwarding?.(true);
379
+ },
380
+ removeSource(source) {
381
+ if (!sources.delete(source)) return;
382
+ source.setEventForwarding?.(false);
169
383
  },
170
384
  });
171
385
  }
@@ -174,30 +388,71 @@ export function activateHost(host) {
174
388
  if (!host || typeof host.connect !== "function") {
175
389
  throw new TypeError("a Nanocodex host must define connect()");
176
390
  }
177
- activeHost = host;
391
+ installHostBridge();
392
+ }
393
+
394
+ export function installHostBridge() {
178
395
  globalThis.nanocodexHost = hostBridge;
179
396
  }
180
397
 
181
- export function bindHostSession(host, sessionId) {
398
+ export function loadDurabilityRuntime() {
399
+ return import("./runtime/durability.mjs");
400
+ }
401
+
402
+ export function reportError(error) {
403
+ try {
404
+ if (typeof globalThis.reportError === "function") globalThis.reportError(error);
405
+ else globalThis.console?.error?.(error);
406
+ } catch {}
407
+ }
408
+
409
+ export function bindHostSession(host, sessionId, reservation) {
182
410
  const existing = hostSessions.get(sessionId);
411
+ const replacementOwner = cloudflareSessionOwner(reservation, sessionId);
183
412
  if (existing && existing !== host) {
184
- throw new Error(`Nanocodex session ID is already active: ${sessionId}`);
413
+ if (replacementOwner === undefined
414
+ || cloudflareHostReservations.get(existing)?.ownerId !== replacementOwner) {
415
+ throw new Error(`Nanocodex session ID is already active: ${sessionId}`);
416
+ }
185
417
  }
186
418
  hostSessions.set(sessionId, host);
419
+ if (replacementOwner !== undefined) {
420
+ reservation.host = host;
421
+ }
187
422
  }
188
423
 
189
424
  export function releaseHostSession(host, sessionId) {
190
- if (hostSessions.get(sessionId) === host) hostSessions.delete(sessionId);
425
+ if (hostSessions.get(sessionId) !== host) return;
426
+ hostSessions.delete(sessionId);
427
+ }
428
+
429
+ export function registerDefinitionHost(host, cloudflareReservation) {
430
+ const id = nextDefinitionHost++;
431
+ definitionHosts.set(id, host);
432
+ if (cloudflareReservation !== undefined) {
433
+ if (!cloudflareAgentSessions.has(cloudflareReservation)
434
+ || cloudflareReservation.released) {
435
+ definitionHosts.delete(id);
436
+ throw new Error("Cloudflare Agent session reservation is no longer active");
437
+ }
438
+ cloudflareHostReservations.set(host, cloudflareReservation);
439
+ }
440
+ return id;
441
+ }
442
+
443
+ export function releaseDefinitionHost(id) {
444
+ definitionHosts.delete(id);
191
445
  }
192
446
 
193
447
  const hostBridge = Object.freeze({
194
- async connect(endpoint, apiKey, accountId, fedramp, sessionId, turnState) {
195
- const host = requiredSessionHost(sessionId);
448
+ async connect(endpoint, apiKey, accountId, fedramp, sessionId, threadId, turnState) {
449
+ const host = requiredSessionHost(threadId);
196
450
  let result;
197
451
  try {
198
452
  result = JSON.parse(await host.connect(endpoint, apiKey, sessionId, {
199
453
  accountId: accountId ?? undefined,
200
454
  fedramp,
455
+ threadId,
201
456
  turnState: turnState ?? undefined,
202
457
  }));
203
458
  } catch (error) {
@@ -205,7 +460,7 @@ const hostBridge = Object.freeze({
205
460
  }
206
461
  const handle = nextHostConnection++;
207
462
  hostConnections.set(handle, { host, handle: result.handle });
208
- hostSessions.set(sessionId, host);
463
+ hostSessions.set(threadId, host);
209
464
  return JSON.stringify({ ...result, handle });
210
465
  },
211
466
  send(handle, message) {
@@ -214,10 +469,10 @@ const hostBridge = Object.freeze({
214
469
  ? connection.host.send(connection.handle, message)
215
470
  : Promise.resolve(JSON.stringify({ ok: false, reconnectable: true, error: "unknown WebSocket handle" }));
216
471
  },
217
- next(handle, timeoutMs) {
472
+ next(handle) {
218
473
  const connection = hostConnections.get(handle);
219
474
  return connection
220
- ? connection.host.next(connection.handle, timeoutMs)
475
+ ? connection.host.next(connection.handle)
221
476
  : Promise.resolve(JSON.stringify({ kind: "closed", detail: "for an unknown WebSocket handle" }));
222
477
  },
223
478
  close(handle) {
@@ -233,59 +488,220 @@ const hostBridge = Object.freeze({
233
488
  }
234
489
  return host.sleep(milliseconds);
235
490
  },
236
- executeCode(source, sessionId, callId) {
237
- return requiredSessionHost(sessionId).executeCode(source, sessionId, callId);
491
+ bindSubagentSession(
492
+ hostDefinitionId,
493
+ rootSessionId,
494
+ sessionId,
495
+ contextJson,
496
+ hostContextRef,
497
+ ) {
498
+ let host;
499
+ if (contextJson === undefined) {
500
+ contextJson = sessionId;
501
+ sessionId = rootSessionId;
502
+ rootSessionId = hostDefinitionId;
503
+ host = requiredSessionHost(rootSessionId);
504
+ } else {
505
+ host = requiredDefinitionHost(hostDefinitionId);
506
+ }
507
+ if (!cloudflareHostMayBindSubagent(host)) return;
508
+ const existing = hostSessions.get(sessionId);
509
+ if (existing && existing !== host) {
510
+ const ownerId = cloudflareHostReservations.get(host)?.ownerId;
511
+ if (ownerId === undefined
512
+ || cloudflareHostReservations.get(existing)?.ownerId !== ownerId) {
513
+ throw new Error(`Nanocodex subagent session ID is already active: ${sessionId}`);
514
+ }
515
+ }
516
+ host.bindSubagentSession(sessionId, JSON.parse(contextJson), hostContextRef);
517
+ hostSessions.set(sessionId, host);
518
+ },
519
+ releaseSubagentSession(hostDefinitionId, rootSessionId, sessionId) {
520
+ let host;
521
+ if (sessionId !== undefined) {
522
+ host = definitionHosts.get(hostDefinitionId);
523
+ } else if (rootSessionId !== undefined) {
524
+ sessionId = rootSessionId;
525
+ host = hostSessions.get(hostDefinitionId);
526
+ } else {
527
+ sessionId = hostDefinitionId;
528
+ host = hostSessions.get(sessionId);
529
+ }
530
+ if (!host || hostSessions.get(sessionId) !== host) return;
531
+ host.releaseSession(sessionId);
532
+ if (hostSessions.get(sessionId) === host) hostSessions.delete(sessionId);
533
+ },
534
+ executeCode(source, sessionId, callId, model) {
535
+ return requiredSessionHost(sessionId).executeCode(source, sessionId, callId, model);
238
536
  },
239
- executeTool(name, input, sessionId, callId) {
240
- return requiredSessionHost(sessionId).executeTool(name, input, sessionId, callId);
537
+ waitCode(input, sessionId, callId) {
538
+ return requiredSessionHost(sessionId).waitCode(input, sessionId, callId);
241
539
  },
242
- toolMode(sessionId) {
243
- // The WASM constructor asks before its session is adopted.
244
- return (hostSessions.get(sessionId) ?? requiredActiveHost()).toolMode();
540
+ nextCodeUpdate(sessionId, callId) {
541
+ return requiredSessionHost(sessionId).nextCodeUpdate(sessionId, callId);
245
542
  },
246
- toolDefinitions(sessionId) {
543
+ executeTool(name, input, sessionId, callId, model) {
544
+ return requiredSessionHost(sessionId).executeTool(name, input, sessionId, callId, model);
545
+ },
546
+ beginCodeTurn(sessionId) {
547
+ hostSessions.get(sessionId)?.beginCodeTurn?.(sessionId);
548
+ },
549
+ cancelCodeTurn(sessionId) {
550
+ hostSessions.get(sessionId)?.cancelCodeTurn?.(sessionId);
551
+ },
552
+ cancelCode(sessionId) {
553
+ hostSessions.get(sessionId)?.cancelCode?.(sessionId);
554
+ },
555
+ readWorkspaceFile(path, sessionId) {
556
+ return requiredSessionHost(sessionId).readWorkspaceFile(path);
557
+ },
558
+ async listWorkspace(path, sessionId) {
559
+ return JSON.stringify(await requiredSessionHost(sessionId).listWorkspace(path));
560
+ },
561
+ writeWorkspaceFile(path, contents, sessionId) {
562
+ return requiredSessionHost(sessionId).writeWorkspaceFile(path, contents);
563
+ },
564
+ removeWorkspaceFile(path, sessionId) {
565
+ return requiredSessionHost(sessionId).removeWorkspaceFile(path);
566
+ },
567
+ async subscriptionLoad(subscriptionId) {
568
+ return (await loadSubscriptionRuntime()).load(subscriptionId);
569
+ },
570
+ async subscriptionCompareAndSwap(subscriptionId, expectedRevision, payload) {
571
+ return (await loadSubscriptionRuntime()).compareAndSwap(
572
+ subscriptionId,
573
+ expectedRevision,
574
+ payload,
575
+ );
576
+ },
577
+ async subscriptionRequest(subscriptionId, request) {
578
+ return (await loadSubscriptionRuntime()).request(subscriptionId, request);
579
+ },
580
+ toolMode(definitionHostId, sessionId) {
581
+ return requiredDefinitionHost(definitionHostId).toolMode(sessionId);
582
+ },
583
+ toolDefinitions(definitionHostId, sessionId) {
247
584
  // ModelRun builds its stable tool prefix inside the WASM constructor,
248
- // immediately before the returned session can be adopted. Runtime
249
- // factories activate their host directly around that synchronous step.
250
- return (hostSessions.get(sessionId) ?? requiredActiveHost()).toolDefinitions();
585
+ // before the returned session can be adopted. The private definition host
586
+ // keeps that lookup instance-scoped for roots and Rust-spawned children.
587
+ return requiredDefinitionHost(definitionHostId).toolDefinitions(sessionId);
251
588
  },
252
- emitEvent(eventJson) {
253
- const event = JSON.parse(eventJson);
254
- requiredSessionHost(event.request_id).emitEvent(eventJson);
589
+ async durabilityReadRecords(routeId, stateId, keys) {
590
+ return (await loadDurabilityRuntime()).readRecords(routeId, stateId, keys);
591
+ },
592
+ async durabilityReadRecord(routeId, stateId, key) {
593
+ return (await loadDurabilityRuntime()).readRecord(routeId, stateId, key);
594
+ },
595
+ async durabilityAcquire(routeId, stateId, ownerId) {
596
+ return (await loadDurabilityRuntime()).acquire(routeId, stateId, ownerId);
597
+ },
598
+ async durabilityReplace(
599
+ routeId,
600
+ stateId,
601
+ ownerId,
602
+ fence,
603
+ expectedRevision,
604
+ payload,
605
+ records,
606
+ ) {
607
+ return (await loadDurabilityRuntime()).replace(
608
+ routeId,
609
+ stateId,
610
+ ownerId,
611
+ fence,
612
+ expectedRevision,
613
+ payload,
614
+ records,
615
+ );
616
+ },
617
+ emitEvent(sessionId, eventJson, encodedBytes, encodedAgentId) {
618
+ requiredSessionHost(sessionId).emitEvent(
619
+ eventJson,
620
+ encodedBytes,
621
+ parseSubagentAgentId(encodedAgentId),
622
+ );
255
623
  },
256
624
  });
257
625
 
258
- function createAgent(raw, runtime) {
626
+ /** Decodes the string ABI without changing JavaScript's numeric AgentId contract. */
627
+ export function parseSubagentAgentId(encoded) {
628
+ if (encoded === undefined) return undefined;
629
+ if (typeof encoded !== "string" || !/^[1-9][0-9]*$/.test(encoded)) {
630
+ throw new TypeError("subagent agent ID must be a canonical positive decimal string");
631
+ }
632
+ const agentId = Number(encoded);
633
+ if (!Number.isSafeInteger(agentId)) {
634
+ throw new RangeError("subagent agent ID exceeds JavaScript's safe integer range");
635
+ }
636
+ return agentId;
637
+ }
638
+
639
+ export function loadSubscriptionRuntime() {
640
+ return import("./runtime/chatgpt-subscription.mjs");
641
+ }
642
+
643
+ function createAgent(
644
+ raw,
645
+ runtime,
646
+ requestedReservation,
647
+ reserveSession = runtime.reserveSessions !== false,
648
+ ) {
649
+ let reservation = requestedReservation;
259
650
  if (!raw || typeof raw.prompt !== "function") {
651
+ releaseAgentSession(reservation);
260
652
  throw new TypeError("the runtime returned an invalid Nanocodex agent handle");
261
653
  }
654
+ try {
655
+ if (reserveSession) {
656
+ reservation ??= reserveAgentSession(raw.sessionId);
657
+ adoptAgentSession(reservation, raw.sessionId);
658
+ }
659
+ } catch (error) {
660
+ const errors = [error];
661
+ runCleanup(errors, () => runtime.dispose(raw));
662
+ runCleanup(errors, () => releaseAgentSession(reservation));
663
+ throwCleanupErrors(errors);
664
+ }
262
665
  const state = {
263
666
  raw,
264
667
  runtime,
668
+ reservation,
669
+ releaseObservers: new Set(),
265
670
  disposed: false,
266
671
  released: false,
267
672
  shutdownPromise: undefined,
268
673
  subscriptions: new Set(),
674
+ agentId: typeof raw.agentId === "string" ? raw.agentId : raw.sessionId,
269
675
  sessionId: raw.sessionId,
270
676
  uid: `agent-${nextAgentUid++}`,
271
677
  };
272
678
  try {
273
679
  runtime.adopt?.(raw);
274
680
  } catch (error) {
275
- runtime.dispose(raw);
276
- throw error;
681
+ const errors = [error];
682
+ runCleanup(errors, () => runtime.dispose(raw));
683
+ runCleanup(errors, () => releaseAgentSession(reservation));
684
+ throwCleanupErrors(errors);
277
685
  }
278
686
  const agent = agentView(state, {});
279
- return runtime.decorate ? runtime.decorate(agent) : agent;
687
+ try {
688
+ return runtime.decorate ? runtime.decorate(agent, raw) : agent;
689
+ } catch (error) {
690
+ const errors = [error];
691
+ runCleanup(errors, () => releaseAgentState(state));
692
+ throwCleanupErrors(errors);
693
+ }
280
694
  }
281
695
 
282
696
  function agentView(state, extensions) {
283
697
  let agent;
698
+ let reservedKeys;
284
699
  const base = {
285
700
  uid: state.uid,
286
701
  key: state.runtime.key,
287
702
  name: state.runtime.name,
288
703
  type: state.runtime.type,
704
+ get agentId() { return state.agentId; },
289
705
  get sessionId() { return state.sessionId; },
290
706
  extend(fn) {
291
707
  if (typeof fn !== "function") throw new TypeError("agent.extend requires a decorator function");
@@ -294,7 +710,7 @@ function agentView(state, extensions) {
294
710
  throw new TypeError("an agent decorator must return an object");
295
711
  }
296
712
  const extension = { ...value };
297
- for (const key of Object.keys(base)) delete extension[key];
713
+ for (const key of reservedKeys) delete extension[key];
298
714
  return agentView(state, deepMerge(extensions, extension));
299
715
  },
300
716
  dispose() {
@@ -302,6 +718,7 @@ function agentView(state, extensions) {
302
718
  releaseAgentState(state);
303
719
  },
304
720
  };
721
+ reservedKeys = Object.keys(base);
305
722
  agent = Object.assign(base, extensions);
306
723
  agentStates.set(agent, state);
307
724
  return agent;
@@ -309,7 +726,15 @@ function agentView(state, extensions) {
309
726
 
310
727
  function requiredSessionHost(sessionId) {
311
728
  const host = hostSessions.get(sessionId);
312
- if (!host) throw new Error(`no Nanocodex host is active for session: ${sessionId}`);
729
+ if (!host || !cloudflareHostMayBindSubagent(host)) {
730
+ throw new Error(`no Nanocodex host is active for session: ${sessionId}`);
731
+ }
732
+ return host;
733
+ }
734
+
735
+ function requiredDefinitionHost(id) {
736
+ const host = definitionHosts.get(id);
737
+ if (!host) throw new Error(`no Nanocodex definition host is active: ${id}`);
313
738
  return host;
314
739
  }
315
740
 
@@ -323,9 +748,164 @@ function releaseAgentState(state) {
323
748
  }
324
749
  runCleanup(errors, () => state.runtime.release?.(state.raw));
325
750
  runCleanup(errors, () => state.runtime.dispose(state.raw));
751
+ runCleanup(errors, () => releaseAgentSession(state.reservation));
752
+ for (const observer of [...state.releaseObservers]) {
753
+ runCleanup(errors, () => observer({ graceful: state.shutdownPromise !== undefined }));
754
+ }
755
+ state.releaseObservers.clear();
326
756
  throwCleanupErrors(errors);
327
757
  }
328
758
 
759
+ /** Internal adapter seam: reserves a stable identity before starting its runtime. */
760
+ export function reserveAgentSession(sessionId) {
761
+ if (typeof sessionId !== "string" || !sessionId) {
762
+ throw new TypeError("sessionId must be a non-empty string");
763
+ }
764
+ if (activeAgentSessions.has(sessionId) || pendingCloudflareAgentSessions.has(sessionId)) {
765
+ throw new Error(`Nanocodex session ID is already active: ${sessionId}`);
766
+ }
767
+ const reservation = { sessionId, adopted: false, released: false };
768
+ activeAgentSessions.set(sessionId, reservation);
769
+ return reservation;
770
+ }
771
+
772
+ /** Internal Cloudflare seam: prepares, but does not activate, a reconstructed DO owner. */
773
+ export function prepareCloudflareAgentSession(sessionId, ownerId) {
774
+ if (typeof sessionId !== "string" || !sessionId) {
775
+ throw new TypeError("sessionId must be a non-empty string");
776
+ }
777
+ if (typeof ownerId !== "string" || !ownerId) {
778
+ throw new TypeError("Cloudflare Durable Object owner ID must be a non-empty string");
779
+ }
780
+ const existing = activeAgentSessions.get(sessionId);
781
+ if (pendingCloudflareAgentSessions.has(sessionId)
782
+ || (existing !== undefined
783
+ && (!cloudflareAgentSessions.has(existing)
784
+ || existing.ownerId !== ownerId
785
+ || !existing.committed))) {
786
+ throw new Error(`Nanocodex session ID is already active: ${sessionId}`);
787
+ }
788
+ const reservation = {
789
+ sessionId,
790
+ ownerId,
791
+ adopted: false,
792
+ committed: false,
793
+ predecessor: undefined,
794
+ predecessorHost: undefined,
795
+ host: undefined,
796
+ released: false,
797
+ };
798
+ cloudflareAgentSessions.add(reservation);
799
+ pendingCloudflareAgentSessions.set(sessionId, reservation);
800
+ return reservation;
801
+ }
802
+
803
+ /** @internal Whether this exact Cloudflare generation may publish child descriptors. */
804
+ export function mayBindCloudflareSubagentSession(reservation) {
805
+ return cloudflareAgentSessions.has(reservation)
806
+ && !reservation.released
807
+ && (pendingCloudflareAgentSessions.get(reservation.sessionId) === reservation
808
+ || activeAgentSessions.get(reservation.sessionId) === reservation);
809
+ }
810
+
811
+ /** @internal Whether this committed current generation may delete child descriptors. */
812
+ export function mayReleaseCloudflareSubagentSession(reservation) {
813
+ return cloudflareAgentSessions.has(reservation)
814
+ && !reservation.released
815
+ && reservation.committed
816
+ && activeAgentSessions.get(reservation.sessionId) === reservation;
817
+ }
818
+
819
+ function cloudflareHostMayBindSubagent(host) {
820
+ const reservation = cloudflareHostReservations.get(host);
821
+ return reservation === undefined || mayBindCloudflareSubagentSession(reservation);
822
+ }
823
+
824
+ /** Internal Cloudflare seam: activates a prepared owner after raw construction acquires its durable fence. */
825
+ export function activateCloudflareAgentSession(reservation) {
826
+ if (!cloudflareAgentSessions.has(reservation) || reservation.released) {
827
+ throw new Error("Cloudflare Agent session reservation is no longer active");
828
+ }
829
+ if (activeAgentSessions.get(reservation.sessionId) === reservation) return;
830
+ if (pendingCloudflareAgentSessions.get(reservation.sessionId) !== reservation) {
831
+ throw new Error("Cloudflare Agent session reservation is no longer pending");
832
+ }
833
+ const existing = activeAgentSessions.get(reservation.sessionId);
834
+ if (existing !== undefined
835
+ && (!cloudflareAgentSessions.has(existing)
836
+ || existing.ownerId !== reservation.ownerId
837
+ || !existing.committed)) {
838
+ throw new Error(`Nanocodex session ID is already active: ${reservation.sessionId}`);
839
+ }
840
+ reservation.predecessor = existing;
841
+ reservation.predecessorHost = hostSessions.get(reservation.sessionId);
842
+ activeAgentSessions.set(reservation.sessionId, reservation);
843
+ pendingCloudflareAgentSessions.delete(reservation.sessionId);
844
+ }
845
+
846
+ /** Internal Cloudflare seam: publishes a reconstructed owner after adapter setup succeeds. */
847
+ export function commitCloudflareAgentSession(reservation) {
848
+ if (!cloudflareAgentSessions.has(reservation)
849
+ || reservation.released
850
+ || !reservation.adopted
851
+ || activeAgentSessions.get(reservation.sessionId) !== reservation) {
852
+ throw new Error("Cloudflare Agent session reservation is not ready to commit");
853
+ }
854
+ reservation.committed = true;
855
+ reservation.predecessor = undefined;
856
+ reservation.predecessorHost = undefined;
857
+ }
858
+
859
+ function adoptAgentSession(reservation, sessionId) {
860
+ if (reservation.released || activeAgentSessions.get(reservation.sessionId) !== reservation) {
861
+ throw new Error(`Nanocodex session reservation is no longer active: ${sessionId}`);
862
+ }
863
+ if (reservation.sessionId !== sessionId) {
864
+ throw new Error(
865
+ `Nanocodex runtime changed reserved session ID ${reservation.sessionId} to ${sessionId}`,
866
+ );
867
+ }
868
+ if (reservation.adopted) {
869
+ throw new Error(`Nanocodex session reservation was already adopted: ${sessionId}`);
870
+ }
871
+ reservation.adopted = true;
872
+ }
873
+
874
+ /** Internal adapter seam: rolls back a runtime reservation that was not adopted. */
875
+ export function releaseAgentSession(reservation) {
876
+ if (!reservation || reservation.released) return;
877
+ reservation.released = true;
878
+ if (pendingCloudflareAgentSessions.get(reservation.sessionId) === reservation) {
879
+ pendingCloudflareAgentSessions.delete(reservation.sessionId);
880
+ }
881
+ if (activeAgentSessions.get(reservation.sessionId) === reservation) {
882
+ if (cloudflareAgentSessions.has(reservation)
883
+ && !reservation.committed
884
+ && reservation.predecessor !== undefined
885
+ && !reservation.predecessor.released) {
886
+ activeAgentSessions.set(reservation.sessionId, reservation.predecessor);
887
+ if (reservation.predecessorHost !== undefined) {
888
+ hostSessions.set(reservation.sessionId, reservation.predecessorHost);
889
+ }
890
+ } else {
891
+ activeAgentSessions.delete(reservation.sessionId);
892
+ if (hostSessions.get(reservation.sessionId) === reservation.host) {
893
+ hostSessions.delete(reservation.sessionId);
894
+ }
895
+ }
896
+ }
897
+ }
898
+
899
+ function cloudflareSessionOwner(reservation, sessionId) {
900
+ if (!cloudflareAgentSessions.has(reservation)
901
+ || reservation.released
902
+ || reservation.sessionId !== sessionId
903
+ || activeAgentSessions.get(sessionId) !== reservation) {
904
+ return undefined;
905
+ }
906
+ return reservation.ownerId;
907
+ }
908
+
329
909
  async function joinAgentShutdown(state) {
330
910
  await Promise.resolve();
331
911
  let shutdownFailed = false;
@@ -359,11 +939,6 @@ async function joinAgentShutdown(state) {
359
939
  if (cleanupFailed) throw cleanupError;
360
940
  }
361
941
 
362
- function requiredActiveHost() {
363
- if (!activeHost) throw new Error("no Nanocodex host is active");
364
- return activeHost;
365
- }
366
-
367
942
  function connectFailure(error) {
368
943
  if (typeof error === "string") {
369
944
  try {
@@ -398,11 +973,13 @@ function createTurn(raw, agent) {
398
973
  if (!raw || typeof raw.result !== "function") {
399
974
  throw new TypeError("the runtime returned an invalid Nanocodex turn handle");
400
975
  }
401
- const state = { raw, agent, result: undefined, disposed: false };
976
+ const state = { raw, agent, acceptance: undefined, result: undefined, disposed: false };
402
977
  const turn = {
403
978
  get agent() { return state.agent; },
979
+ accepted: () => awaitTurnAcceptance(turn),
404
980
  result: () => getTurnResult(turn),
405
981
  steer: (input) => steer(turn, input),
982
+ withdrawSteer: (options) => withdrawSteer(turn, options),
406
983
  cancel: () => cancel(turn),
407
984
  dispose() {
408
985
  if (state.disposed) return;
@@ -420,32 +997,66 @@ function createTurnResult(raw) {
420
997
  || typeof raw.finalMessage !== "string"
421
998
  || typeof raw.snapshot !== "function"
422
999
  || typeof raw.usage !== "function"
1000
+ || typeof raw.free !== "function"
423
1001
  ) {
424
1002
  raw?.free?.();
425
1003
  throw new TypeError("the runtime returned an invalid Nanocodex turn result");
426
1004
  }
427
1005
  const state = {
1006
+ disposed: false,
428
1007
  raw,
429
- snapshotValue: undefined,
430
- usageValue: undefined,
1008
+ snapshotPromise: undefined,
1009
+ usagePromise: undefined,
431
1010
  snapshot() {
432
- state.snapshotValue ||= freezeJson(JSON.parse(raw.snapshot()));
433
- return state.snapshotValue;
1011
+ if (state.disposed) return Promise.reject(new Error("the Nanocodex turn result has been disposed"));
1012
+ state.snapshotPromise ||= materializeTurnResultValue(state, "snapshot");
1013
+ return state.snapshotPromise;
434
1014
  },
435
1015
  usage() {
436
- state.usageValue ||= freezeJson(JSON.parse(raw.usage()));
437
- return state.usageValue;
1016
+ if (state.disposed) return Promise.reject(new Error("the Nanocodex turn result has been disposed"));
1017
+ state.usagePromise ||= materializeTurnResultValue(state, "usage");
1018
+ return state.usagePromise;
438
1019
  },
439
1020
  };
440
1021
  const result = {
441
1022
  finalMessage: raw.finalMessage,
442
- get snapshot() { return state.snapshot(); },
443
- get usage() { return state.usage(); },
1023
+ snapshot: () => state.snapshot(),
1024
+ usage: () => state.usage(),
1025
+ dispose() {
1026
+ if (state.disposed) return;
1027
+ state.disposed = true;
1028
+ resultFinalizer?.unregister(result);
1029
+ state.raw.free();
1030
+ },
444
1031
  };
445
1032
  resultStates.set(result, state);
1033
+ resultFinalizer?.register(result, raw, result);
446
1034
  return Object.freeze(result);
447
1035
  }
448
1036
 
1037
+ function materializeTurnResultValue(state, method) {
1038
+ let encoded;
1039
+ try {
1040
+ encoded = state.raw[method]();
1041
+ } catch (error) {
1042
+ return Promise.reject(error);
1043
+ }
1044
+ return Promise.resolve(encoded).then((value) => {
1045
+ if (typeof value !== "string") {
1046
+ throw new TypeError(`the runtime returned an invalid encoded turn ${method}`);
1047
+ }
1048
+ return freezeJson(JSON.parse(value));
1049
+ });
1050
+ }
1051
+
1052
+ async function encodedTurnResultValue(state, method) {
1053
+ const encoded = await state.raw[method]();
1054
+ if (typeof encoded !== "string") {
1055
+ throw new TypeError(`the runtime returned an invalid encoded turn ${method}`);
1056
+ }
1057
+ return encoded;
1058
+ }
1059
+
449
1060
  function agentState(agent) {
450
1061
  const state = knownAgentState(agent);
451
1062
  if (state.disposed) throw new Error("the Nanocodex agent has been disposed");
@@ -468,6 +1079,7 @@ function turnState(turn) {
468
1079
  function resultState(result) {
469
1080
  const state = resultStates.get(result);
470
1081
  if (!state) throw new TypeError("expected a completed Nanocodex turn result");
1082
+ if (state.disposed) throw new Error("the Nanocodex turn result has been disposed");
471
1083
  return state;
472
1084
  }
473
1085
 
@@ -512,7 +1124,7 @@ function throwCleanupErrors(errors) {
512
1124
  }
513
1125
  }
514
1126
 
515
- function freezeJson(value) {
1127
+ export function freezeJson(value) {
516
1128
  if (!value || typeof value !== "object") return value;
517
1129
  const pending = [value];
518
1130
  while (pending.length) {