nanocodex 0.6.5 → 0.6.6

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 (39) hide show
  1. package/README.md +15 -1
  2. package/browser/Voice.mjs +11 -4
  3. package/browser/VoiceSession.mjs +20 -6
  4. package/cloud/types.d.mts +1 -0
  5. package/cloudflare/Agent.d.mts +4 -0
  6. package/cloudflare/Agent.mjs +103 -9
  7. package/cloudflare/agent-settings.d.mts +1 -1
  8. package/cloudflare/agent-settings.mjs +3 -3
  9. package/cloudflare/gateway-binding-responses.mjs +3 -3
  10. package/cloudflare/gateway-responses.mjs +5 -5
  11. package/cloudflare/managed-auth.d.mts +1 -1
  12. package/cloudflare/managed-auth.mjs +6 -0
  13. package/cloudflare/tracing.d.mts +16 -0
  14. package/cloudflare/tracing.mjs +47 -0
  15. package/cloudflare/workers-ai-responses.d.mts +1 -1
  16. package/cloudflare/workers-ai-responses.mjs +3 -2
  17. package/internal.mjs +5 -0
  18. package/managed/Agent.d.mts +4 -1
  19. package/managed/Agent.mjs +22 -6
  20. package/managed/Voice.mjs +3 -1
  21. package/managed/internal.mjs +2 -1
  22. package/package.json +6 -2
  23. package/pkg-node/nanocodex.d.ts +14 -2
  24. package/pkg-node/nanocodex.js +73 -50
  25. package/pkg-node/nanocodex_bg.wasm.d.ts +5 -3
  26. package/pkg-web/.nanocodex-bindgen-stamp +2 -2
  27. package/pkg-web/nanocodex-build.json +1 -1
  28. package/pkg-web/nanocodex.d.ts +19 -5
  29. package/pkg-web/nanocodex.js +72 -50
  30. package/pkg-web/nanocodex_bg.js +72 -50
  31. package/pkg-web/nanocodex_bg.wasm +0 -0
  32. package/pkg-web/nanocodex_bg.wasm.d.ts +5 -3
  33. package/pkg-web/nanocodex_worker.js +1 -1
  34. package/runtime/mcp-runtime.mjs +5 -3
  35. package/runtime/response-controls.mjs +15 -4
  36. package/runtime/subagent-routing.mjs +2 -1
  37. package/tools/environment.d.mts +9 -0
  38. package/tools/environment.mjs +14 -1
  39. package/types.d.mts +3 -2
package/README.md CHANGED
@@ -252,6 +252,20 @@ normal browser Agent, an account-owned managed Agent, or a grant-scoped
252
252
  one-use ticket for that path. The browser binding never receives ChatGPT
253
253
  credentials or places its reusable grant bearer in a WebSocket URL.
254
254
 
255
+ ### Content-free Worker tracing
256
+
257
+ `nanocodex/cloudflare/tracing` wraps native `tracing.enterSpan(name, callback)`
258
+ with sanitized exception events. Use static operation names: they become
259
+ exception codes on failure. The original error is rethrown; its message and
260
+ stack are never passed to `recordException`.
261
+
262
+ `setSpanAttributes(span, attributes)` sets bulk metadata, while
263
+ `annotateActiveSpan(attributes)` annotates the current invocation or active
264
+ span without changing nesting. `recordSpanException(span, code)` handles
265
+ failures represented by results rather than thrown errors. Supply only static
266
+ codes and content-free attributes. On older workerd versions, these retain
267
+ individual attributes and `error.type` without requiring the September APIs.
268
+
255
269
  ### Durable Cloudflare Agent
256
270
 
257
271
  `nanocodex/cloudflare` is the standard Durable Object consumer. It keeps the
@@ -1167,7 +1181,7 @@ package manager or build step:
1167
1181
 
