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
@@ -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
+ };
@@ -0,0 +1,28 @@
1
+ import type { BrowserHttpRequest } from "../browser/host.mjs";
2
+
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
+
5
+ /** Structural Workers AI binding; provider credentials stay with Cloudflare. */
6
+ export type WorkersAiBinding<Model extends ResponsesCanonicalModel = "@cf/zai-org/glm-5.3"> = {
7
+ run(model: Model, input: Record<string, unknown>): Promise<unknown>;
8
+ };
9
+ export type WorkersAiResponsesOptions<Model extends ResponsesCanonicalModel = "@cf/zai-org/glm-5.3"> = Readonly<{
10
+ /** Canonical response identity; defaults to GLM-5.3. */
11
+ model?: Model;
12
+ /** Local transport identity, never fetched. Defaults to https://workers-ai.invalid/v1. */
13
+ apiBaseUrl?: string;
14
+ }>;
15
+ export type WorkersAiResponsesTransport = Readonly<{
16
+ apiBaseUrl: string;
17
+ createResponse(endpoint: string, sessionId: string, request: BrowserHttpRequest): Promise<Response>;
18
+ }>;
19
+ /**
20
+ * Incremental (stream:true) or buffered, stateless Responses SSE over the GLM-5.3 Workers AI binding.
21
+ * Requires full text history; opaque compaction and unsupported modalities fail explicitly.
22
+ * x-nanocodex-inference-buffering reports streaming or buffered (including binding fallback).
23
+ * Text/reasoning stream incrementally; tool events follow terminal tool validation.
24
+ * Structured output formats and malformed or truncated tool calls fail explicitly.
25
+ * Custom grammars are supplied as instructions, not enforced by the provider.
26
+ * Cancellation stops waiting; the binding does not expose cancellation of inference.
27
+ */
28
+ export function createWorkersAiResponses<Model extends ResponsesCanonicalModel = "@cf/zai-org/glm-5.3">(ai: WorkersAiBinding<Model>, options?: WorkersAiResponsesOptions<Model>): WorkersAiResponsesTransport;
@@ -0,0 +1,399 @@
1
+ import { toolAliasFailure } from "./tool-alias-diagnostic.mjs";
2
+ import { chatReasoningText } from "./chat-reasoning.mjs";
3
+ import { providerStream, streamResponse } from "./provider-stream.mjs";
4
+ const MODEL = "@cf/zai-org/glm-5.3";
5
+ const MODELS = [MODEL, "gpt-6-astra", "gpt-6.1-sol", "gpt-6-luna", "kimi-k3", "mimo-v2.6-pro"];
6
+ const BASE = "https://workers-ai.invalid/v1";
7
+ const fail = (message) => { throw new Error(`Workers AI Responses: ${message}`); };
8
+ const json = (value) => typeof value === "string" ? value : JSON.stringify(value);
9
+ const key = (namespace, name) => JSON.stringify([namespace ?? null, name]);
10
+
11
+ /** Stateless Responses transport for the Workers AI binding. */
12
+ export function createWorkersAiResponses(ai, options = {}) {
13
+ if (typeof ai?.run !== "function") throw new TypeError("Workers AI binding must provide run()");
14
+ const apiBaseUrl = options.apiBaseUrl ?? BASE;
15
+ const model = options.model ?? MODEL;
16
+ if (!MODELS.includes(model)) fail("unsupported canonical model");
17
+ return Object.freeze({
18
+ apiBaseUrl,
19
+ async createResponse(endpoint, _sessionId, request) {
20
+ if (endpoint !== `${apiBaseUrl}/responses`) fail("unexpected endpoint; compaction is not supported");
21
+ if (request.authorization !== "host_managed") fail("hostManaged authorization is required");
22
+ request.signal?.throwIfAborted();
23
+ const body = JSON.parse(request.body);
24
+ const { input, registry } = translate(body, model);
25
+ const pending = Promise.resolve().then(() => ai.run(model, input)).catch(() => {
26
+ request.signal?.throwIfAborted();
27
+ // Binding failures may contain credentials or raw request/provider data.
28
+ fail("provider request failed");
29
+ }).then(result => {
30
+ // A binding may ignore cancellation while creating its stream. Release a
31
+ // late result even after abortable() has stopped waiting for inference.
32
+ if (request.signal?.aborted) {
33
+ const stream = result instanceof ReadableStream ? result : result?.providerStream ? result.body : null;
34
+ if (stream) void stream.cancel().catch(() => {});
35
+ request.signal.throwIfAborted();
36
+ }
37
+ return result;
38
+ });
39
+ const result = await abortable(pending, request.signal);
40
+ request.signal?.throwIfAborted();
41
+ if (body.stream === true) {
42
+ const source = result instanceof ReadableStream ? providerStream(result, "workers_ai_chat") : result;
43
+ if (source?.providerStream) {
44
+ return streamResponse(source, (value, prologue = false) => normalizeResponse(value, registry, model, prologue ? undefined : body.tool_choice), responseEvents,
45
+ request.signal);
46
+ }
47
+ // Some bindings return a completed object despite stream:true. Validate
48
+ // normally and label this honestly; HTTP gateways never take this path.
49
+ }
50
+ return toResponse(result, registry, model, body.tool_choice);
51
+ },
52
+ });
53
+ }
54
+
55
+ async function abortable(promise, signal) {
56
+ if (!signal) return promise;
57
+ signal.throwIfAborted();
58
+ let listener;
59
+ const cancelled = new Promise((_, reject) => {
60
+ listener = () => reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
61
+ signal.addEventListener("abort", listener, { once: true });
62
+ });
63
+ try { return await Promise.race([promise, cancelled]); }
64
+ finally { signal.removeEventListener("abort", listener); }
65
+ }
66
+
67
+ function translate(body, model) {
68
+ if (!body || typeof body !== "object" || Array.isArray(body)) fail("expected a Responses request object");
69
+ if (body.model !== undefined && body.model !== model) fail("unsupported model override; expected pinned model");
70
+ const effort = (value) => {
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
+ return value;
73
+ };
74
+ if (body.previous_response_id) fail("previous_response_id is unsupported; send the complete Responses history");
75
+ if (body.context_management?.length) fail("provider compaction is unsupported; supply portable text history");
76
+ const history = typeof body.input === "string"
77
+ ? [{ type: "message", role: "user", content: body.input }] : body.input ?? [];
78
+ if (!Array.isArray(history)) fail("expected complete Responses history");
79
+ if (body.text?.format && body.text.format.type !== "text") fail("structured output formats are unsupported");
80
+ const registry = new Map();
81
+ const byIdentity = new Map();
82
+ function register(tool, namespace, description = "") {
83
+ if (tool.type === "namespace") {
84
+ if (namespace) fail("nested tool namespaces are unsupported");
85
+ for (const child of tool.tools) register(child, tool.name, tool.description);
86
+ return;
87
+ }
88
+ if (!["function", "custom", "tool_search"].includes(tool.type)) fail(`unsupported tool type ${tool.type}`);
89
+ if (tool.type === "tool_search" && tool.execution !== "client") fail("tool search must use client execution");
90
+ const name = tool.type === "tool_search" ? "tool_search" : tool.name;
91
+ const identity = key(namespace, name);
92
+ let entry = byIdentity.get(identity);
93
+ if (!entry) {
94
+ entry = { alias: `tool_${registry.size}`, name, namespace, type: tool.type };
95
+ registry.set(entry.alias, entry);
96
+ byIdentity.set(identity, entry);
97
+ }
98
+ entry.definition = { type: "function", function: {
99
+ name: entry.alias,
100
+ description: [namespace ? `${namespace}.${name}` : name, description, tool.description,
101
+ tool.type === "custom" ? "Pass the exact free-form tool input as the JSON string field input. Preserve all code and newlines." : "",
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 } : {}),
104
+ parameters: tool.type === "custom" ? {
105
+ type: "object", properties: { input: { type: "string" } }, required: ["input"], additionalProperties: false,
106
+ } : tool.parameters ?? { type: "object", properties: {} },
107
+ } };
108
+ }
109
+ for (const tool of body.tools ?? []) register(tool);
110
+ for (const item of history) {
111
+ if (item.type === "additional_tools" || item.type === "tool_search_output") {
112
+ for (const tool of item.tools ?? []) register(tool);
113
+ }
114
+ }
115
+ const messages = [];
116
+ if (body.instructions) messages.push({ role: "system", content: body.instructions });
117
+ const vision = model !== MODEL;
118
+ const pendingImages = [];
119
+ const pending = new Set();
120
+ const seenCallIds = new Set();
121
+ let reasoningEffort = effort(body.reasoning?.effort);
122
+ for (const item of history) {
123
+ switch (item.type ?? "message") {
124
+ case "additional_tools": break;
125
+ case "configuration_update":
126
+ reasoningEffort = effort(item.reasoning?.effort) ?? reasoningEffort;
127
+ break;
128
+ case "message": {
129
+ if (pending.size) fail("tool calls require their outputs before the next message");
130
+ if (!["user", "assistant", "system", "developer"].includes(item.role)) fail(`unsupported message role ${item.role}`);
131
+ const content = vision && item.role === "user" ? visionContent(item.content) : textContent(item.content);
132
+ const previous = messages.at(-1);
133
+ if (item.role === "assistant" && previous?.role === "assistant" && previous.content === null && !previous.tool_calls) previous.content = content;
134
+ else messages.push({ role: item.role === "developer" ? "system" : item.role, content });
135
+ break;
136
+ }
137
+ case "agent_message": {
138
+ if (pending.size) fail("tool calls require their outputs before an agent message");
139
+ messages.push({ role: "user", content: JSON.stringify({ author: item.author, recipient: item.recipient,
140
+ message: textContent(item.content) }) });
141
+ break;
142
+ }
143
+ case "reasoning": {
144
+ // Encrypted provider reasoning is not portable, but is not user history.
145
+ const text = [...(item.summary ?? []), ...(item.content ?? [])].map(part => part.text ?? "").join("\n");
146
+ const details = decodeReasoning(item.encrypted_content, model);
147
+ if (text || details) {
148
+ const previous = messages.at(-1);
149
+ const fields = { ...(text ? { reasoning_content: text } : {}), ...(details ? { reasoning_details: details } : {}) };
150
+ if (previous?.role === "assistant") Object.assign(previous, fields);
151
+ else messages.push({ role: "assistant", content: null, ...fields });
152
+ }
153
+ break;
154
+ }
155
+ case "function_call":
156
+ case "custom_tool_call":
157
+ case "tool_search_call": {
158
+ if (item.encrypted_function_args?.length) fail("encrypted function arguments cannot be replayed");
159
+ const name = item.type === "tool_search_call" ? "tool_search" : item.name;
160
+ let entry = byIdentity.get(key(item.namespace, name));
161
+ if (!entry) {
162
+ // Historical calls may belong to tools no longer available this turn.
163
+ entry = { alias: `history_${byIdentity.size}` };
164
+ byIdentity.set(key(item.namespace, name), entry);
165
+ }
166
+ if (typeof item.call_id !== "string" || !item.call_id || seenCallIds.has(item.call_id)) fail("missing or duplicate tool call ID");
167
+ if (pending.size && messages.at(-1)?.role !== "assistant") fail("tool calls require all outputs before another tool call");
168
+ seenCallIds.add(item.call_id);
169
+ pending.add(item.call_id);
170
+ const call = { id: item.call_id, type: "function", function: { name: entry.alias,
171
+ arguments: item.type === "custom_tool_call" ? JSON.stringify({ input: item.input }) : json(item.arguments) } };
172
+ const previous = messages.at(-1);
173
+ if (previous?.role === "assistant") (previous.tool_calls ??= []).push(call);
174
+ else messages.push({ role: "assistant", content: null, tool_calls: [call] });
175
+ break;
176
+ }
177
+ case "function_call_output":
178
+ case "custom_tool_call_output":
179
+ case "tool_search_output": {
180
+ if (!pending.delete(item.call_id)) fail("tool output has no matching call in full history");
181
+ const content = item.type === "tool_search_output" ? JSON.stringify({ tools: item.tools })
182
+ : vision ? visionContent(item.output) : textContent(item.output);
183
+ // Chat providers accept screenshots as user image parts, not tool text.
184
+ // Keep every call/result matched before appending the image observations.
185
+ if (Array.isArray(content)) {
186
+ messages.push({ role: "tool", tool_call_id: item.call_id,
187
+ content: content.filter(part => part.type === "text").map(part => part.text).join("\n") });
188
+ const images = content.filter(part => part.type === "image_url");
189
+ if (images.length) pendingImages.push({ role: "user", content: [
190
+ { type: "text", text: `Image observations returned by tool call ${item.call_id}:` }, ...images,
191
+ ] });
192
+ } else messages.push({ role: "tool", tool_call_id: item.call_id, content });
193
+ if (!pending.size) messages.push(...pendingImages.splice(0));
194
+ break;
195
+ }
196
+ case "compaction": case "compaction_summary": case "context_compaction": case "compaction_trigger":
197
+ fail("compaction is unsupported; restore portable text history before using Workers AI");
198
+ break;
199
+ default: fail(`unsupported history item ${item.type}`);
200
+ }
201
+ }
202
+ if (pending.size) fail("full history contains tool calls without outputs");
203
+ const input = { messages, stream: body.stream === true };
204
+ if (registry.size) input.tools = [...registry.values()].map(entry => entry.definition);
205
+ for (const field of ["temperature", "top_p", "parallel_tool_calls"]) {
206
+ if (body[field] !== undefined) input[field] = body[field];
207
+ }
208
+ if (body.max_output_tokens !== undefined) input.max_completion_tokens = body.max_output_tokens;
209
+ if (reasoningEffort !== undefined) input.reasoning_effort = reasoningEffort;
210
+ if (body.tool_choice !== undefined) {
211
+ if (typeof body.tool_choice === "string") {
212
+ if (!["auto", "none", "required"].includes(body.tool_choice)) fail("unsupported tool_choice");
213
+ input.tool_choice = body.tool_choice;
214
+ }
215
+ else {
216
+ const choice = byIdentity.get(key(body.tool_choice.namespace, body.tool_choice.name ?? (body.tool_choice.type === "tool_search" ? "tool_search" : undefined)));
217
+ if (!choice?.definition) fail("tool_choice refers to an unavailable tool");
218
+ input.tool_choice = { type: "function", function: { name: choice.alias } };
219
+ }
220
+ }
221
+ return { input, registry };
222
+ }
223
+
224
+ function textContent(content) {
225
+ if (typeof content === "string") return content;
226
+ if (!Array.isArray(content)) fail("expected text content");
227
+ return content.map(part => {
228
+ if (!["input_text", "output_text", "text"].includes(part.type) || typeof part.text !== "string") {
229
+ fail(`unsupported content ${part.type}; GLM history must contain text`);
230
+ }
231
+ return part.text;
232
+ }).join("\n");
233
+ }
234
+
235
+ // Opaque transport envelope, not encryption. Keep provider reasoning metadata
236
+ // byte-for-byte at the JSON value level across Responses history/tool replay.
237
+ const REASONING_PREFIX = "nanocodex-chat-reasoning-v1:";
238
+ function encodeReasoning(details, model) {
239
+ const value = JSON.stringify({ model, details });
240
+ if (value.length > 4 * 1024 * 1024) fail("reasoning details exceed limit");
241
+ return REASONING_PREFIX + value;
242
+ }
243
+ function decodeReasoning(value, model) {
244
+ if (typeof value !== "string" || !value.startsWith(REASONING_PREFIX)) return undefined;
245
+ if (value.length > 4 * 1024 * 1024 + REASONING_PREFIX.length) fail("reasoning details exceed limit");
246
+ let parsed;
247
+ try { parsed = JSON.parse(value.slice(REASONING_PREFIX.length)); } catch { fail("invalid reasoning replay"); }
248
+ if (parsed.model !== model || !Array.isArray(parsed.details)
249
+ || parsed.details.some(d => !d || typeof d !== "object" || Array.isArray(d))) fail("invalid reasoning replay");
250
+ return parsed.details;
251
+ }
252
+ function visionContent(content) {
253
+ if (typeof content === "string") return content;
254
+ if (!Array.isArray(content)) fail("expected text or image content");
255
+ return content.map(part => {
256
+ if (["input_text", "output_text", "text"].includes(part.type) && typeof part.text === "string") return { type: "text", text: part.text };
257
+ if (part.type !== "input_image" || typeof part.image_url !== "string"
258
+ || !(/^(https:\/\/|data:image\/(png|jpeg|jpg|webp|gif);base64,)/.test(part.image_url))
259
+ || (part.detail !== undefined && !["auto", "low", "high", "original"].includes(part.detail))) fail("unsupported image content");
260
+ return { type: "image_url", image_url: { url: part.image_url,
261
+ ...(part.detail === undefined ? {} : { detail: part.detail === "original" ? "high" : part.detail }) } };
262
+ });
263
+ }
264
+
265
+ function normalizeResponse(result, registry, model, toolChoice) {
266
+ const choice = result?.choices?.[0];
267
+ if (result?.error || !choice?.message || typeof choice.message !== "object" || Array.isArray(choice.message)
268
+ || !["stop", "tool_calls", "length", "content_filter"].includes(choice.finish_reason)) {
269
+ fail("invalid chat completion or unsupported finish reason");
270
+ }
271
+ const message = choice.message;
272
+ if (message.content != null && typeof message.content !== "string") fail("unsupported completion content");
273
+ if (message.tool_calls != null && !Array.isArray(message.tool_calls)) fail("invalid completion tool calls");
274
+ if (message.refusal) fail("provider refused completion");
275
+ if (["length", "content_filter"].includes(choice.finish_reason) && message.tool_calls?.length) {
276
+ fail("incomplete completion cannot dispatch tool calls");
277
+ }
278
+ const output = [];
279
+ const id = `resp_${crypto.randomUUID()}`;
280
+ const reasoning = chatReasoningText(message);
281
+ const details = message.reasoning_details;
282
+ if (details !== undefined && (!Array.isArray(details) || details.some(d => !d || typeof d !== "object" || Array.isArray(d)))) fail("invalid reasoning details");
283
+ if ((typeof reasoning === "string" && reasoning) || details?.length) {
284
+ output.push({ type: "reasoning", id: `rs_${crypto.randomUUID()}`, status: "completed",
285
+ summary: [], content: reasoning ? [{ type: "reasoning_text", text: reasoning }] : [],
286
+ ...(details?.length ? { encrypted_content: encodeReasoning(details, model) } : {}) });
287
+ }
288
+ if (typeof message.content === "string" && message.content) {
289
+ output.push({ type: "message", id: `msg_${crypto.randomUUID()}`, role: "assistant", status: "completed",
290
+ content: [{ type: "output_text", text: message.content, annotations: [] }] });
291
+ }
292
+ // Validate the caller's tool-choice contract even when a gateway needs to
293
+ // emulate forcing with a restricted tool set. Never dispatch a violating call.
294
+ if (toolChoice === "none" && message.tool_calls?.length) fail("tool choice forbids calls");
295
+ if (choice.finish_reason === "stop" && (toolChoice === "required" || typeof toolChoice === "object")
296
+ && !message.tool_calls?.length) fail("required tool call missing");
297
+ const callIds = new Set();
298
+ for (const call of message.tool_calls ?? []) {
299
+ if (!call || (call.type !== undefined && call.type !== "function")) fail("unsupported completion tool call");
300
+ const returnedName = call.function?.name;
301
+ let entry = registry.get(returnedName);
302
+ if (!entry && typeof returnedName === "string") {
303
+ // GLM sometimes returns the original name shown in the description instead
304
+ // of the advertised alias. Resolve only an exact, unambiguous registered
305
+ // identity; never guess among namespaces or dispatch an undeclared tool.
306
+ const matches = [...registry.values()].filter(candidate => returnedName === candidate.name
307
+ || returnedName === (candidate.namespace ? `${candidate.namespace}.${candidate.name}` : candidate.name));
308
+ if (matches.length === 1) entry = matches[0];
309
+ }
310
+ if (!entry) {
311
+ const diagnostic = toolAliasFailure(returnedName, registry);
312
+ fail(diagnostic === "unknown tool alias" ? "model returned an unknown tool alias" : `model returned ${diagnostic}`);
313
+ }
314
+ if (toolChoice && typeof toolChoice === "object" && (entry.name !== (toolChoice.name ?? "tool_search")
315
+ || entry.namespace !== toolChoice.namespace)) fail("model returned a different forced tool");
316
+ const argumentsText = json(call.function.arguments);
317
+ let args;
318
+ try { args = JSON.parse(argumentsText); } catch {
319
+ const trimmed = typeof argumentsText === "string" ? argumentsText.trimStart() : "";
320
+ if (entry.type === "custom" && trimmed && !["{", "[", '\"'].includes(trimmed[0])) {
321
+ // Custom tools natively accept freeform strings. Some Chat providers
322
+ // return that input without the advertised JSON wrapper. Preserve it
323
+ // exactly through the existing custom-call path; malformed JSON-looking
324
+ // wrappers and every non-custom tool still fail closed.
325
+ args = { input: argumentsText };
326
+ } else fail("model returned invalid tool JSON");
327
+ }
328
+ if (call.id !== undefined && (typeof call.id !== "string" || !call.id)) fail("invalid tool call ID");
329
+ if (!args || typeof args !== "object" || Array.isArray(args)) fail("tool arguments must be a JSON object");
330
+ const call_id = call.id || `call_${crypto.randomUUID()}`;
331
+ if (callIds.has(call_id)) fail("model returned duplicate tool call IDs");
332
+ callIds.add(call_id);
333
+ const common = { id: `tool_${crypto.randomUUID()}`, call_id, status: "completed" };
334
+ if (entry.type === "tool_search") {
335
+ output.push({ ...common, type: "tool_search_call", execution: "client", arguments: args });
336
+ } else {
337
+ const named = { ...common, name: entry.name, ...(entry.namespace ? { namespace: entry.namespace } : {}) };
338
+ if (entry.type === "custom") {
339
+ if (typeof args?.input !== "string") fail("custom tool arguments must contain a string input");
340
+ output.push({ ...named, type: "custom_tool_call", input: args.input });
341
+ } else output.push({ ...named, type: "function_call", arguments: argumentsText });
342
+ }
343
+ }
344
+ if (choice.finish_reason === "tool_calls" && !callIds.size) fail("tool_calls finish reason omitted tool calls");
345
+ const incomplete = ["length", "content_filter"].includes(choice.finish_reason);
346
+ const usage = result.usage ? {
347
+ input_tokens: result.usage.prompt_tokens ?? 0, output_tokens: result.usage.completion_tokens ?? 0,
348
+ total_tokens: result.usage.total_tokens ?? (result.usage.prompt_tokens ?? 0) + (result.usage.completion_tokens ?? 0),
349
+ input_tokens_details: { cached_tokens: result.usage.prompt_tokens_details?.cached_tokens ?? 0 },
350
+ output_tokens_details: { reasoning_tokens: result.usage.completion_tokens_details?.reasoning_tokens ?? 0 },
351
+ } : null;
352
+ const response = { id, object: "response", model, status: incomplete ? "incomplete" : "completed", output,
353
+ usage, end_turn: !incomplete && !callIds.size,
354
+ ...(incomplete ? { incomplete_details: { reason: choice.finish_reason === "length" ? "max_output_tokens" : "content_filter" } } : {}) };
355
+ return response;
356
+ }
357
+
358
+ function responseEvents(response) {
359
+ const { output } = response;
360
+ const incomplete = response.status === "incomplete";
361
+ const events = [];
362
+ const emit = (type, fields) => events.push({ type, sequence_number: events.length, ...fields });
363
+ emit("response.created", { response: { ...response, status: "in_progress", output: [], usage: null } });
364
+ for (const [output_index, item] of output.entries()) {
365
+ const fields = { output_index, item_id: item.id };
366
+ const initial = { ...item, status: "in_progress" };
367
+ if (item.type === "message" || item.type === "reasoning") initial.content = [];
368
+ if (item.type === "function_call") initial.arguments = "";
369
+ if (item.type === "custom_tool_call") initial.input = "";
370
+ emit("response.output_item.added", { output_index, item: initial });
371
+ if (item.type === "message") {
372
+ emit("response.content_part.added", { ...fields, content_index: 0, part: { type: "output_text", text: "", annotations: [] } });
373
+ emit("response.output_text.delta", { ...fields, content_index: 0, delta: item.content[0].text });
374
+ emit("response.output_text.done", { ...fields, content_index: 0, text: item.content[0].text });
375
+ emit("response.content_part.done", { ...fields, content_index: 0, part: item.content[0] });
376
+ } else if (item.type === "function_call" || item.type === "custom_tool_call") {
377
+ const custom = item.type === "custom_tool_call";
378
+ const stem = custom ? "custom_tool_call_input" : "function_call_arguments";
379
+ const value = custom ? item.input : item.arguments;
380
+ emit(`response.${stem}.delta`, { ...fields, call_id: item.call_id, delta: value });
381
+ emit(`response.${stem}.done`, { ...fields, call_id: item.call_id, [custom ? "input" : "arguments"]: value });
382
+ } else if (item.type === "reasoning" && item.content.length) {
383
+ emit("response.content_part.added", { ...fields, content_index: 0, part: { type: "reasoning_text", text: "" } });
384
+ emit("response.reasoning_text.delta", { ...fields, content_index: 0, delta: item.content[0].text });
385
+ emit("response.reasoning_text.done", { ...fields, content_index: 0, text: item.content[0].text });
386
+ emit("response.content_part.done", { ...fields, content_index: 0, part: item.content[0] });
387
+ }
388
+ emit("response.output_item.done", { output_index, item });
389
+ }
390
+ emit(incomplete ? "response.incomplete" : "response.completed", { response });
391
+ return events;
392
+ }
393
+
394
+ function toResponse(result, registry, model, toolChoice) {
395
+ const events = responseEvents(normalizeResponse(result, registry, model, toolChoice));
396
+ return new Response(events.map(event => `event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`).join(""), {
397
+ headers: { "content-type": "text/event-stream", "cache-control": "no-cache", "x-nanocodex-inference-buffering": "buffered" },
398
+ });
399
+ }
package/host/Agent.d.mts CHANGED
@@ -47,7 +47,7 @@ export declare namespace create {
47
47
  | {
48
48
  durability: DurabilityStore;
49
49
  durabilityId: string;
50
- /** The root remains durable; clean subagent children are in-memory. */
50
+ /** The root remains durable; all subagent children are ephemeral. */
51
51
  tools?: ToolConfiguration<SubagentTool> | undefined;
52
52
  }
53
53
  );
