nanocodex 0.6.6 → 0.6.7

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 (114) hide show
  1. package/README.md +297 -6
  2. package/THIRD-PARTY-LICENSES +23 -0
  3. package/browser/Agent.d.mts +5 -4
  4. package/browser/Agent.mjs +4 -0
  5. package/browser/Claude.d.mts +4 -0
  6. package/browser/Claude.mjs +11 -0
  7. package/browser/InlineAgent.mjs +19 -1
  8. package/browser/host.d.mts +54 -1
  9. package/browser/host.mjs +65 -16
  10. package/browser/hostManagedWebSocket.mjs +11 -2
  11. package/browser/index.d.mts +3 -10
  12. package/browser/index.mjs +2 -5
  13. package/browser/socket-observations.mjs +203 -0
  14. package/cloud/Decorator.d.mts +2 -2
  15. package/cloud/Decorator.mjs +1 -0
  16. package/cloud/Dialog.d.mts +13 -0
  17. package/cloud/Dialog.mjs +52 -6
  18. package/cloud/Transport.d.mts +0 -11
  19. package/cloud/Transport.mjs +1 -386
  20. package/cloud/actions/account.d.mts +12 -0
  21. package/cloud/actions/account.mjs +11 -0
  22. package/cloud/actions/agent.mjs +3 -0
  23. package/cloud/actions/connection.d.mts +3 -1
  24. package/cloud/actions/connection.mjs +19 -4
  25. package/cloud/index.d.mts +1 -1
  26. package/cloud/index.mjs +1 -1
  27. package/cloud/internal.mjs +19 -4
  28. package/cloud/mercator.mjs +1 -1
  29. package/cloud/mock.d.mts +17 -0
  30. package/cloud/mock.mjs +392 -0
  31. package/cloud/types.d.mts +3 -1
  32. package/cloudflare/Agent.d.mts +2 -0
  33. package/cloudflare/Agent.mjs +88 -2
  34. package/cloudflare/agent-settings.d.mts +1 -1
  35. package/cloudflare/agent-settings.mjs +9 -0
  36. package/cloudflare/egress.mjs +9 -6
  37. package/cloudflare/gateway-responses.d.mts +1 -0
  38. package/cloudflare/gateway-responses.mjs +2 -2
  39. package/cloudflare/managed-auth.d.mts +4 -4
  40. package/cloudflare/managed-auth.mjs +14 -2
  41. package/cloudflare/managed-live.d.mts +6 -0
  42. package/cloudflare/managed-live.mjs +56 -1
  43. package/cloudflare/workers-ai-responses.d.mts +2 -1
  44. package/cloudflare/workers-ai-responses.mjs +80 -6
  45. package/host/Agent.d.mts +6 -2
  46. package/host/Claude.d.mts +3 -0
  47. package/host/Claude.mjs +2 -0
  48. package/host/index.d.mts +6 -10
  49. package/host/index.mjs +2 -5
  50. package/index.d.mts +11 -10
  51. package/index.mjs +0 -5
  52. package/internal.mjs +28 -3
  53. package/managed/Agent.d.mts +6 -2
  54. package/managed/Agent.mjs +39 -5
  55. package/managed/ManagedError.d.mts +3 -0
  56. package/node/Agent.d.mts +6 -2
  57. package/node/Agent.mjs +14 -1
  58. package/node/Claude.d.mts +4 -0
  59. package/node/Claude.mjs +24 -0
  60. package/node/host.mjs +18 -9
  61. package/node/index.d.mts +6 -10
  62. package/node/index.mjs +2 -5
  63. package/package.json +41 -9
  64. package/pkg-node/nanocodex.d.ts +109 -0
  65. package/pkg-node/nanocodex.js +818 -51
  66. package/pkg-node/nanocodex_bg.wasm.d.ts +47 -3
  67. package/pkg-web/.nanocodex-bindgen-stamp +3 -3
  68. package/pkg-web/nanocodex-build.json +1 -1
  69. package/pkg-web/nanocodex.d.ts +156 -3
  70. package/pkg-web/nanocodex.js +813 -51
  71. package/pkg-web/nanocodex_bg.js +813 -51
  72. package/pkg-web/nanocodex_bg.wasm +0 -0
  73. package/pkg-web/nanocodex_bg.wasm.d.ts +47 -3
  74. package/pkg-web/nanocodex_worker.js +1 -1
  75. package/runtime/claude-host.mjs +308 -0
  76. package/runtime/claude-subscription.mjs +79 -0
  77. package/runtime/claude.d.mts +112 -0
  78. package/runtime/claude.mjs +221 -0
  79. package/runtime/code-effect-identity.mjs +123 -0
  80. package/runtime/harnesses.mjs +51 -0
  81. package/runtime/mcp-payment.mjs +8 -0
  82. package/runtime/mcp-runtime.d.mts +10 -0
  83. package/runtime/mcp-runtime.mjs +242 -57
  84. package/runtime/mercator.mjs +2 -0
  85. package/runtime/quickjs-evaluator.mjs +23 -11
  86. package/runtime/response-controls.mjs +1 -6
  87. package/runtime/responses-http.mjs +4 -2
  88. package/runtime/retry-after.d.mts +10 -0
  89. package/runtime/retry-after.mjs +86 -0
  90. package/runtime/steer-receipt.mjs +8 -1
  91. package/runtime/subagent-routing.d.mts +4 -2
  92. package/runtime/subagent-routing.mjs +7 -2
  93. package/runtime/subagents.d.mts +8 -3
  94. package/services/account.d.mts +76 -0
  95. package/services/account.mjs +151 -0
  96. package/services/hosted.mjs +72 -0
  97. package/services/index.d.mts +95 -0
  98. package/services/index.mjs +192 -0
  99. package/services/scope.mjs +36 -0
  100. package/{runtime/tempo-provider.d.mts → tempo/index.d.mts} +20 -1
  101. package/{runtime/tempo-provider.mjs → tempo/index.mjs} +39 -3
  102. package/tools/browser/browserShell.mjs +18 -8
  103. package/tools/browser/python.worker.mjs +17 -1
  104. package/tools/browser/threadGit.mjs +12 -2
  105. package/tools/environment.d.mts +8 -0
  106. package/tools/environment.mjs +10 -0
  107. package/tools/index.d.mts +6 -3
  108. package/types.d.mts +104 -4
  109. package/worker/Claude.d.mts +4 -0
  110. package/worker/Claude.mjs +11 -0
  111. package/worker/ClaudeSubscription.d.mts +24 -0
  112. package/worker/ClaudeSubscription.mjs +9 -0
  113. package/worker/index.d.mts +9 -0
  114. package/worker/index.mjs +4 -0
