nanocodex 0.6.4 → 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 (78) hide show
  1. package/README.md +37 -11
  2. package/browser/Agent.d.mts +1 -1
  3. package/browser/ElevenLabsPlayback.mjs +119 -0
  4. package/browser/InlineAgent.mjs +22 -17
  5. package/browser/Transport.d.mts +2 -0
  6. package/browser/Transport.mjs +3 -2
  7. package/browser/Voice.d.mts +4 -0
  8. package/browser/Voice.mjs +38 -6
  9. package/browser/VoiceSession.mjs +37 -7
  10. package/browser/host.d.mts +20 -4
  11. package/browser/host.mjs +156 -18
  12. package/cloud/types.d.mts +1 -0
  13. package/cloudflare/Agent.d.mts +14 -9
  14. package/cloudflare/Agent.mjs +354 -96
  15. package/cloudflare/agent-settings.d.mts +15 -0
  16. package/cloudflare/agent-settings.mjs +135 -0
  17. package/cloudflare/chat-reasoning.mjs +20 -0
  18. package/cloudflare/durable-placement.d.mts +12 -0
  19. package/cloudflare/durable-placement.mjs +82 -0
  20. package/cloudflare/egress.d.mts +31 -0
  21. package/cloudflare/gateway-binding-responses.mjs +84 -0
  22. package/cloudflare/gateway-responses.d.mts +48 -0
  23. package/cloudflare/gateway-responses.mjs +191 -0
  24. package/cloudflare/index.d.mts +6 -0
  25. package/cloudflare/index.mjs +3 -0
  26. package/cloudflare/managed-auth.d.mts +15 -0
  27. package/cloudflare/managed-auth.mjs +119 -0
  28. package/cloudflare/managed-live.d.mts +7 -0
  29. package/cloudflare/managed-live.mjs +56 -0
  30. package/cloudflare/prepared-connection.mjs +49 -0
  31. package/cloudflare/provider-stream.mjs +353 -0
  32. package/cloudflare/rpc.d.mts +9 -0
  33. package/cloudflare/rpc.mjs +14 -0
  34. package/cloudflare/tool-alias-diagnostic.mjs +21 -0
  35. package/cloudflare/tracing.d.mts +16 -0
  36. package/cloudflare/tracing.mjs +47 -0
  37. package/cloudflare/workers-ai-responses.d.mts +28 -0
  38. package/cloudflare/workers-ai-responses.mjs +399 -0
  39. package/host/Agent.d.mts +1 -1
  40. package/index.d.mts +2 -0
  41. package/internal.mjs +53 -14
  42. package/managed/Agent.d.mts +25 -69
  43. package/managed/Agent.mjs +82 -196
  44. package/managed/README.md +9 -38
  45. package/managed/Voice.mjs +5 -2
  46. package/managed/index.d.mts +0 -3
  47. package/managed/internal.mjs +2 -1
  48. package/node/Agent.d.mts +1 -1
  49. package/node/Agent.mjs +9 -3
  50. package/node/host.mjs +5 -0
  51. package/package.json +34 -2
  52. package/pkg-node/nanocodex.d.ts +22 -18
  53. package/pkg-node/nanocodex.js +118 -92
  54. package/pkg-node/nanocodex_bg.wasm.d.ts +6 -5
  55. package/pkg-web/.nanocodex-bindgen-stamp +7 -1
  56. package/pkg-web/nanocodex-build.json +1 -1
  57. package/pkg-web/nanocodex.d.ts +28 -23
  58. package/pkg-web/nanocodex.js +117 -91
  59. package/pkg-web/nanocodex_bg.js +117 -91
  60. package/pkg-web/nanocodex_bg.wasm +0 -0
  61. package/pkg-web/nanocodex_bg.wasm.d.ts +6 -5
  62. package/pkg-web/nanocodex_worker.js +1 -1
  63. package/runtime/before-compaction.mjs +49 -0
  64. package/runtime/code-evaluator.worker.mjs +2 -9
  65. package/runtime/managed-transport.mjs +10 -2
  66. package/runtime/mcp-runtime.mjs +20 -4
  67. package/runtime/quickjs-evaluator.mjs +11 -5
  68. package/runtime/response-controls.mjs +67 -4
  69. package/runtime/steer-receipt.mjs +13 -0
  70. package/runtime/subagent-routing.d.mts +38 -0
  71. package/runtime/subagent-routing.mjs +81 -0
  72. package/runtime/subagents.d.mts +1 -1
  73. package/runtime/tempo-provider.d.mts +1 -1
  74. package/runtime/tempo-provider.mjs +1 -1
  75. package/tools/environment.d.mts +17 -2
  76. package/tools/environment.mjs +30 -3
  77. package/tools/index.d.mts +2 -0
  78. package/types.d.mts +30 -2