package/index.d.mts CHANGED
@@ -24,6 +24,8 @@ export type {
24
24
  AgentEvent,
25
25
  AgentLifecycle,
26
26
  AgentOptions,
27
+ BeforeCompactionRequest,
28
+ CompactionReceipt,
27
29
  AgentSessionContext,
28
30
  ChatGptCredential,
29
31
  ChatGptCredentialSeed,
package/internal.mjs CHANGED
@@ -30,6 +30,7 @@ export function defineRuntime(definition) {
30
30
  type: definition.type ?? "custom",
31
31
  create: definition.create,
32
32
  dispose: definition.dispose || ((agent) => agent.free()),
33
+ shutdown: definition.shutdown || ((agent) => agent.shutdown()),
33
34
  subscribe: definition.subscribe,
34
35
  adopt: definition.adopt,
35
36
  release: definition.release,
@@ -228,6 +229,11 @@ export function compact(agent) {
228
229
  return agentState(agent).raw.compact();
229
230
  }
230
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
+
231
237
  export async function context(agent) {
232
238
  return parseSessionContext(await agentState(agent).raw.context());
233
239
  }
@@ -315,7 +321,10 @@ export function toWasmConfig(options = {}) {
315
321
  copy(config, "thinking", options.thinking);
316
322
  copy(config, "reasoning_mode", options.reasoningMode);
317
323
  copy(config, "fast_mode", options.fastMode);
324
+ copy(config, "stateless_http", options.stateless);
325
+ copy(config, "subagent_routing", options.subagentRouting);
318
326
  copy(config, "websocket_warmup", options.websocketWarmup);
327
+ copy(config, "raw_api_events", options.rawApiEvents);
319
328
  copy(config, "websocket_url", options.websocketUrl);
320
329
  copy(config, "api_base_url", options.apiBaseUrl);
321
330
  copy(config, "instructions", options.instructions);
@@ -343,6 +352,7 @@ export function toWasmConfig(options = {}) {
343
352
  copy(config, "terminal_receipt_retention", options.terminalReceiptRetention);
344
353
  copy(config, "subagents", options.subagents);
345
354
  copy(config, "host_definition_id", options.hostDefinitionId);
355
+ copy(config, "before_compaction", options.beforeCompaction);
346
356
  return config;
347
357
  }
348
358
 
@@ -427,6 +437,13 @@ export function releaseHostSession(host, sessionId) {
427
437
  hostSessions.delete(sessionId);
428
438
  }
429
439
 
440
+ /** Drops only this host's in-memory registrations. */
441
+ export function releaseHostSessions(host) {
442
+ for (const [sessionId, owner] of hostSessions) {
443
+ if (owner === host) releaseHostSession(host, sessionId);
444
+ }
445
+ }
446
+
430
447
  export function registerDefinitionHost(host, cloudflareReservation) {
431
448
  const id = nextDefinitionHost++;
432
449
  definitionHosts.set(id, host);
@@ -515,6 +532,27 @@ const hostBridge = Object.freeze({
515
532
  }
516
533
  return host.sleep(milliseconds);
517
534
  },
535
+ async beforeCompaction(hostDefinitionId, requestJson) {
536
+ const host = requiredDefinitionHost(hostDefinitionId);
537
+ return JSON.stringify(await host.beforeCompaction(JSON.parse(requestJson)));
538
+ },
539
+ cancelBeforeCompaction(hostDefinitionId, boundaryId) {
540
+ definitionHosts.get(hostDefinitionId)?.cancelBeforeCompaction(boundaryId);
541
+ },
542
+ async routeSubagent(hostDefinitionId, requestJson) {
543
+ const host = requiredDefinitionHost(hostDefinitionId);
544
+ if (!cloudflareHostMayBindSubagent(host)) throw new Error("subagent host is no longer active");
545
+ return JSON.stringify(await host.routeSubagent(JSON.parse(requestJson)));
546
+ },
547
+ bindSubagentRoute(hostDefinitionId, requestJson) {
548
+ const host = requiredDefinitionHost(hostDefinitionId);
549
+ if (!cloudflareHostMayBindSubagent(host)) throw new Error("subagent host is no longer active");
550
+ const result = host.bindSubagentRoute(JSON.parse(requestJson));
551
+ if (result && typeof result.then === "function") {
552
+ Promise.resolve(result).catch(() => {});
553
+ throw new TypeError("subagent route binding must be synchronous");
554
+ }
555
+ },
518
556
  bindSubagentSession(
519
557
  hostDefinitionId,
520
558
  rootSessionId,
@@ -534,11 +572,7 @@ const hostBridge = Object.freeze({
534
572
  if (!cloudflareHostMayBindSubagent(host)) return;
535
573
  const existing = hostSessions.get(sessionId);
536
574
  if (existing && existing !== host) {
537
- const ownerId = cloudflareHostReservations.get(host)?.ownerId;
538
- if (ownerId === undefined
539
- || cloudflareHostReservations.get(existing)?.ownerId !== ownerId) {
540
- throw new Error(`Nanocodex subagent session ID is already active: ${sessionId}`);
541
- }
575
+ throw new Error(`Nanocodex subagent session ID is already active: ${sessionId}`);
542
576
  }
543
577
  host.bindSubagentSession(sessionId, JSON.parse(contextJson), hostContextRef);
544
578
  hostSessions.set(sessionId, host);
@@ -556,10 +590,10 @@ const hostBridge = Object.freeze({
556
590
  }
557
591
  if (!host || hostSessions.get(sessionId) !== host) return;
558
592
  host.releaseSession(sessionId);
559
- if (hostSessions.get(sessionId) === host) hostSessions.delete(sessionId);
593
+ releaseHostSession(host, sessionId);
560
594
  },
561
- executeCode(source, sessionId, callId, model) {
562
- return requiredSessionHost(sessionId).executeCode(source, sessionId, callId, model);
595
+ executeCode(source, sessionId, callId, model, turnId) {
596
+ return requiredSessionHost(sessionId).executeCode(source, sessionId, callId, model, turnId);
563
597
  },
564
598
  waitCode(input, sessionId, callId) {
565
599
  return requiredSessionHost(sessionId).waitCode(input, sessionId, callId);
@@ -567,8 +601,8 @@ const hostBridge = Object.freeze({
567
601
  nextCodeUpdate(sessionId, callId) {
568
602
  return requiredSessionHost(sessionId).nextCodeUpdate(sessionId, callId);
569
603
  },
570
- executeTool(name, input, sessionId, callId, model) {
571
- return requiredSessionHost(sessionId).executeTool(name, input, sessionId, callId, model);
604
+ executeTool(name, input, sessionId, callId, model, turnId) {
605
+ return requiredSessionHost(sessionId).executeTool(name, input, sessionId, callId, model, turnId);
572
606
  },
573
607
  beginCodeTurn(sessionId) {
574
608
  hostSessions.get(sessionId)?.beginCodeTurn?.(sessionId);
@@ -827,7 +861,7 @@ export function prepareCloudflareAgentSession(sessionId, ownerId) {
827
861
  return reservation;
828
862
  }
829
863
 
830
- /** @internal Whether this exact Cloudflare generation may publish child descriptors. */
864
+ /** @internal Whether this exact Cloudflare generation may bind live children. */
831
865
  export function mayBindCloudflareSubagentSession(reservation) {
832
866
  return cloudflareAgentSessions.has(reservation)
833
867
  && !reservation.released
@@ -835,7 +869,7 @@ export function mayBindCloudflareSubagentSession(reservation) {
835
869
  || activeAgentSessions.get(reservation.sessionId) === reservation);
836
870
  }
837
871
 
838
- /** @internal Whether this committed current generation may delete child descriptors. */
872
+ /** @internal Whether this committed current generation may release live children. */
839
873
  export function mayReleaseCloudflareSubagentSession(reservation) {
840
874
  return cloudflareAgentSessions.has(reservation)
841
875
  && !reservation.released
@@ -871,13 +905,16 @@ export function activateCloudflareAgentSession(reservation) {
871
905
  }
872
906
 
873
907
  /** Internal Cloudflare seam: publishes a reconstructed owner after adapter setup succeeds. */
874
- export function commitCloudflareAgentSession(reservation) {
908
+ export function commitCloudflareAgentSession(reservation, beforeCommit) {
875
909
  if (!cloudflareAgentSessions.has(reservation)
876
910
  || reservation.released
877
911
  || !reservation.adopted
878
912
  || activeAgentSessions.get(reservation.sessionId) !== reservation) {
879
913
  throw new Error("Cloudflare Agent session reservation is not ready to commit");
880
914
  }
915
+ // A synchronous durable transition must succeed before rollback authority is
916
+ // relinquished. No other generation can interleave validation and publication.
917
+ beforeCommit?.();
881
918
  reservation.committed = true;
882
919
  reservation.predecessor = undefined;
883
920
  reservation.predecessorHost = undefined;
@@ -938,7 +975,9 @@ async function joinAgentShutdown(state) {
938
975
  let shutdownFailed = false;
939
976
  let shutdownError;
940
977
  try {
941
- await state.raw.shutdown();
978
+ await (state.runtime.shutdown === undefined
979
+ ? state.raw.shutdown()
980
+ : state.runtime.shutdown(state.raw));
942
981
  } catch (error) {
943
982
  shutdownFailed = true;
944
983
  shutdownError = error;