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.
- package/README.md +37 -11
- package/browser/Agent.d.mts +1 -1
- package/browser/ElevenLabsPlayback.mjs +119 -0
- package/browser/InlineAgent.mjs +22 -17
- package/browser/Transport.d.mts +2 -0
- package/browser/Transport.mjs +3 -2
- package/browser/Voice.d.mts +4 -0
- package/browser/Voice.mjs +38 -6
- package/browser/VoiceSession.mjs +37 -7
- package/browser/host.d.mts +20 -4
- package/browser/host.mjs +156 -18
- package/cloud/types.d.mts +1 -0
- package/cloudflare/Agent.d.mts +14 -9
- package/cloudflare/Agent.mjs +354 -96
- package/cloudflare/agent-settings.d.mts +15 -0
- package/cloudflare/agent-settings.mjs +135 -0
- package/cloudflare/chat-reasoning.mjs +20 -0
- package/cloudflare/durable-placement.d.mts +12 -0
- package/cloudflare/durable-placement.mjs +82 -0
- package/cloudflare/egress.d.mts +31 -0
- package/cloudflare/gateway-binding-responses.mjs +84 -0
- package/cloudflare/gateway-responses.d.mts +48 -0
- package/cloudflare/gateway-responses.mjs +191 -0
- package/cloudflare/index.d.mts +6 -0
- package/cloudflare/index.mjs +3 -0
- package/cloudflare/managed-auth.d.mts +15 -0
- package/cloudflare/managed-auth.mjs +119 -0
- package/cloudflare/managed-live.d.mts +7 -0
- package/cloudflare/managed-live.mjs +56 -0
- package/cloudflare/prepared-connection.mjs +49 -0
- package/cloudflare/provider-stream.mjs +353 -0
- package/cloudflare/rpc.d.mts +9 -0
- package/cloudflare/rpc.mjs +14 -0
- package/cloudflare/tool-alias-diagnostic.mjs +21 -0
- package/cloudflare/tracing.d.mts +16 -0
- package/cloudflare/tracing.mjs +47 -0
- package/cloudflare/workers-ai-responses.d.mts +28 -0
- package/cloudflare/workers-ai-responses.mjs +399 -0
- package/host/Agent.d.mts +1 -1
- package/index.d.mts +2 -0
- package/internal.mjs +53 -14
- package/managed/Agent.d.mts +25 -69
- package/managed/Agent.mjs +82 -196
- package/managed/README.md +9 -38
- package/managed/Voice.mjs +5 -2
- package/managed/index.d.mts +0 -3
- package/managed/internal.mjs +2 -1
- package/node/Agent.d.mts +1 -1
- package/node/Agent.mjs +9 -3
- package/node/host.mjs +5 -0
- package/package.json +34 -2
- package/pkg-node/nanocodex.d.ts +22 -18
- package/pkg-node/nanocodex.js +118 -92
- package/pkg-node/nanocodex_bg.wasm.d.ts +6 -5
- package/pkg-web/.nanocodex-bindgen-stamp +7 -1
- package/pkg-web/nanocodex-build.json +1 -1
- package/pkg-web/nanocodex.d.ts +28 -23
- package/pkg-web/nanocodex.js +117 -91
- package/pkg-web/nanocodex_bg.js +117 -91
- package/pkg-web/nanocodex_bg.wasm +0 -0
- package/pkg-web/nanocodex_bg.wasm.d.ts +6 -5
- package/pkg-web/nanocodex_worker.js +1 -1
- package/runtime/before-compaction.mjs +49 -0
- package/runtime/code-evaluator.worker.mjs +2 -9
- package/runtime/managed-transport.mjs +10 -2
- package/runtime/mcp-runtime.mjs +20 -4
- package/runtime/quickjs-evaluator.mjs +11 -5
- package/runtime/response-controls.mjs +67 -4
- package/runtime/steer-receipt.mjs +13 -0
- package/runtime/subagent-routing.d.mts +38 -0
- package/runtime/subagent-routing.mjs +81 -0
- package/runtime/subagents.d.mts +1 -1
- package/runtime/tempo-provider.d.mts +1 -1
- package/runtime/tempo-provider.mjs +1 -1
- package/tools/environment.d.mts +17 -2
- package/tools/environment.mjs +30 -3
- package/tools/index.d.mts +2 -0
- 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;
|
|
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
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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;
|