1168
1182
  ```html
1169
1183
  <script type="module">
1170
- import { Agent, Transport } from "https://cdn.jsdelivr.net/npm/nanocodex@0.6.5/host/index.mjs";
1184
+ import { Agent, Transport } from "https://cdn.jsdelivr.net/npm/nanocodex@0.6.6/host/index.mjs";
1171
1185
  const agent = await Agent.create({
1172
1186
  transport: Transport.hostManaged({
1173
1187
  websocketUrl: "/api/responses",
package/browser/Voice.mjs CHANGED
@@ -198,10 +198,17 @@ export function create(agent, options = {}) {
198
198
  // Media readiness updates the UI independently; start still waits for task
199
199
  // admission, and a later rejection closes media through the same error path.
200
200
  startPromise = next.start().then(publishReady).catch(async (cause) => {
201
- if (session === next) session = undefined;
202
- if (activeResources.get(agent) === resource) activeResources.delete(agent);
203
- cleanupWatcher();
204
- await next.close().catch(() => next.abort());
201
+ if (session === next) {
202
+ session = undefined;
203
+ if (activeResources.get(agent) === resource) activeResources.delete(agent);
204
+ cleanupWatcher();
205
+ }
206
+ // Keep replacement starts behind failed startup cleanup, just like stop().
207
+ // A stale attempt must never detach a replacement session's event watcher.
208
+ const closing = next.close().catch(() => next.abort());
209
+ if (generation === current) stopPromise = closing;
210
+ await closing;
211
+ if (stopPromise === closing) stopPromise = undefined;
205
212
  if (destroyed || generation !== current) return;
206
213
  if (attempt === 0 && cause?.code === "peer_connection_timeout") return start(parameters, 1);
207
214
  const error = cause instanceof Error ? cause : new Error(String(cause));
@@ -90,6 +90,7 @@ export class BrowserVoiceSession {
90
90
  #elevenLabs;
91
91
  #muted = false;
92
92
  #inputGeneration = 0;
93
+ #playbackRevision = 0;
93
94
  #meterTimer;
94
95
  #backendReady;
95
96
  #resolveBackendReady;
@@ -267,7 +268,7 @@ export class BrowserVoiceSession {
267
268
  const channel = this.#channel;
268
269
  channel.addEventListener("message", (event) => {
269
270
  if (this.#directControl && !this.#closed && this.#channel === channel) {
270
- this.#receiveControl(event.data, () => this.#channel === channel);
271
+ this.#receiveControl(event.data, () => this.#channel === channel && channel.readyState === "open");
271
272
  }
272
273
  });
273
274
  channel.addEventListener("close", () => {
@@ -311,6 +312,7 @@ export class BrowserVoiceSession {
311
312
  }
312
313
 
313
314
  noteTypedInput() {
315
+ this.#playbackRevision++;
314
316
  this.#playbackEnabled = false;
315
317
  this.#elevenLabs?.interrupt();
316
318
  this.#speaker?.setEnabled(false);
@@ -429,14 +431,19 @@ export class BrowserVoiceSession {
429
431
  for (const text of effects.undelivered_answers ?? []) this.#options.onUndeliveredAnswer?.(text);
430
432
  if (effects.input_generation !== undefined) {
431
433
  if (effects.input_generation < this.#inputGeneration) return;
432
- if (effects.input_generation > this.#inputGeneration) this.#elevenLabs?.interrupt();
434
+ if (effects.input_generation > this.#inputGeneration) {
435
+ this.#playbackRevision++;
436
+ this.#elevenLabs?.interrupt();
437
+ }
433
438
  this.#inputGeneration = effects.input_generation;
434
439
  }
435
440
  if (effects.playback_enabled === false) {
441
+ this.#playbackRevision++;
436
442
  this.#playbackEnabled = false;
437
443
  this.#elevenLabs?.interrupt();
438
444
  this.#speaker?.setEnabled(false);
439
445
  }
446
+ const playbackRevision = this.#playbackRevision;
440
447
  let sent = 0;
441
448
  for (const frame of effects.frames ?? []) {
442
449
  if (this.#directControl && this.#channel?.readyState === "open") {
@@ -448,12 +455,15 @@ export class BrowserVoiceSession {
448
455
  }
449
456
  }
450
457
  if (effects.acknowledge_frames && sent > 0) await this.#core?.framesSent(sent);
451
- if (!this.#closed && effects.playback_enabled === true && sent === (effects.frames?.length ?? 0)) {
458
+ // A live interruption can overtake asynchronous frame acknowledgement.
459
+ // Keep its speaker gate authoritative while retaining accepted captions.
460
+ const currentPlayback = playbackRevision === this.#playbackRevision;
461
+ if (currentPlayback && !this.#closed && effects.playback_enabled === true && sent === (effects.frames?.length ?? 0)) {
452
462
  this.#playbackEnabled = true;
453
463
  this.#speaker?.setEnabled(!this.#elevenLabs);
454
464
  }
455
465
  for (const entry of effects.transcripts ?? []) {
456
- if (this.#playbackEnabled && !this.#closed) this.#elevenLabs?.transcript(entry);
466
+ if (currentPlayback && this.#playbackEnabled && !this.#closed) this.#elevenLabs?.transcript(entry);
457
467
  this.#options.onTranscript(entry.speaker, entry.text, entry);
458
468
  }
459
469
  if (effects.status) this.#status(effects.status);
@@ -490,7 +500,7 @@ export class BrowserVoiceSession {
490
500
  let opened = false;
491
501
  sideband.addEventListener("message", (event) => {
492
502
  if (!this.#closed && generation === this.#sidebandGeneration) {
493
- this.#receiveControl(event.data, () => generation === this.#sidebandGeneration);
503
+ this.#receiveControl(event.data, () => generation === this.#sidebandGeneration && sideband.readyState === WebSocket.OPEN);
494
504
  }
495
505
  });
496
506
  sideband.addEventListener("close", () => {
@@ -516,7 +526,11 @@ export class BrowserVoiceSession {
516
526
  this.#applyLive(async () => {
517
527
  if (!this.#directControl) await this.#admission;
518
528
  if (this.#closed || !isCurrent()) return;
519
- if (await this.#core.requiresAgentAdmission(payload)) {
529
+ const requiresAdmission = await this.#core.requiresAgentAdmission(payload);
530
+ // Classification can yield while this transport closes or is replaced.
531
+ // Only requests accepted on the current connection may enter the queue.
532
+ if (this.#closed || !isCurrent()) return;
533
+ if (requiresAdmission) {
520
534
  // Retain accepted requests on close, but keep captions and interruptions
521
535
  // independent of the durable route and its ordered task queue.
522
536
  void this.#enqueue(async () => {
package/cloud/types.d.mts CHANGED
@@ -22,6 +22,7 @@ export type ConnectorConnection = Readonly<{
22
22
  label: string;
23
23
  accountId?: string | undefined;
24
24
  capabilities?: readonly CloudAccount[] | undefined;
25
+ scopes?: readonly string[] | undefined;
25
26
  }>;
26
27
 
27
28
  /** Provider-neutral status for a connector service capability. */
@@ -5,6 +5,7 @@ import type {
5
5
  AgentEvent,
6
6
  DefaultAgent,
7
7
  ToolConfiguration,
8
+ SessionSnapshot,
8
9
  } from "../types.mjs";
9
10
  import type { CloudflareDurableObjectStorage } from "../runtime/cloudflare-durability-store.mjs";
10
11
  import type { Tool as SubagentTool } from "../runtime/subagents.mjs";
@@ -52,6 +53,9 @@ export type Agent<extended extends object = {}> =
52
53
  ): Agent<extended & extension>;
53
54
  }>;
54
55
 
56
+ /** Copies the exact latest committed model boundary; rejects before the first safe boundary. */
57
+ export function checkpoint(agent: Agent): Promise<SessionSnapshot>;
58
+
55
59
  /** Removes the package-owned durable history for one Cloudflare Agent. */
56
60
  export function destroy(owner: DurableObjectOwner): void;
57
61
 
@@ -6,6 +6,7 @@ import { responseControlsBody, responseControlsSocket } from "../runtime/respons
6
6
  import * as HostAgent from "../host/Agent.mjs";
7
7
  import {
8
8
  CLOUDFLARE_SESSION_RESERVATION,
9
+ checkpoint as checkpointAgent,
9
10
  commitCloudflareAgentSession,
10
11
  installHostBridge,
11
12
  loadDurabilityRuntime,
@@ -38,6 +39,7 @@ import {
38
39
  const STARTUP_TIMEOUT_MS = 10_000;
39
40
  const INTERNAL_RUNTIME = Symbol.for("nanocodex.cloudflare.internalRuntime");
40
41
  const INTERNAL_CONFIGURATION = Symbol.for("nanocodex.cloudflare.internalConfiguration");
42
+ const INTERNAL_FORK_RESUME = Symbol.for("nanocodex.cloudflare.internalForkResume");
41
43
  const EPHEMERAL_APPLICATION_OPTIONS = new Set([
42
44
  "beforeCompaction",
43
45
  "additionalInstructions",
@@ -70,6 +72,7 @@ export function bindAgent(module, hostAgent = HostAgent) {
70
72
  pruneDurableReceipts: (owner, options) => pruneDurableReceipts(module, owner, options),
71
73
  create: (owner, options) => create(module, owner, options, hostAgent),
72
74
  createEphemeral: (owner, options) => createEphemeral(module, owner, options),
75
+ checkpoint,
73
76
  destroy,
74
77
  exportDurabilityState,
75
78
  exportDurabilityHead,
@@ -78,6 +81,11 @@ export function bindAgent(module, hostAgent = HostAgent) {
78
81
  });
79
82
  }
80
83
 
84
+ /** Copies the latest safe committed boundary as a resumable SessionSnapshot. */
85
+ export function checkpoint(agent) {
86
+ return checkpointAgent(agent);
87
+ }
88
+
81
89
  /** Atomically steers an active Cloudflare Agent turn or starts a new turn. */
82
90
  export function route(agent, options) {
83
91
  return routePrompt(agent, options);
@@ -122,6 +130,7 @@ export function destroy(owner) {
122
130
  stateId,
123
131
  );
124
132
  }
133
+ storage.sql.exec("DROP TABLE IF EXISTS nanocodex_cloudflare_fork_resume");
125
134
  clearCloudflareEventSocket(context);
126
135
  });
127
136
  }
@@ -294,7 +303,7 @@ async function createPrepared(module, resolved, options, hostAgent, lifecycle, p
294
303
  if (typeof prepare !== "function") throw new TypeError("Cloudflare Agent preparation must be a function");
295
304
  const configuration = options[INTERNAL_CONFIGURATION];
296
305
  validateInternalConfiguration(configuration);
297
- if (!configuration || !["gpt-6-astra", "gpt-6-sol", "gpt-6-luna"].includes(configuration.model)) {
306
+ if (!configuration || !["gpt-6-astra", "gpt-6.1-sol", "gpt-6-luna"].includes(configuration.model)) {
298
307
  throw new Error("Cloudflare Agent preparation requires a native root configuration");
299
308
  }
300
309
  const runtime = options[INTERNAL_RUNTIME];
@@ -304,7 +313,37 @@ async function createPrepared(module, resolved, options, hostAgent, lifecycle, p
304
313
  const signal = runtime.preparationSignal;
305
314
  signal?.throwIfAborted();
306
315
  createCloudflareDurabilityStore(resolved.context.storage);
316
+ initializeAgentStorage(resolved.context.storage);
317
+ const pristineAtStart = storedSessionId(resolved.context.storage) === undefined
318
+ && storedStateId(resolved.context.storage) === undefined;
319
+ const initialForkResume = options?.[INTERNAL_FORK_RESUME];
320
+ let initialForkDigest;
321
+ if (initialForkResume !== undefined) {
322
+ if (!initialForkResume || typeof initialForkResume !== "object" || Array.isArray(initialForkResume))
323
+ throw new TypeError("Cloudflare Agent fork resume must be a SessionSnapshot");
324
+ const bytes = new Uint8Array(await crypto.subtle.digest("SHA-256",
325
+ new TextEncoder().encode(JSON.stringify(initialForkResume))));
326
+ initialForkDigest = [...bytes].map(byte => byte.toString(16).padStart(2, "0")).join("");
327
+ }
307
328
  const { sessionId, stateId } = durableIdentity(resolved.context.storage, options.durabilityId);
329
+ if (initialForkDigest !== undefined) {
330
+ // Pin the seed before optional tool discovery/transport preparation. A
331
+ // failed preparation can reopen the exact fork with the same snapshot.
332
+ resolved.context.storage.sql.exec(`CREATE TABLE IF NOT EXISTS nanocodex_cloudflare_fork_resume (
333
+ singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
334
+ state_id TEXT NOT NULL, digest TEXT NOT NULL
335
+ )`);
336
+ const previous = resolved.context.storage.sql.exec(
337
+ "SELECT state_id, digest FROM nanocodex_cloudflare_fork_resume WHERE singleton = 1",
338
+ ).toArray()[0];
339
+ if ((!pristineAtStart && !previous) || (previous
340
+ && (previous.state_id !== stateId || previous.digest !== initialForkDigest))) {
341
+ throw new Error("Cloudflare Agent fork resume conflicts with its retained seed");
342
+ }
343
+ resolved.context.storage.sql.exec(
344
+ "INSERT OR IGNORE INTO nanocodex_cloudflare_fork_resume(singleton,state_id,digest) VALUES (1,?,?)",
345
+ stateId, initialForkDigest);
346
+ }
308
347
  const endpoint = cloudflareEgress({ binding: scopeCloudflareEgress(resolved.egress, resolved.subject) });
309
348
  const connection = prepareConnection(endpoint, sessionId, signal);
310
349
  let completing;
@@ -328,10 +367,12 @@ async function createPrepared(module, resolved, options, hostAgent, lifecycle, p
328
367
  if (prepared?.durabilityId !== stateId
329
368
  || ["model", "reasoning_mode"].some(key => pinned?.[key] !== configuration[key])
330
369
  || runtime?.prepare !== undefined || runtime?.workersAi !== undefined || runtime?.gateway !== undefined
331
- || (runtime?.inferenceForSession !== undefined && runtime?.preserveRootTransport !== true)) {
370
+ || (runtime?.inferenceForSession !== undefined && runtime?.preserveRootTransport !== true)
371
+ || (initialForkDigest !== undefined && JSON.stringify(prepared?.[INTERNAL_FORK_RESUME]) !== JSON.stringify(initialForkResume))) {
332
372
  throw new Error("Cloudflare Agent preparation changed its pinned transport or configuration");
333
373
  }
334
- return createOwned(module, resolved, prepared, hostAgent, lifecycle, connection);
374
+ return createOwned(module, resolved, prepared, hostAgent, lifecycle, connection,
375
+ pristineAtStart ? { sessionId, stateId } : undefined);
335
376
  });
336
377
  void completing.catch(() => {});
337
378
  return completing;
@@ -350,7 +391,7 @@ async function createPrepared(module, resolved, options, hostAgent, lifecycle, p
350
391
  }
351
392
  }
352
393
 
353
- async function createOwned(module, resolved, options, hostAgent, lifecycle, preparedConnection) {
394
+ async function createOwned(module, resolved, options, hostAgent, lifecycle, preparedConnection, pristinePreparedIdentity) {
354
395
  const { context, egress, subject } = resolved;
355
396
  const configured = applicationOptions(options);
356
397
  const {
@@ -358,6 +399,7 @@ async function createOwned(module, resolved, options, hostAgent, lifecycle, prep
358
399
  eventPersistence = "durable",
359
400
  [INTERNAL_RUNTIME]: internalRuntime,
360
401
  [INTERNAL_CONFIGURATION]: internalConfiguration,
402
+ [INTERNAL_FORK_RESUME]: forkResume,
361
403
  ...agentOptions
362
404
  } = configured;
363
405
  if (internalRuntime !== undefined
@@ -368,6 +410,10 @@ async function createOwned(module, resolved, options, hostAgent, lifecycle, prep
368
410
  && typeof internalRuntime.onSocketTiming !== "function") {
369
411
  throw new TypeError("Cloudflare Agent socket timing hook must be a function");
370
412
  }
413
+ if (internalRuntime?.onResponseCreateSent !== undefined
414
+ && typeof internalRuntime.onResponseCreateSent !== "function") {
415
+ throw new TypeError("Cloudflare Agent response.create sent hook must be a function");
416
+ }
371
417
  if (internalRuntime?.onRequestShape !== undefined
372
418
  && typeof internalRuntime.onRequestShape !== "function") {
373
419
  throw new TypeError("Cloudflare Agent request shape hook must be a function");
@@ -397,7 +443,54 @@ async function createOwned(module, resolved, options, hostAgent, lifecycle, prep
397
443
  : undefined;
398
444
  if (eventPersistence === "caller") clearCloudflareEventSocket(context);
399
445
  const durability = createCloudflareDurabilityStore(context.storage);
446
+ let resumeDigest;
447
+ if (forkResume !== undefined) {
448
+ if (!forkResume || typeof forkResume !== "object" || Array.isArray(forkResume)) {
449
+ throw new TypeError("Cloudflare Agent fork resume must be a SessionSnapshot");
450
+ }
451
+ initializeAgentStorage(context.storage);
452
+ context.storage.sql.exec(`CREATE TABLE IF NOT EXISTS nanocodex_cloudflare_fork_resume (
453
+ singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
454
+ state_id TEXT NOT NULL, digest TEXT NOT NULL
455
+ )`);
456
+ const digestBytes = new Uint8Array(await crypto.subtle.digest("SHA-256",
457
+ new TextEncoder().encode(JSON.stringify(forkResume))));
458
+ resumeDigest = [...digestBytes].map(byte => byte.toString(16).padStart(2, "0")).join("");
459
+ const recordedSeed = context.storage.sql.exec(
460
+ "SELECT state_id, digest FROM nanocodex_cloudflare_fork_resume WHERE singleton = 1",
461
+ ).toArray()[0];
462
+ const existingSessionId = storedSessionId(context.storage);
463
+ const existingStateId = storedStateId(context.storage);
464
+ // The preparation barrier may have installed the new identity already. A
465
+ // failed/restarted child construction may retain that identity and a
466
+ // revision-zero owner fence, but still has no executed checkpoint. The
467
+ // durable seed is unchanged; allow that cold retry and reject any real head.
468
+ if ((existingSessionId !== undefined || existingStateId !== undefined)
469
+ && !((pristinePreparedIdentity?.sessionId === existingSessionId
470
+ && pristinePreparedIdentity?.stateId === existingStateId)
471
+ || (existingSessionId !== undefined && existingStateId !== undefined
472
+ && (durabilityId === undefined || existingStateId === durabilityId)
473
+ && recordedSeed?.state_id === existingStateId
474
+ && recordedSeed?.digest === resumeDigest))) {
475
+ throw new Error("Cloudflare Agent fork resume requires a pristine Durable Object");
476
+ }
477
+ if (recordedSeed && (recordedSeed.digest !== resumeDigest
478
+ || (existingStateId !== undefined && recordedSeed.state_id !== existingStateId))) {
479
+ throw new Error("Cloudflare Agent fork resume conflicts with its retained seed");
480
+ }
481
+ const checkStateId = existingStateId ?? durabilityId;
482
+ if (checkStateId !== undefined && context.storage.sql.exec(
483
+ "SELECT revision, payload FROM nanocodex_durable_states WHERE state_id = ?", checkStateId,
484
+ ).toArray().some(row => row.revision !== "0" || row.payload !== null)) {
485
+ throw new Error("Cloudflare Agent fork resume requires pristine durability state");
486
+ }
487
+ }
400
488
  const { sessionId, stateId } = durableIdentity(context.storage, durabilityId);
489
+ if (resumeDigest !== undefined) {
490
+ context.storage.sql.exec(
491
+ "INSERT OR IGNORE INTO nanocodex_cloudflare_fork_resume(singleton,state_id,digest) VALUES (1,?,?)",
492
+ stateId, resumeDigest);
493
+ }
401
494
  const workersAi = internalRuntime?.workersAi;
402
495
  const gateway = internalRuntime?.gateway;
403
496
  if (gateway !== undefined && (workersAi !== undefined
@@ -439,7 +532,7 @@ async function createOwned(module, resolved, options, hostAgent, lifecycle, prep
439
532
  throw new Error("Native inference cannot replace a routed transport");
440
533
  }
441
534
  const parsed = JSON.parse(body);
442
- if (!["gpt-6-astra", "gpt-6-sol", "gpt-6-luna"].includes(parsed.model)
535
+ if (!["gpt-6-astra", "gpt-6.1-sol", "gpt-6-luna"].includes(parsed.model)
443
536
  || !["none", "low", "medium", "high", "xhigh", "max"].includes(parsed.reasoning?.effort)) {
444
537
  throw new Error("Native inference requires a supported GPT model and thinking");
445
538
  }
@@ -473,7 +566,7 @@ async function createOwned(module, resolved, options, hostAgent, lifecycle, prep
473
566
  }
474
567
  selected = createWorkersAiResponses(profile.workersAi.ai);
475
568
  } else {
476
- if (!["gpt-6-astra", "gpt-6-sol", "gpt-6-luna"].includes(profile.model)) {
569
+ if (!["gpt-6-astra", "gpt-6.1-sol", "gpt-6-luna"].includes(profile.model)) {
477
570
  throw new Error("Session model requires an explicit inference binding");
478
571
  }
479
572
  selected = frontierEndpoint;
@@ -500,7 +593,7 @@ async function createOwned(module, resolved, options, hostAgent, lifecycle, prep
500
593
  const opened = await (preparation === undefined
501
594
  ? endpoint.createWebSocket(url, id, request) : preparation.take(url, id, request));
502
595
  if (request.authorization === "preconnect") startup.resolve();
503
- return { ...opened, socket: responseControlsSocket(opened.socket, internalRuntime?.responseControls, internalRuntime?.onRequestShape) };
596
+ return { ...opened, socket: responseControlsSocket(opened.socket, internalRuntime?.responseControls, internalRuntime?.onRequestShape, internalRuntime?.onResponseCreateSent) };
504
597
  } catch (error) {
505
598
  if (request.authorization === "preconnect") startup.reject(error);
506
599
  throw error;
@@ -542,6 +635,7 @@ async function createOwned(module, resolved, options, hostAgent, lifecycle, prep
542
635
  sessionId,
543
636
  durability,
544
637
  durabilityId: stateId,
638
+ ...(forkResume === undefined ? {} : { resume: forkResume }),
545
639
  });
546
640
  // Managed voice needs the durable session before the separate Responses
547
641
  // relay is ready. Its preconnection remains owned by the host and a later
@@ -730,14 +824,14 @@ function validateInternalConfiguration(configuration) {
730
824
  "reasoning_mode",
731
825
  "fast_mode",
732
826
  ].includes(key))
733
- || !["gpt-6-sol", "gpt-6-luna", "gpt-6-astra", "@cf/zai-org/glm-5.3", "kimi-k3", "mimo-v2.6-pro"]
827
+ || !["gpt-6.1-sol", "gpt-6-luna", "gpt-6-astra", "@cf/zai-org/glm-5.3", "kimi-k3", "mimo-v2.6-pro"]
734
828
  .includes(configuration.model)
735
829
  || !["none", "low", "medium", "high", "xhigh", "max"].includes(configuration.thinking)
736
830
  || !["standard", "pro"].includes(configuration.reasoning_mode)
737
831
  || typeof configuration.fast_mode !== "boolean"
738
832
  || (["@cf/zai-org/glm-5.3", "kimi-k3", "mimo-v2.6-pro"].includes(configuration.model)
739
833
  && (!(configuration.model === "kimi-k3" ? ["low", "high"] : ["low", "medium", "high"]).includes(configuration.thinking) || configuration.reasoning_mode !== "standard"))
740
- || (configuration.model === "gpt-6-astra" && configuration.thinking === "none")) {
834
+ || (["gpt-6-astra", "gpt-6.1-sol"].includes(configuration.model) && configuration.thinking === "none")) {
741
835
  throw new TypeError("Cloudflare Agent internal configuration is invalid");
742
836
  }
743
837
  }
@@ -1,4 +1,4 @@
1
- export const AGENT_MODELS: readonly ["gpt-6-sol", "gpt-6-luna", "gpt-6-astra", "@cf/zai-org/glm-5.3", "kimi-k3", "mimo-v2.6-pro"];
1
+ export const AGENT_MODELS: readonly ["gpt-6.1-sol", "gpt-6-luna", "gpt-6-astra", "@cf/zai-org/glm-5.3", "kimi-k3", "mimo-v2.6-pro"];
2
2
  export const AGENT_THINKING: readonly ["none", "low", "medium", "high", "xhigh", "max"];
3
3
  export const AGENT_REASONING_MODES: readonly ["standard", "pro"];
4
4
  export type ManagedAgentSettings = Readonly<{ model: typeof AGENT_MODELS[number]; thinking: typeof AGENT_THINKING[number]; reasoning_mode: typeof AGENT_REASONING_MODES[number]; fast_mode: boolean }>;
@@ -1,5 +1,5 @@
1
1
  export const AGENT_MODELS = [
2
- "gpt-6-sol",
2
+ "gpt-6.1-sol",
3
3
  "gpt-6-luna",
4
4
  "gpt-6-astra",
5
5
  "@cf/zai-org/glm-5.3",
@@ -108,8 +108,8 @@ export function validateAgentSettings(settings) {
108
108
  && (!(settings.model === "kimi-k3" ? ["low", "high"] : ["low", "medium", "high"]).includes(settings.thinking) || settings.reasoning_mode !== "standard" || settings.fast_mode)) {
109
109
  throw new TypeError("Gateway model requires a supported effort, standard mode, and no fast mode");
110
110
  }
111
- if (settings.model === "gpt-6-astra" && settings.thinking === "none") {
112
- throw new TypeError("GPT-6 Astra requires low, medium, high, xhigh, or max thinking");
111
+ if (["gpt-6-astra", "gpt-6.1-sol"].includes(settings.model) && settings.thinking === "none") {
112
+ throw new TypeError("GPT-6 Astra and GPT-6.1 Sol require low, medium, high, xhigh, or max thinking");
113
113
  }
114
114
  if (settings.model === "gpt-6-astra" && settings.reasoning_mode === "pro") {
115
115
  throw new TypeError("GPT-6 Astra does not support pro reasoning mode");
@@ -23,9 +23,9 @@ export function toBindingResponsesInput(chat, effort) {
23
23
  }
24
24
  const payload = { input, stream: chat.stream === true, store: false, reasoning: { effort } };
25
25
  if (chat.tools) payload.tools = chat.tools.map(tool => ({ type: "function", ...tool.function,
26
- // Preserve optional/loose managed schemas; Responses otherwise normalizes
27
- // omitted strict to true, unlike the existing Chat transport.
28
- strict: false }));
26
+ // Responses normalizes omitted strict to true; preserve explicit strict
27
+ // functions while leaving legacy optional/loose schemas non-strict.
28
+ strict: tool.function.strict === true }));
29
29
  if (chat.tool_choice !== undefined) payload.tool_choice = typeof chat.tool_choice === "string"
30
30
  ? chat.tool_choice : { type: "function", name: chat.tool_choice.function.name };
31
31
  if (chat.max_completion_tokens !== undefined) payload.max_output_tokens = chat.max_completion_tokens;
@@ -6,7 +6,7 @@ const ENDPOINTS = Object.freeze({
6
6
  openrouter: "https://openrouter.ai/api/v1/chat/completions",
7
7
  vercel: "https://ai-gateway.vercel.sh/v1/chat/completions",
8
8
  });
9
- const MODELS = ["@cf/zai-org/glm-5.3", "gpt-6-astra", "gpt-6-sol", "gpt-6-luna", "kimi-k3", "mimo-v2.6-pro"];
9
+ const MODELS = ["@cf/zai-org/glm-5.3", "gpt-6-astra", "gpt-6.1-sol", "gpt-6-luna", "kimi-k3", "mimo-v2.6-pro"];
10
10
  const fail = message => { throw new Error(`Gateway Responses: ${message}`); };
11
11
 
12
12
  /** Server-side, full-history transport; no WebSocket or opaque compaction. */
@@ -14,7 +14,7 @@ export function createGatewayResponses(options) {
14
14
  const { provider, model, reasoningEffort, apiKey, fetch: fetchImpl = globalThis.fetch } = options;
15
15
  if (provider !== "cloudflare" && !Object.hasOwn(ENDPOINTS, provider)) fail("unsupported provider");
16
16
  if (!MODELS.includes(model)) fail("unsupported canonical model");
17
- if (!(["gpt-6-sol", "gpt-6-luna"].includes(model) ? ["none", "low", "medium", "high", "xhigh", "max"] : model === "kimi-k3" ? ["low", "high"] : ["low", "medium", "high"]).includes(reasoningEffort)) fail("unsupported reasoning effort");
17
+ if (!(model === "gpt-6-luna" ? ["none", "low", "medium", "high", "xhigh", "max"] : model === "gpt-6.1-sol" ? ["low", "medium", "high", "xhigh", "max"] : model === "kimi-k3" ? ["low", "high"] : ["low", "medium", "high"]).includes(reasoningEffort)) fail("unsupported reasoning effort");
18
18
  const cloudflareHttp = provider === "cloudflare" && (options.accountId !== undefined || apiKey !== undefined);
19
19
  if (provider === "cloudflare") {
20
20
  if (!model.startsWith("gpt-")) fail("Cloudflare gateway requires an OpenAI canonical model");
@@ -36,8 +36,8 @@ export function createGatewayResponses(options) {
36
36
  const adapter = (signal, attempt, reasoningMode) => createWorkersAiResponses({
37
37
  async run(_model, input) {
38
38
  signal?.throwIfAborted();
39
- if (provider !== "cloudflare" && ["gpt-6-sol", "gpt-6-luna"].includes(model) && reasoningEffort !== "none" && input.tools?.length) {
40
- fail("GPT-6 Sol/Luna function calling requires Responses or reasoning effort none");
39
+ if (provider !== "cloudflare" && input.tools?.length && (model === "gpt-6.1-sol" || model === "gpt-6-luna" && reasoningEffort !== "none")) {
40
+ fail("pinned Chat model/effort cannot call tools; use Responses");
41
41
  }
42
42
  if (input.reasoning_effort !== undefined && input.reasoning_effort !== reasoningEffort) fail("reasoning override does not match pinned effort");
43
43
  if (provider === "cloudflare" && !cloudflareHttp) {
@@ -160,7 +160,7 @@ export function createGatewayResponses(options) {
160
160
  // The portable Chat translator has no reasoning.mode field. Preserve it
161
161
  // explicitly on Responses transports and reject unsupported pro requests.
162
162
  const body = JSON.parse(request.body);
163
- const supportsMode = ["gpt-6-sol", "gpt-6-luna"].includes(model);
163
+ const supportsMode = ["gpt-6.1-sol", "gpt-6-luna"].includes(model);
164
164
  const validateMode = value => {
165
165
  if (value !== undefined && !["standard", "pro"].includes(value)) fail("unsupported reasoning mode");
166
166
  if (value === "pro" && (provider !== "cloudflare" || !supportsMode)) fail("pro reasoning requires a supported Responses model");
@@ -2,7 +2,7 @@ export type OrganizationCapability = "agents:read" | "agents:portability" | "age
2
2
  export type ApiKeyBase = Readonly<{ id: string; label: string; prefix: string; createdAt: number; digest: string; userId: string }>;
3
3
  export type StoredApiKey = ApiKeyBase & Readonly<{ organizationId: string; teamId: string; role: "owner" | "writer" | "reader"; capabilities: readonly OrganizationCapability[]; authorizationEpoch: number }>;
4
4
  export type ApiKeyPrincipal = Readonly<{ kind: "api_key"; userId: string; organizationId: string; teamId: string; role: StoredApiKey["role"]; subjectId: `api_key:${string}`; credentialId: string; authorizationEpoch: number; capabilities: readonly OrganizationCapability[] }>;
5
- export type AdmissionPrincipal = Readonly<{ kind: "api_key" | "account_session" | "connect_grant" | "service"; userId: string; organizationId: string; teamId: string; authorizationEpoch: number; capabilities: readonly string[]; connectGrant?: Readonly<{ grantId: string; connectors: readonly string[]; connectorConnections?: unknown; mcpIds: readonly string[]; appToolCatalogDigest?: string }> }>;
5
+ export type AdmissionPrincipal = Readonly<{ kind: "api_key" | "account_session" | "connect_grant" | "service"; userId: string; organizationId: string; teamId: string; authorizationEpoch: number; capabilities: readonly string[]; connectGrant?: Readonly<{ grantId: string; connectors: readonly string[]; connectorConnections?: unknown; mcpIds: readonly string[]; appToolCatalogDigest?: string; sandboxExecution?: true; outputCheckpoints?: true }> }>;
6
6
  export const API_KEY: RegExp;
7
7
  export function isUserId(value: unknown): value is string;
8
8
  export function isOrganizationCapabilities(value: unknown): value is readonly OrganizationCapability[];
@@ -77,10 +77,16 @@ export function forwardPrincipalAssertions(headers, principal) {
77
77
  CONNECT_CONNECTOR_CONNECTIONS_HEADER,
78
78
  CONNECT_MCP_IDS_HEADER,
79
79
  CONNECT_APP_TOOL_CATALOG_DIGEST_HEADER,
80
+ "x-nanocodex-connect-sandbox-execution",
81
+ "x-nanocodex-connect-output-checkpoints",
80
82
  ]) {
81
83
  headers.delete(name);
82
84
  }
83
85
  if (principal.connectGrant) {
86
+ if (principal.connectGrant.outputCheckpoints === true) headers.set("x-nanocodex-connect-output-checkpoints", "true");
87
+ if (principal.connectGrant.sandboxExecution === true) {
88
+ headers.set("x-nanocodex-connect-sandbox-execution", "true");
89
+ }
84
90
  headers.set(CONNECT_GRANT_ID_HEADER, principal.connectGrant.grantId);
85
91
  headers.set(CONNECT_CONNECTORS_HEADER, JSON.stringify(principal.connectGrant.connectors));
86
92
  if (principal.connectGrant.connectorConnections !== undefined) {
@@ -0,0 +1,16 @@
1
+ export type SpanAttributes = Record<string, string | number | boolean | undefined>;
2
+ export interface TraceSpan {
3
+ setAttribute(key: string, value: string | number | boolean): unknown;
4
+ setAttributes?(attributes: SpanAttributes): unknown;
5
+ recordException?(exception: { code: string }): void;
6
+ }
7
+ /** Only content-free metadata may be supplied. Uses individual attributes on older workerd. */
8
+ export declare function setSpanAttributes(span: TraceSpan, attributes: SpanAttributes): void;
9
+ /** Annotates the active native span when supported; does not create a span. */
10
+ export declare function annotateActiveSpan(attributes: SpanAttributes): void;
11
+ /** code must be a static operation label, never derived from a caught error. */
12
+ export declare function recordSpanException(span: TraceSpan, code: string): void;
13
+ /** Native nested spans that record sanitized failures and rethrow the original error. */
14
+ export declare const tracing: {
15
+ enterSpan<T, A extends unknown[]>(name: string, callback: (span: TraceSpan, ...args: A) => T, ...args: A): T;
16
+ };
@@ -0,0 +1,47 @@
1
+ import { tracing as runtimeTracing } from "cloudflare:workers";
2
+
3
+ /** Attributes must be content-free metadata, never prompts, credentials or bodies. */
4
+ export function setSpanAttributes(span, attributes) {
5
+ if (typeof span.setAttributes === "function") {
6
+ span.setAttributes(attributes);
7
+ } else {
8
+ // The pinned local workerd predates the September 2026 span additions.
9
+ for (const [key, value] of Object.entries(attributes)) {
10
+ if (value !== undefined) span.setAttribute(key, value);
11
+ }
12
+ }
13
+ }
14
+
15
+ /** Annotate the current invocation/context without creating another child span. */
16
+ export function annotateActiveSpan(attributes) {
17
+ const span = runtimeTracing.getActiveSpan?.();
18
+ if (span) setSpanAttributes(span, attributes);
19
+ }
20
+
21
+ /** code must be a static operation label, never derived from a caught error. */
22
+ export function recordSpanException(span, code) {
23
+ span.setAttribute("error.type", code);
24
+ // Error objects include their raw message and stack. These can contain secrets.
25
+ span.recordException?.({ code });
26
+ }
27
+
28
+ /** Native nesting and lifetime, with content-free exception events. */
29
+ export const tracing = {
30
+ enterSpan(name, callback, ...args) {
31
+ return runtimeTracing.enterSpan(name, span => {
32
+ try {
33
+ const result = callback(span, ...args);
34
+ if (result != null && typeof result.then === "function") {
35
+ return Promise.resolve(result).catch(error => {
36
+ recordSpanException(span, name);
37
+ throw error;
38
+ });
39
+ }
40
+ return result;
41
+ } catch (error) {
42
+ recordSpanException(span, name);
43
+ throw error;
44
+ }
45
+ });
46
+ },
47
+ };
@@ -1,6 +1,6 @@
1
1
  import type { BrowserHttpRequest } from "../browser/host.mjs";
2
2
 
3
- export type ResponsesCanonicalModel = "@cf/zai-org/glm-5.3" | "gpt-6-astra" | "gpt-6-sol" | "gpt-6-luna" | "kimi-k3" | "mimo-v2.6-pro";
3
+ export type ResponsesCanonicalModel = "@cf/zai-org/glm-5.3" | "gpt-6-astra" | "gpt-6.1-sol" | "gpt-6-luna" | "kimi-k3" | "mimo-v2.6-pro";
4
4
 
5
5
  /** Structural Workers AI binding; provider credentials stay with Cloudflare. */
6
6
  export type WorkersAiBinding<Model extends ResponsesCanonicalModel = "@cf/zai-org/glm-5.3"> = {
@@ -2,7 +2,7 @@ import { toolAliasFailure } from "./tool-alias-diagnostic.mjs";
2
2
  import { chatReasoningText } from "./chat-reasoning.mjs";
3
3
  import { providerStream, streamResponse } from "./provider-stream.mjs";
4
4
  const MODEL = "@cf/zai-org/glm-5.3";
5
- const MODELS = [MODEL, "gpt-6-astra", "gpt-6-sol", "gpt-6-luna", "kimi-k3", "mimo-v2.6-pro"];
5
+ const MODELS = [MODEL, "gpt-6-astra", "gpt-6.1-sol", "gpt-6-luna", "kimi-k3", "mimo-v2.6-pro"];
6
6
  const BASE = "https://workers-ai.invalid/v1";
7
7
  const fail = (message) => { throw new Error(`Workers AI Responses: ${message}`); };
8
8
  const json = (value) => typeof value === "string" ? value : JSON.stringify(value);
@@ -68,7 +68,7 @@ function translate(body, model) {
68
68
  if (!body || typeof body !== "object" || Array.isArray(body)) fail("expected a Responses request object");
69
69
  if (body.model !== undefined && body.model !== model) fail("unsupported model override; expected pinned model");
70
70
  const effort = (value) => {
71
- if (value !== undefined && !(["gpt-6-sol", "gpt-6-luna"].includes(model) ? ["none", "low", "medium", "high", "xhigh", "max"] : model === "kimi-k3" ? ["low", "high"] : ["low", "medium", "high"]).includes(value)) fail("unsupported reasoning effort for pinned model");
71
+ if (value !== undefined && !(model === "gpt-6-luna" ? ["none", "low", "medium", "high", "xhigh", "max"] : model === "gpt-6.1-sol" ? ["low", "medium", "high", "xhigh", "max"] : model === "kimi-k3" ? ["low", "high"] : ["low", "medium", "high"]).includes(value)) fail("unsupported reasoning effort for pinned model");
72
72
  return value;
73
73
  };
74
74
  if (body.previous_response_id) fail("previous_response_id is unsupported; send the complete Responses history");
@@ -100,6 +100,7 @@ function translate(body, model) {
100
100
  description: [namespace ? `${namespace}.${name}` : name, description, tool.description,
101
101
  tool.type === "custom" ? "Pass the exact free-form tool input as the JSON string field input. Preserve all code and newlines." : "",
102
102
  tool.format?.definition ? `Input grammar (${tool.format.syntax}): ${tool.format.definition}` : ""].filter(Boolean).join("\n"),
103
+ ...(tool.type === "function" && tool.strict === true ? { strict: true } : {}),
103
104
  parameters: tool.type === "custom" ? {
104
105
  type: "object", properties: { input: { type: "string" } }, required: ["input"], additionalProperties: false,
105
106
  } : tool.parameters ?? { type: "object", properties: {} },
package/internal.mjs CHANGED
@@ -229,6 +229,11 @@ export function compact(agent) {
229
229
  return agentState(agent).raw.compact();
230
230
  }
231
231
 
232
+ /** Copies the live agent's latest committed, resumable model boundary. */
233
+ export async function checkpoint(agent) {
234
+ return JSON.parse(await agentState(agent).raw.checkpoint());
235
+ }
236
+
232
237
  export async function context(agent) {
233
238
  return parseSessionContext(await agentState(agent).raw.context());
234
239
  }