package/browser/host.mjs CHANGED
@@ -1,3 +1,4 @@
1
+ import { createBeforeCompaction } from "../runtime/before-compaction.mjs";
1
2
  import { createResponsesHttp, responsesHttpHeaders } from "../runtime/responses-http.mjs";
2
3
  import { createCodeRuntime, toolResult } from "../runtime/code-runtime.mjs";
3
4
  import {
@@ -12,10 +13,17 @@ import { openHostManagedWebSocket } from "./hostManagedWebSocket.mjs";
12
13
  const DEFAULT_MAX_QUEUED_MESSAGES = 4_096;
13
14
  const DEFAULT_MAX_QUEUED_BYTES = 32 * 1024 * 1024;
14
15
  const DEFAULT_MAX_BUFFERED_SEND_BYTES = 16 * 1024 * 1024;
16
+ const SEND_BUFFER_WAIT_MS = 5_000;
17
+ const SEND_BUFFER_POLL_MS = 10;
15
18
  const MPP_CLIENT_PROTOCOL_ERROR_CLOSE_CODE = 3008;
16
19
  const WEBSOCKET_OPEN = 1;
17
20
 
18
21
  export function createBrowserHost(options = {}) {
22
+ const onSocketTiming = options.onSocketTiming;
23
+ if (onSocketTiming !== undefined && typeof onSocketTiming !== "function") {
24
+ throw new TypeError("host socket timing hook must be a function");
25
+ }
26
+ const preservation = createBeforeCompaction(options.beforeCompaction);
19
27
  const toolMode = options.toolMode ?? "code";
20
28
  if (toolMode !== "code" && toolMode !== "direct") {
21
29
  throw new TypeError("toolMode must be code or direct");
@@ -190,6 +198,7 @@ export function createBrowserHost(options = {}) {
190
198
  let settled = false;
191
199
  const connection = {
192
200
  socket,
201
+ timing: socketTiming(),
193
202
  queue: [],
194
203
  queuedBytes: 0,
195
204
  waiter: undefined,
@@ -200,6 +209,7 @@ export function createBrowserHost(options = {}) {
200
209
  if (settled) return;
201
210
  settled = true;
202
211
  connectingConnections.delete(connection);
212
+ finishSocketTiming(connection);
203
213
  reject(error);
204
214
  };
205
215
  connection.reject = rejectConnection;
@@ -234,6 +244,7 @@ export function createBrowserHost(options = {}) {
234
244
  : { kind: "binary" });
235
245
  });
236
246
  socket.addEventListener("close", (event) => {
247
+ connection.wakeSend?.();
237
248
  if (!settled) {
238
249
  rejectConnection(new Error(`WebSocket closed during connection with code ${event.code}`));
239
250
  } else if (!connection.intentionallyClosed && !connection.overflowed) {
@@ -267,6 +278,7 @@ export function createBrowserHost(options = {}) {
267
278
  const handle = nextHandle++;
268
279
  const connection = {
269
280
  socket,
281
+ timing: socketTiming(),
270
282
  queue: [],
271
283
  queuedBytes: 0,
272
284
  waiter: undefined,
@@ -300,37 +312,66 @@ export function createBrowserHost(options = {}) {
300
312
  return JSON.stringify({ handle, status: 101, reasoning_included: false });
301
313
  }
302
314
 
303
- function send(handle, message) {
315
+ async function send(handle, message) {
304
316
  const connection = connections.get(handle);
305
- if (!connection || connection.socket.readyState !== WEBSOCKET_OPEN) {
306
- return Promise.resolve(JSON.stringify({
307
- ok: false,
308
- reconnectable: true,
309
- error: "WebSocket is no longer open",
310
- }));
317
+ const closed = () => disposal || !connection || connections.get(handle) !== connection
318
+ || connection.socket.readyState !== WEBSOCKET_OPEN;
319
+ const closedResult = () => JSON.stringify({
320
+ ok: false,
321
+ reconnectable: true,
322
+ error: "WebSocket is no longer open",
323
+ });
324
+ if (closed()) return closedResult();
325
+ // The runtime sends one request at a time. Do not retain an unbounded queue
326
+ // of request bodies while the underlying socket is under pressure.
327
+ if (connection.sending) {
328
+ return JSON.stringify({ ok: false, reconnectable: false,
329
+ error: "concurrent WebSocket sends are unsupported" });
311
330
  }
331
+ connection.sending = true;
312
332
  try {
313
333
  if (connection.managed) {
314
334
  connection.socket.send(JSON.stringify({ mpp: "message", data: message }));
315
- return Promise.resolve(JSON.stringify({ ok: true }));
335
+ return JSON.stringify({ ok: true });
316
336
  }
317
337
  const frameBytes = utf8ByteLength(message);
318
- if (frameBytes > maxBufferedSendBytes
319
- || connection.socket.bufferedAmount + frameBytes > maxBufferedSendBytes) {
320
- return Promise.resolve(JSON.stringify({
338
+ if (frameBytes > maxBufferedSendBytes) {
339
+ return JSON.stringify({
321
340
  ok: false,
322
341
  reconnectable: false,
323
- error: `buffered WebSocket sends exceeded ${maxBufferedSendBytes} bytes`,
324
- }));
342
+ error: `WebSocket frame size ${frameBytes} bytes exceeds ${maxBufferedSendBytes} bytes (buffered ${connection.socket.bufferedAmount} bytes)`,
343
+ });
344
+ }
345
+ const deadline = Date.now() + SEND_BUFFER_WAIT_MS;
346
+ while (connection.socket.bufferedAmount + frameBytes > maxBufferedSendBytes) {
347
+ if (closed()) return closedResult();
348
+ if (Date.now() >= deadline) {
349
+ // Nothing was sent. The transport may reconnect, but this invocation
350
+ // must never send later after reporting a retryable failure.
351
+ return JSON.stringify({ ok: false, reconnectable: true,
352
+ error: `WebSocket send buffer did not drain within ${SEND_BUFFER_WAIT_MS} ms (frame ${frameBytes} bytes, buffered ${connection.socket.bufferedAmount} bytes, limit ${maxBufferedSendBytes} bytes)` });
353
+ }
354
+ await new Promise((resolve) => {
355
+ const timer = setTimeout(wake, SEND_BUFFER_POLL_MS);
356
+ function wake() {
357
+ clearTimeout(timer);
358
+ connection.wakeSend = undefined;
359
+ resolve();
360
+ }
361
+ connection.wakeSend = wake;
362
+ });
325
363
  }
364
+ if (closed()) return closedResult();
326
365
  connection.socket.send(message);
327
- return Promise.resolve(JSON.stringify({ ok: true }));
366
+ return JSON.stringify({ ok: true });
328
367
  } catch (error) {
329
- return Promise.resolve(JSON.stringify({
368
+ return JSON.stringify({
330
369
  ok: false,
331
370
  reconnectable: connection.socket.readyState !== WEBSOCKET_OPEN,
332
371
  error: error instanceof Error ? error.message : String(error),
333
- }));
372
+ });
373
+ } finally {
374
+ connection.sending = false;
334
375
  }
335
376
  }
336
377
 
@@ -342,6 +383,13 @@ export function createBrowserHost(options = {}) {
342
383
  if (connection.queue.length) {
343
384
  const entry = connection.queue.shift();
344
385
  connection.queuedBytes -= entry.bytes;
386
+ if (entry.enqueuedAt !== undefined && connection.timing) {
387
+ const metrics = connection.timing.metrics;
388
+ const residenceMs = Math.max(0, performance.now() - entry.enqueuedAt);
389
+ metrics.delivered_message_count += 1;
390
+ metrics.queue_residence_total_ms += residenceMs;
391
+ metrics.queue_residence_max_ms = Math.max(metrics.queue_residence_max_ms, residenceMs);
392
+ }
345
393
  return Promise.resolve(JSON.stringify(entry.message));
346
394
  }
347
395
  if (connection.waiter) return Promise.reject(new Error("concurrent reads are unsupported"));
@@ -358,13 +406,32 @@ export function createBrowserHost(options = {}) {
358
406
  if (!connection) return;
359
407
  connections.delete(handle);
360
408
  connection.intentionallyClosed = true;
409
+ connection.wakeSend?.();
361
410
  connection.waiter?.({ kind: "closed", detail: "by the WASM runtime" });
411
+ finishSocketTiming(connection);
412
+ connection.queue.length = 0;
413
+ connection.queuedBytes = 0;
362
414
  return connection.socket.close();
363
415
  }
364
416
 
365
417
  function enqueue(connection, message) {
366
- if (connection.overflowed) return;
418
+ if (connection.overflowed || connection.intentionallyClosed) return;
419
+ const timing = connection.timing;
420
+ const dataMessage = message.kind === "text" || message.kind === "binary";
421
+ const enqueuedAt = timing && dataMessage && !connection.waiter ? performance.now() : undefined;
422
+ if (timing && dataMessage) {
423
+ timing.metrics.message_count += 1;
424
+ // Only the exact metadata event can produce diagnostics; deltas stay opaque.
425
+ if (message.kind === "text" && timing.provider.size < 32) {
426
+ const metadata = providerSocketTiming(message.text);
427
+ if (metadata) {
428
+ const key = metadata.response_id ?? timing.provider.size;
429
+ if (!timing.provider.has(key)) timing.provider.set(key, metadata);
430
+ }
431
+ }
432
+ }
367
433
  if (connection.waiter) {
434
+ if (timing && dataMessage) timing.metrics.delivered_message_count += 1;
368
435
  connection.waiter(message);
369
436
  return;
370
437
  }
@@ -383,12 +450,50 @@ export function createBrowserHost(options = {}) {
383
450
  connection.socket.close(1009, "receive queue exceeded configured bounds");
384
451
  return;
385
452
  }
386
- connection.queue.push({ message, bytes });
453
+ const entry = { message, bytes };
454
+ if (enqueuedAt !== undefined) {
455
+ entry.enqueuedAt = enqueuedAt;
456
+ timing.metrics.buffered_message_count += 1;
457
+ }
458
+ connection.queue.push(entry);
387
459
  connection.queuedBytes += bytes;
388
460
  }
389
461
 
462
+ // No counters, timestamps, metadata parsing or observations without the internal hook.
463
+ function socketTiming() {
464
+ return onSocketTiming === undefined ? undefined : {
465
+ metrics: {
466
+ message_count: 0,
467
+ delivered_message_count: 0,
468
+ buffered_message_count: 0,
469
+ queue_residence_total_ms: 0,
470
+ queue_residence_max_ms: 0,
471
+ },
472
+ provider: new Map(),
473
+ };
474
+ }
475
+
476
+ function finishSocketTiming(connection) {
477
+ const timing = connection.timing;
478
+ if (!timing) return;
479
+ // Finalize owned consumption, not the remote close event: buffered frames may
480
+ // still be drained after that event. Clear first for reentrant close/dispose.
481
+ connection.timing = undefined;
482
+ const metrics = timing.metrics;
483
+ try {
484
+ const result = onSocketTiming({
485
+ ...metrics,
486
+ discarded_message_count: metrics.message_count - metrics.delivered_message_count,
487
+ provider_timings: [...timing.provider.values()],
488
+ });
489
+ // Diagnostics must never fail transport cleanup, including an async hook.
490
+ if (result?.then) void Promise.resolve(result).catch(() => {});
491
+ } catch { /* Passive observations cannot fail transport cleanup. */ }
492
+ }
493
+
390
494
  function dispose() {
391
495
  if (disposal) return disposal;
496
+ preservation.dispose();
392
497
  http.dispose();
393
498
  disposalError = new Error("Nanocodex host was disposed during WebSocket connection");
394
499
  disposal = Promise.resolve().then(async () => {
@@ -495,6 +600,8 @@ export function createBrowserHost(options = {}) {
495
600
  if (references > 0) references -= 1;
496
601
  return references === 0 ? dispose() : Promise.resolve();
497
602
  },
603
+ beforeCompaction: preservation.preserve,
604
+ cancelBeforeCompaction: preservation.cancel,
498
605
  httpOpen: http.httpOpen,
499
606
  httpReady: http.httpReady,
500
607
  httpNext: http.httpNext,
@@ -511,6 +618,14 @@ export function createBrowserHost(options = {}) {
511
618
  cancelCodeTurn: code.cancelTurn,
512
619
  nextCodeUpdate: code.nextCodeUpdate,
513
620
  executeTool: code.executeTool,
621
+ routeSubagent: (request) => {
622
+ if (!options.subagentRouting) throw new Error("subagent routing is not configured");
623
+ return options.subagentRouting.resolve(request);
624
+ },
625
+ bindSubagentRoute: (request) => {
626
+ if (!options.subagentRouting) throw new Error("subagent routing is not configured");
627
+ return options.subagentRouting.bind(request);
628
+ },
514
629
  bindSubagentSession: code.bindSubagentSession,
515
630
  cancelCode: code.cancel,
516
631
  readWorkspaceFile: async (path) => {
@@ -554,3 +669,26 @@ function normalizeWebSocketConnection(opened) {
554
669
  }
555
670
  return { socket: opened };
556
671
  }
672
+
673
+ // Provider-reported fields are nested spans, not additive measurements. Bound the
674
+ // diagnostic parser and retain only three finite durations plus a response ID.
675
+ function providerSocketTiming(text) {
676
+ if (text.length > 16_384 || !text.includes('"responsesapi.websocket_timing"')) return;
677
+ let event;
678
+ try { event = JSON.parse(text); } catch { return; }
679
+ if (event?.type !== "responsesapi.websocket_timing"
680
+ || (event.response_id !== undefined && (typeof event.response_id !== "string"
681
+ || !/^resp_[A-Za-z0-9_-]{1,128}$/.test(event.response_id)))
682
+ || !event.timing_metrics || typeof event.timing_metrics !== "object"
683
+ || Array.isArray(event.timing_metrics)) return;
684
+ const timing = {};
685
+ for (const key of ["pre_inference_ms", "engine_queue_max_ms", "engine_service_ttft_total_ms"]) {
686
+ const value = event.timing_metrics[key];
687
+ if (typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 86_400_000) {
688
+ timing[key] = value;
689
+ }
690
+ }
691
+ if (Object.keys(timing).length) return {
692
+ ...(event.response_id === undefined ? {} : { response_id: event.response_id }), ...timing,
693
+ };
694
+ }
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";
@@ -24,14 +25,6 @@ export type DurableObjectContext = Readonly<{
24
25
  /** The owning Cloudflare Durable Object instance. Runtime fields remain adapter-private. */
25
26
  export type DurableObjectOwner = object;
26
27
 
27
- /** Rust-owned first-prompt retrieval policy; the host owns authenticated execution. */
28
- export type BootstrapPlan = Readonly<{
29
- query: string;
30
- voice_bootstrap: boolean;
31
- calls: readonly Readonly<{ name: "find_session" | "memory"; arguments: unknown }>[];
32
- }>;
33
- export function bootstrapPlan(input: string): Promise<BootstrapPlan>;
34
-
35
28
  export type EventFrame = Readonly<{
36
29
  cursor: string;
37
30
  event: AgentEvent;
@@ -60,6 +53,9 @@ export type Agent<extended extends object = {}> =
60
53
  ): Agent<extended & extension>;
61
54
  }>;
62
55
 
56
+ /** Copies the exact latest committed model boundary; rejects before the first safe boundary. */
57
+ export function checkpoint(agent: Agent): Promise<SessionSnapshot>;
58
+
63
59
  /** Removes the package-owned durable history for one Cloudflare Agent. */
64
60
  export function destroy(owner: DurableObjectOwner): void;
65
61
 
@@ -100,6 +96,8 @@ export function create(owner: create.Owner, options?: create.Options): Promise<c
100
96
  export declare namespace create {
101
97
  type Owner = DurableObjectOwner;
102
98
  type Options = Readonly<{
99
+ /** Awaited host preservation barrier; scoped to this root, never inherited by children. */
100
+ beforeCompaction?: AgentOptions["beforeCompaction"];
103
101
  /** Stable portable state identity. It cannot change after first construction or import. */
104
102
  durabilityId?: string | undefined;
105
103
  /**
@@ -115,7 +113,7 @@ export declare namespace create {
115
113
  * The caller must preserve older exact-ID results before selecting this.
116
114
  */
117
115
  terminalReceiptRetention?: number | undefined;
118
- /** Live child execution is in-memory; interrupted descriptors survive reconstruction. */
116
+ /** Children exist only in memory and are discarded when the root runtime shuts down. */
119
117
  tools?: ToolConfiguration<SubagentTool> | undefined;
120
118
  }>;
121
119
  type ReturnType = Agent;
@@ -134,3 +132,10 @@ export declare namespace createEphemeral {
134
132
  }>;
135
133
  type ReturnType = DefaultAgent;
136
134
  }
135
+
136
+ /** Reads a correlated receipt without acquiring or restoring the agent. */
137
+ export function steerReceipt(owner: DurableObjectOwner, operationId: string, messageId: string): Readonly<{
138
+ input_key: string; index: number; withdrawn: boolean;
139
+ }> | null;
140
+ /** Fingerprint of the exact browser Prompt serialization retained by Rust. */
141
+ export function steerInputKey(input: import("../types.mjs").PromptInput): Promise<string>;