package/browser/host.mjs CHANGED
@@ -1,6 +1,7 @@
1
+ import { createCodeEffectIdentity } from "../runtime/code-effect-identity.mjs";
1
2
  import { createBeforeCompaction } from "../runtime/before-compaction.mjs";
2
3
  import { createResponsesHttp, responsesHttpHeaders } from "../runtime/responses-http.mjs";
3
- import { createCodeRuntime, toolResult } from "../runtime/code-runtime.mjs";
4
+ import { createCodeRuntime, toolResult, traceToolInvocation } from "../runtime/code-runtime.mjs";
4
5
  import {
5
6
  toolRouterBrand,
6
7
  toolRouterRuntime,
@@ -9,6 +10,7 @@ import {
9
10
  import { utf8ByteLength } from "../runtime/utf8.mjs";
10
11
  import { createWorkerEvaluator } from "../runtime/worker-evaluator.mjs";
11
12
  import { openHostManagedWebSocket } from "./hostManagedWebSocket.mjs";
13
+ import { createSocketObservations } from "./socket-observations.mjs";
12
14
 
13
15
  const DEFAULT_MAX_QUEUED_MESSAGES = 4_096;
14
16
  const DEFAULT_MAX_QUEUED_BYTES = 32 * 1024 * 1024;
@@ -19,14 +21,24 @@ const MPP_CLIENT_PROTOCOL_ERROR_CLOSE_CODE = 3008;
19
21
  const WEBSOCKET_OPEN = 1;
20
22
 
21
23
  export function createBrowserHost(options = {}) {
24
+ const internalRuntime = options[Symbol.for("nanocodex.browser.internalRuntime")];
25
+ if (internalRuntime !== undefined
26
+ && (!internalRuntime || typeof internalRuntime !== "object" || Array.isArray(internalRuntime))) {
27
+ throw new TypeError("browser host internal runtime options must be an object");
28
+ }
29
+ const traceTool = internalRuntime?.traceTool;
30
+ if (traceTool !== undefined && typeof traceTool !== "function") {
31
+ throw new TypeError("browser host tool tracing hook must be a function");
32
+ }
22
33
  const onSocketTiming = options.onSocketTiming;
23
34
  if (onSocketTiming !== undefined && typeof onSocketTiming !== "function") {
24
35
  throw new TypeError("host socket timing hook must be a function");
25
36
  }
37
+ const socketObservations = createSocketObservations(options.onSocketEvent);
26
38
  const preservation = createBeforeCompaction(options.beforeCompaction);
27
39
  const toolMode = options.toolMode ?? "code";
28
- if (toolMode !== "code" && toolMode !== "direct") {
29
- throw new TypeError("toolMode must be code or direct");
40
+ if (toolMode !== "code" && toolMode !== "code-only" && toolMode !== "direct") {
41
+ throw new TypeError("toolMode must be code, code-only or direct");
30
42
  }
31
43
  const toolsRouter = options.tools?.[toolRouterBrand]
32
44
  ? options.tools[toolRouterRuntime]
@@ -38,7 +50,7 @@ export function createBrowserHost(options = {}) {
38
50
  if (toolsMcp && options.mcp) {
39
51
  throw new TypeError("MCP is already configured in Tools");
40
52
  }
41
- if ((toolsMcp || options.mcp) && toolMode !== "code") {
53
+ if ((toolsMcp || options.mcp) && toolMode === "direct") {
42
54
  throw new TypeError("remote MCP requires Code Mode");
43
55
  }
44
56
  const toolsLifecycle = options.tools?.[toolRuntimeLifecycle];
@@ -76,8 +88,12 @@ export function createBrowserHost(options = {}) {
76
88
  : () => Promise.reject(new Error(
77
89
  "browser Code Mode requires a child Worker or an explicit codeEvaluator",
78
90
  )));
91
+ const effectIdentity = createCodeEffectIdentity(options.codeEffectJournal);
79
92
  const code = createCodeRuntime(options.tools, {
93
+ traceTool,
80
94
  evaluate: codeEvaluator,
95
+ effectJournal: options.codeEffectJournal,
96
+ effectIdentity: options.codeEffectJournal ? effectIdentity.resolve : undefined,
81
97
  subagentSessions: options.subagentSessions,
82
98
  });
83
99
  const toolProviders = options.toolProviders ?? [];
@@ -192,12 +208,22 @@ export function createBrowserHost(options = {}) {
192
208
  if (preconnected !== undefined) await closePreconnected();
193
209
  ownership = openOwned(() => createWebSocket(endpoint, sessionId, request));
194
210
  }
195
- const opened = await ownership.promise;
196
- const { socket, ...handshake } = normalizeWebSocketConnection(opened);
211
+ let connection;
212
+ const observation = socketObservations?.connect(sessionId, () => ({
213
+ queued_message_count: connection?.queue.length ?? 0,
214
+ socket_delivered_message_count: connection?.timing?.metrics.delivered_message_count ?? 0,
215
+ socket_queue_residence_max_ms: connection?.timing?.metrics.queue_residence_max_ms ?? 0,
216
+ buffered_send_bytes: connection?.socket.bufferedAmount ?? 0,
217
+ }));
218
+ let normalized;
219
+ try { normalized = normalizeWebSocketConnection(await ownership.promise); }
220
+ catch (error) { observation?.error(); observation?.close(undefined, false); throw error; }
221
+ const { socket, ...handshake } = normalized;
197
222
  return new Promise((resolve, reject) => {
198
223
  let settled = false;
199
- const connection = {
224
+ connection = {
200
225
  socket,
226
+ observation,
201
227
  timing: socketTiming(),
202
228
  queue: [],
203
229
  queuedBytes: 0,
@@ -209,6 +235,7 @@ export function createBrowserHost(options = {}) {
209
235
  if (settled) return;
210
236
  settled = true;
211
237
  connectingConnections.delete(connection);
238
+ observation?.close(undefined, false);
212
239
  finishSocketTiming(connection);
213
240
  reject(error);
214
241
  };
@@ -228,6 +255,7 @@ export function createBrowserHost(options = {}) {
228
255
  delete connection.reject;
229
256
  const handle = nextHandle++;
230
257
  connections.set(handle, connection);
258
+ observation?.connected(handshake);
231
259
  resolve(JSON.stringify({
232
260
  handle,
233
261
  status: handshake.status ?? 101,
@@ -244,6 +272,7 @@ export function createBrowserHost(options = {}) {
244
272
  : { kind: "binary" });
245
273
  });
246
274
  socket.addEventListener("close", (event) => {
275
+ observation?.close(event, connection.intentionallyClosed);
247
276
  connection.wakeSend?.();
248
277
  if (!settled) {
249
278
  rejectConnection(new Error(`WebSocket closed during connection with code ${event.code}`));
@@ -252,6 +281,7 @@ export function createBrowserHost(options = {}) {
252
281
  }
253
282
  });
254
283
  socket.addEventListener("error", () => {
284
+ observation?.error();
255
285
  if (!settled) {
256
286
  rejectConnection(new Error("WebSocket connection failed"));
257
287
  } else {
@@ -329,6 +359,8 @@ export function createBrowserHost(options = {}) {
329
359
  error: "concurrent WebSocket sends are unsupported" });
330
360
  }
331
361
  connection.sending = true;
362
+ let sent = false;
363
+ connection.observation?.sendStarted();
332
364
  try {
333
365
  if (connection.managed) {
334
366
  connection.socket.send(JSON.stringify({ mpp: "message", data: message }));
@@ -363,6 +395,8 @@ export function createBrowserHost(options = {}) {
363
395
  }
364
396
  if (closed()) return closedResult();
365
397
  connection.socket.send(message);
398
+ sent = true;
399
+ connection.observation?.sent();
366
400
  return JSON.stringify({ ok: true });
367
401
  } catch (error) {
368
402
  return JSON.stringify({
@@ -371,6 +405,8 @@ export function createBrowserHost(options = {}) {
371
405
  error: error instanceof Error ? error.message : String(error),
372
406
  });
373
407
  } finally {
408
+ // A send that did not reach socket.send must not leave a waiting timer.
409
+ if (!sent) connection.observation?.sendFailed();
374
410
  connection.sending = false;
375
411
  }
376
412
  }
@@ -406,6 +442,7 @@ export function createBrowserHost(options = {}) {
406
442
  if (!connection) return;
407
443
  connections.delete(handle);
408
444
  connection.intentionallyClosed = true;
445
+ connection.observation?.close(undefined, true);
409
446
  connection.wakeSend?.();
410
447
  connection.waiter?.({ kind: "closed", detail: "by the WASM runtime" });
411
448
  finishSocketTiming(connection);
@@ -422,11 +459,13 @@ export function createBrowserHost(options = {}) {
422
459
  if (timing && dataMessage) {
423
460
  timing.metrics.message_count += 1;
424
461
  // Only the exact metadata event can produce diagnostics; deltas stay opaque.
425
- if (message.kind === "text" && timing.provider.size < 32) {
462
+ connection.observation?.message(message.kind === "text" ? message.text : undefined);
463
+ if (message.kind === "text" && (timing.provider.size < 32 || connection.observation)) {
426
464
  const metadata = providerSocketTiming(message.text);
427
465
  if (metadata) {
466
+ connection.observation?.provider(metadata);
428
467
  const key = metadata.response_id ?? timing.provider.size;
429
- if (!timing.provider.has(key)) timing.provider.set(key, metadata);
468
+ if (timing.provider.size < 32 && !timing.provider.has(key)) timing.provider.set(key, metadata);
430
469
  }
431
470
  }
432
471
  }
@@ -461,7 +500,7 @@ export function createBrowserHost(options = {}) {
461
500
 
462
501
  // No counters, timestamps, metadata parsing or observations without the internal hook.
463
502
  function socketTiming() {
464
- return onSocketTiming === undefined ? undefined : {
503
+ return onSocketTiming === undefined && socketObservations === undefined ? undefined : {
465
504
  metrics: {
466
505
  message_count: 0,
467
506
  delivered_message_count: 0,
@@ -479,6 +518,7 @@ export function createBrowserHost(options = {}) {
479
518
  // Finalize owned consumption, not the remote close event: buffered frames may
480
519
  // still be drained after that event. Clear first for reentrant close/dispose.
481
520
  connection.timing = undefined;
521
+ if (onSocketTiming === undefined) return;
482
522
  const metrics = timing.metrics;
483
523
  try {
484
524
  const result = onSocketTiming({
@@ -513,7 +553,7 @@ export function createBrowserHost(options = {}) {
513
553
  }
514
554
  for (const handle of [...connections.keys()]) cleanup(() => close(handle));
515
555
  cleanup(() => closePreconnected(disposalError));
516
- cleanup(() => code.reset());
556
+ cleanup(() => { effectIdentity.reset(); return code.reset(); });
517
557
  cleanup(() => mcpInstalled?.then(() => {}));
518
558
  cleanup(() => toolsLifecycle?.close());
519
559
  cleanup(() => options.onDispose?.());
@@ -612,10 +652,19 @@ export function createBrowserHost(options = {}) {
612
652
  next,
613
653
  close,
614
654
  sleep: (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)),
615
- executeCode: code.executeCodeObserved,
616
- waitCode: code.waitCodeObserved,
655
+ executeCode: traceTool === undefined ? code.executeCodeObserved
656
+ : (source, sessionId = "default", callId = "exec", model = "unknown", turnId, localDefinitions, executeLocalTool) =>
657
+ traceToolInvocation(traceTool, "exec", {
658
+ sessionId, callId, ...(turnId == null ? {} : { turnId }),
659
+ }, () => code.executeCodeObserved(source, sessionId, callId, model, turnId, localDefinitions, executeLocalTool)),
660
+ waitCode: traceTool === undefined ? code.waitCodeObserved
661
+ : (input, sessionId = "default", callId = "wait") =>
662
+ traceToolInvocation(traceTool, "wait", { sessionId, callId },
663
+ () => code.waitCodeObserved(input, sessionId, callId)),
617
664
  beginCodeTurn: code.beginTurn,
618
665
  cancelCodeTurn: code.cancelTurn,
666
+ preemptCode: code.preempt,
667
+ preemptCodeTurn: code.preemptTurn,
619
668
  nextCodeUpdate: code.nextCodeUpdate,
620
669
  executeTool: code.executeTool,
621
670
  routeSubagent: (request) => {
@@ -653,9 +702,9 @@ export function createBrowserHost(options = {}) {
653
702
  },
654
703
  toolMode: () => toolMode,
655
704
  toolDefinitions: code.toolDefinitions,
656
- releaseSession: code.releaseSession,
657
- emitEvent: onEvent,
658
- reset: code.reset,
705
+ releaseSession: (sessionId) => { effectIdentity.release(sessionId); socketObservations?.release(sessionId); return code.releaseSession(sessionId); },
706
+ emitEvent: (event, ...args) => { code.observeEvent(event); effectIdentity.observe(event); socketObservations?.runtime(event); return onEvent(event, ...args); },
707
+ reset: () => { effectIdentity.reset(); return code.reset(); },
659
708
  dispose,
660
709
  });
661
710
  }
@@ -1,3 +1,5 @@
1
+ import { retryAfterAdvice } from "../runtime/retry-after.mjs";
2
+
1
3
  const DEFAULT_TIMEOUT_MS = 10_000;
2
4
 
3
5
  /** Open Nanocodex's same-origin Responses proxy and consume its setup frame. */
@@ -74,13 +76,20 @@ function waitForProxyHandshake(socket, timeoutMs) {
74
76
  && message.status <= 599) {
75
77
  const status = message.status;
76
78
  const body = typeof message.error === "string" ? message.error : `HTTP ${status}`;
77
- const retryAfter = Number(message.retryAfter);
79
+ const advice = retryAfterAdvice(
80
+ typeof message.retryAfter === "number" ? String(message.retryAfter) : message.retryAfter,
81
+ );
82
+ // The proxy may already have captured advice before draining its rejection body.
83
+ if (Number.isSafeInteger(message.retry_after_deadline_ms) && message.retry_after_deadline_ms >= 0) {
84
+ advice.retry_after_deadline_ms = message.retry_after_deadline_ms;
85
+ }
78
86
  fail(Object.assign(
79
87
  new Error(`Agent connection rejected with HTTP ${status}: ${body}`),
80
88
  {
81
89
  status,
82
90
  body,
83
- ...(Number.isFinite(retryAfter) && retryAfter >= 0 ? { retryAfter } : {}),
91
+ ...advice,
92
+ ...(advice.retry_after !== undefined ? { retryAfter: advice.retry_after } : {}),
84
93
  },
85
94
  ));
86
95
  return;
@@ -4,16 +4,6 @@ export {
4
4
  subscriptionRevision,
5
5
  } from "../runtime/subscription-store.mjs";
6
6
  export { createQuickJsEvaluator } from "../runtime/quickjs-evaluator.mjs";
7
- export {
8
- createTempoProvider,
9
- createTempoProviderFromAccounts,
10
- DEFAULT_MERCATOR_MCP_URL,
11
- } from "../runtime/tempo-provider.mjs";
12
- export type {
13
- AccountsTempoProviderOptions,
14
- AccountsWallet,
15
- TempoProvider,
16
- } from "../runtime/tempo-provider.mjs";
17
7
  export type {
18
8
  AgentEvent,
19
9
  AgentLifecycle,
@@ -46,6 +36,7 @@ export type {
46
36
  TurnResult,
47
37
  TurnUsage,
48
38
  McpPayment,
39
+ PaidMcpPayment,
49
40
  McpServer,
50
41
  McpServers,
51
42
  MemoryChatGptSubscriptionStore,
@@ -79,3 +70,5 @@ export type {
79
70
  BrowserWebSocketConnection,
80
71
  BrowserWebSocketRequest,
81
72
  } from "./host.mjs";
73
+
74
+ export * as Claude from "./Claude.mjs";
package/browser/index.mjs CHANGED
@@ -4,11 +4,6 @@ export {
4
4
  subscriptionRevision,
5
5
  } from "../runtime/subscription-store.mjs";
6
6
  export { createQuickJsEvaluator } from "../runtime/quickjs-evaluator.mjs";
7
- export {
8
- createTempoProvider,
9
- createTempoProviderFromAccounts,
10
- DEFAULT_MERCATOR_MCP_URL,
11
- } from "../runtime/tempo-provider.mjs";
12
7
  export * as Agent from "./Agent.mjs";
13
8
  export * as ChatGptSubscription from "./ChatGptSubscription.mjs";
14
9
  export * as Subagents from "../runtime/subagents.mjs";
@@ -21,3 +16,5 @@ export {
21
16
  defaultHostManagedWebSocketUrl,
22
17
  openHostManagedWebSocket,
23
18
  } from "./hostManagedWebSocket.mjs";
19
+
20
+ export * as Claude from "./Claude.mjs";
@@ -0,0 +1,203 @@
1
+ // Passive, content-free observations. Timers report silence; they never cancel
2
+ // inference. Each socket retains only one active request and 32 response links.
3
+ export function createSocketObservations(observe) {
4
+ if (observe === undefined) return;
5
+ if (typeof observe !== "function") throw new TypeError("host socket event hook must be a function");
6
+ const turns = new Map();
7
+ const calls = new Map();
8
+ const sockets = new Set();
9
+ const emit = (value) => {
10
+ try {
11
+ const result = observe(value);
12
+ if (result?.then) void Promise.resolve(result).catch(() => {});
13
+ } catch { /* Observation cannot change transport behavior. */ }
14
+ };
15
+ const retain = (map, key, value, limit = 128) => {
16
+ if (!map.has(key) && map.size >= limit) map.delete(map.keys().next().value);
17
+ map.set(key, value);
18
+ };
19
+ return {
20
+ runtime(encoded) {
21
+ if (typeof encoded === "string" && !/"(?:input\.accepted|model\.(?:call|compaction|warmup)\.|run\.)/.test(encoded.slice(0, 512))) return;
22
+ let event;
23
+ try { event = typeof encoded === "string" ? JSON.parse(encoded) : encoded; } catch { return; }
24
+ const p = event?.payload;
25
+ if (!p) return;
26
+ if (event.type === "input.accepted" && typeof p.turn_id === "string" && typeof p.session_id === "string") {
27
+ retain(turns, p.turn_id, p.session_id);
28
+ }
29
+ const sessionId = turns.get(p.turn_id) ?? event.request_id;
30
+ if (event.type === "model.call.started" || event.type === "model.compaction.started" || event.type === "model.warmup.started") {
31
+ retain(calls, sessionId, { ...(Number.isSafeInteger(p.call_index ?? p.after_model_call_index)
32
+ ? { model_call_index: p.call_index ?? p.after_model_call_index } : {}),
33
+ phase: event.type.startsWith("model.compaction.") ? "compaction" : event.type.startsWith("model.warmup.") ? "warmup" : "generation" });
34
+ } else if (/^model\.(call|compaction|warmup)\.(completed|failed)$/.test(event.type)) {
35
+ for (const socket of sockets) {
36
+ if (socket.sessionId === sessionId) socket.finished(p.call_index ?? p.after_model_call_index, p.response_id, event.type.endsWith(".failed") ? "failed" : "completed");
37
+ }
38
+ calls.delete(sessionId);
39
+ }
40
+ },
41
+ release(sessionId) {
42
+ calls.delete(sessionId);
43
+ for (const [turn, session] of turns) if (session === sessionId) turns.delete(turn);
44
+ },
45
+ connect(sessionId, snapshot) {
46
+ const measure = () => { try { return snapshot(); } catch { return {}; } };
47
+ const socketId = crypto.randomUUID();
48
+ const began = performance.now();
49
+ const responses = new Map();
50
+ let handshake = {};
51
+ let active, last, timer, ordinal = 0, closed = false;
52
+ const record = (event, fields = {}, request = active ?? last) => emit({
53
+ event, socket_id: socketId, request_id: sessionId,
54
+ ...(uuid(handshake.egressRequestId) ? { egress_request_id: handshake.egressRequestId } : {}),
55
+ ...(identifier(handshake.requestId, "req_") || uuid(handshake.requestId) ? { provider_request_id: handshake.requestId } : {}),
56
+ ...(request ? { socket_request_index: request.index, ...request.context,
57
+ ...(request.responseId ? { response_id: request.responseId } : {}) } : {}),
58
+ ...fields,
59
+ });
60
+ const watch = (event, started) => {
61
+ clearTimeout(timer);
62
+ let after = 1_000;
63
+ const waiting = () => {
64
+ if (closed) return;
65
+ const now = performance.now();
66
+ record(event, { elapsed_ms: now - started,
67
+ ...(active ? { received_message_count: active.messages,
68
+ ...(active.firstMessage === undefined ? {} : { first_message_ms: active.firstMessage - active.started }),
69
+ ...milestones(active),
70
+ last_message_age_ms: now - (active.lastMessage ?? active.started), ...measure() } : {}) });
71
+ after = Math.min(after * 2, 5_000);
72
+ timer = setTimeout(waiting, after);
73
+ };
74
+ timer = setTimeout(waiting, after);
75
+ };
76
+ const responseLink = (request, id) => {
77
+ if (!request || !identifier(id, "resp_")) return;
78
+ request.responseId = id;
79
+ retain(responses, id, request, 32);
80
+ };
81
+ const finish = (outcome) => {
82
+ if (!active) return;
83
+ clearTimeout(timer);
84
+ record("request.finished", { outcome, elapsed_ms: performance.now() - active.started,
85
+ received_message_count: active.messages, ...milestones(active), ...measure() });
86
+ last = active;
87
+ active = undefined;
88
+ };
89
+ const socket = {
90
+ sessionId,
91
+ connected(value) {
92
+ handshake = value;
93
+ clearTimeout(timer);
94
+ record("socket.opened", { elapsed_ms: performance.now() - began });
95
+ },
96
+ sendStarted() {
97
+ finish("superseded");
98
+ active = { index: ++ordinal, context: calls.get(sessionId) ?? {}, started: performance.now(), messages: 0 };
99
+ record("request.send_started", measure());
100
+ watch("request.send_waiting", active.started);
101
+ },
102
+ sent() {
103
+ if (!active) return;
104
+ const now = performance.now();
105
+ record("request.sent", { send_wait_ms: now - active.started, ...measure() });
106
+ if (active.firstMessage === undefined) active.started = now;
107
+ active.sentAt = now;
108
+ watch("request.waiting", active.started);
109
+ },
110
+ sendFailed() { finish("send_failed"); },
111
+ message(text) {
112
+ const now = performance.now();
113
+ if (!active) return;
114
+ active.messages++;
115
+ active.lastMessage = now;
116
+ // Only parse bounded envelopes while a relevant milestone is missing.
117
+ // A large/unrecognized envelope remains unclassified, never copied to logs.
118
+ const candidates = typeof text === "string" && text.length <= 16_384
119
+ && (active.firstMessage === undefined || active.firstOutput === undefined
120
+ || DELTA_STAGES.some(([field, , types]) => active[field] === undefined
121
+ && types.some(type => text.includes(`"${type}"`))));
122
+ let frame;
123
+ if (candidates) {
124
+ try { frame = JSON.parse(text); } catch { /* Invalid frames remain the runtime's responsibility. */ }
125
+ }
126
+ const type = EVENT_TYPES.has(frame?.type) ? frame.type : "unclassified";
127
+ const classification = { provider_event_type: type };
128
+ if (type !== "unclassified") responseLink(active, frame?.response?.id);
129
+ // A new frame ends the current silence interval. Streaming calls
130
+ // produce no waiting records while frames keep arriving.
131
+ if (active.sentAt !== undefined) watch("request.waiting", active.started);
132
+ if (active.firstMessage === undefined) {
133
+ active.firstMessage = now;
134
+ record("request.first_message", { elapsed_ms: now - active.started, ...classification, ...measure() });
135
+ }
136
+ // Keep the historical output-item milestone, but identify what caused
137
+ // it. Empty item announcements are not evidence of generated text.
138
+ if (active.firstOutput === undefined && OUTPUT_EVENTS.has(type)) {
139
+ active.firstOutput = now;
140
+ record("request.first_output", { elapsed_ms: now - active.started, ...classification,
141
+ output_kind: OUTPUT_KINDS.get(type) ?? "item", ...measure() });
142
+ }
143
+ for (const [field, stage, types] of DELTA_STAGES) {
144
+ if (active[field] === undefined && types.includes(type) && typeof frame.delta === "string" && frame.delta.length > 0) {
145
+ active[field] = now;
146
+ record(stage, { elapsed_ms: now - active.started, ...classification, ...measure() });
147
+ }
148
+ }
149
+ },
150
+ provider(timing) {
151
+ const request = timing.response_id ? responses.get(timing.response_id) : active ?? last;
152
+ if (request?.timingReported) return;
153
+ if (request) request.timingReported = true;
154
+ record("provider.timing", timing, request ?? null);
155
+ },
156
+ finished(index, responseId, outcome) {
157
+ if (active?.context.model_call_index !== index) return;
158
+ responseLink(active, responseId);
159
+ finish(outcome);
160
+ },
161
+ close(event, intentional = false) {
162
+ if (closed) return;
163
+ closed = true;
164
+ clearTimeout(timer);
165
+ record("socket.closed", { elapsed_ms: performance.now() - began, intentional,
166
+ ...(Number.isInteger(event?.code) ? { close_code: event.code } : {}),
167
+ ...(typeof event?.wasClean === "boolean" ? { close_clean: event.wasClean } : {}),
168
+ ...(active ? { received_message_count: active.messages,
169
+ last_message_age_ms: performance.now() - (active.lastMessage ?? active.started) } : {}), ...measure() });
170
+ active = undefined;
171
+ sockets.delete(socket);
172
+ },
173
+ error() { record("socket.error", { elapsed_ms: performance.now() - began, ...measure() }); },
174
+ };
175
+ sockets.add(socket);
176
+ record("socket.connecting", {}, undefined);
177
+ watch("socket.connect_waiting", began);
178
+ return socket;
179
+ },
180
+ };
181
+ }
182
+
183
+ function uuid(value) { return typeof value === "string" && /^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i.test(value); }
184
+ function identifier(value, prefix) { return typeof value === "string" && value.startsWith(prefix) && /^[A-Za-z0-9_-]{1,160}$/.test(value); }
185
+ const OUTPUT_EVENTS = new Set(["response.output_text.delta", "response.reasoning_summary_text.delta",
186
+ "response.reasoning_summary.delta", "response.reasoning_content.delta", "response.output_item.added", "response.output_item.done"]);
187
+
188
+ // These names are a fixed protocol allowlist, never provider-supplied log text.
189
+ const DELTA_STAGES = [
190
+ ["firstReasoning", "request.first_reasoning_delta", ["response.reasoning_text.delta", "response.reasoning_summary_text.delta",
191
+ "response.reasoning_summary.delta", "response.reasoning_content.delta"]],
192
+ ["firstAnswer", "request.first_answer_delta", ["response.output_text.delta"]],
193
+ ["firstTool", "request.first_tool_delta", ["response.function_call_arguments.delta", "response.custom_tool_call_input.delta"]],
194
+ ];
195
+ const OUTPUT_KINDS = new Map(DELTA_STAGES.flatMap(([, stage, types]) => types.map(type => [type,
196
+ stage === "request.first_reasoning_delta" ? "reasoning" : stage === "request.first_answer_delta" ? "answer" : "tool"])));
197
+ const EVENT_TYPES = new Set([...OUTPUT_EVENTS, ...OUTPUT_KINDS.keys(), "response.created", "response.in_progress", "response.queued",
198
+ "response.completed", "response.failed", "response.incomplete", "error", "responsesapi.websocket_timing"]);
199
+ function milestones(request) {
200
+ return Object.fromEntries([["firstOutput", "first_output_ms"], ["firstReasoning", "first_reasoning_delta_ms"],
201
+ ["firstAnswer", "first_answer_delta_ms"], ["firstTool", "first_tool_delta_ms"]]
202
+ .filter(([field]) => request[field] !== undefined).map(([field, key]) => [key, request[field] - request.started]));
203
+ }
@@ -1,4 +1,4 @@
1
- import type { logout } from "./actions/account.mjs";
1
+ import type { links, logout } from "./actions/account.mjs";
2
2
  import type { create as createAgent } from "./actions/agent.mjs";
3
3
  import type { request as connectorRequest } from "./actions/connector.mjs";
4
4
  import type { connect, disconnect, reconnect } from "./actions/connection.mjs";
@@ -10,7 +10,7 @@ import type { Client } from "./Client.mjs";
10
10
  import type { Connection, HostConnection } from "./types.mjs";
11
11
 
12
12
  export type ConnectActions<connection extends Connection | HostConnection = Connection> = {
13
- account: { logout(): logout.ReturnType };
13
+ account: { links(options?: links.Options): links.ReturnType; logout(): logout.ReturnType };
14
14
  agent: { create(options: createAgent.Options): Promise<createAgent.ReturnType> };
15
15
  connector: { request(options: connectorRequest.Options): connectorRequest.ReturnType };
16
16
  connectors: {
@@ -11,6 +11,7 @@ import * as mpp from "./actions/mpp.mjs";
11
11
  export function connectActions() {
12
12
  return (client) => ({
13
13
  account: {
14
+ links: (options) => account.links(client, options),
14
15
  logout: () => account.logout(client),
15
16
  },
16
17
  agent: {
@@ -1,5 +1,16 @@
1
1
  export const DEFAULT_HOST: "https://nanocodex.gakonst.workers.dev/connect-dialog/";
2
2
 
3
+ /** Visual tokens for the hosted dialog. Arbitrary CSS is not supported. */
4
+ export type Appearance = Readonly<{
5
+ theme?: "light" | "dark" | "system" | undefined;
6
+ /** Six-digit hex color, for example #635bff. */
7
+ accentColor?: `#${string}` | undefined;
8
+ /** Installed or self-hosted font list, at most 160 characters. No CSS functions. */
9
+ fontFamily?: string | undefined;
10
+ /** Control corner radius in pixels, from 0 to 24. */
11
+ borderRadius?: number | undefined;
12
+ }>;
13
+
3
14
  export type Request = ConnectionRequest | FundingRequest;
4
15
 
5
16
  export type ConnectionRequest = Readonly<{
@@ -93,12 +104,14 @@ export function from<const type extends string>(parameters: Readonly<{
93
104
  }>): Dialog<type>;
94
105
 
95
106
  export function iframe(options?: Readonly<{
107
+ appearance?: Appearance | undefined;
96
108
  host?: string | undefined;
97
109
  key?: string | undefined;
98
110
  name?: string | undefined;
99
111
  }>): Dialog<"iframe">;
100
112
 
101
113
  export function popup(options?: Readonly<{
114
+ appearance?: Appearance | undefined;
102
115
  host?: string | undefined;
103
116
  key?: string | undefined;
104
117
  name?: string | undefined;