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
package/cloudflare/Agent.mjs
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
|
+
import { steerInputKey } from "../runtime/steer-receipt.mjs";
|
|
2
|
+
export { steerInputKey };
|
|
3
|
+
import { createGatewayResponses } from "./gateway-responses.mjs";
|
|
4
|
+
import { createWorkersAiResponses } from "./workers-ai-responses.mjs";
|
|
1
5
|
import { responseControlsBody, responseControlsSocket } from "../runtime/response-controls.mjs";
|
|
2
6
|
import * as HostAgent from "../host/Agent.mjs";
|
|
3
7
|
import {
|
|
4
8
|
CLOUDFLARE_SESSION_RESERVATION,
|
|
9
|
+
checkpoint as checkpointAgent,
|
|
5
10
|
commitCloudflareAgentSession,
|
|
6
11
|
installHostBridge,
|
|
7
12
|
loadDurabilityRuntime,
|
|
@@ -12,7 +17,7 @@ import {
|
|
|
12
17
|
releaseAgentSession,
|
|
13
18
|
routePrompt,
|
|
14
19
|
} from "../internal.mjs";
|
|
15
|
-
import { pruneDurableReceipts as pruneWasmDurableReceipts
|
|
20
|
+
import { pruneDurableReceipts as pruneWasmDurableReceipts } from "../pkg-web/nanocodex.js";
|
|
16
21
|
import * as Transport from "../browser/Transport.mjs";
|
|
17
22
|
import { initializeBrowserEngine } from "../browser/engine.mjs";
|
|
18
23
|
import { createCloudflareDurabilityStore } from "../runtime/cloudflare-durability-store.mjs";
|
|
@@ -24,6 +29,7 @@ import {
|
|
|
24
29
|
importDurabilityState as importPortableState,
|
|
25
30
|
} from "../runtime/durability-store.mjs";
|
|
26
31
|
import { cloudflareEgress } from "./egress.mjs";
|
|
32
|
+
import { prepareConnection } from "./prepared-connection.mjs";
|
|
27
33
|
import { scopeCloudflareEgress } from "./egress-subject.mjs";
|
|
28
34
|
import {
|
|
29
35
|
clearCloudflareEventSocket,
|
|
@@ -33,7 +39,9 @@ import {
|
|
|
33
39
|
const STARTUP_TIMEOUT_MS = 10_000;
|
|
34
40
|
const INTERNAL_RUNTIME = Symbol.for("nanocodex.cloudflare.internalRuntime");
|
|
35
41
|
const INTERNAL_CONFIGURATION = Symbol.for("nanocodex.cloudflare.internalConfiguration");
|
|
42
|
+
const INTERNAL_FORK_RESUME = Symbol.for("nanocodex.cloudflare.internalForkResume");
|
|
36
43
|
const EPHEMERAL_APPLICATION_OPTIONS = new Set([
|
|
44
|
+
"beforeCompaction",
|
|
37
45
|
"additionalInstructions",
|
|
38
46
|
"fastMode",
|
|
39
47
|
"instructions",
|
|
@@ -46,6 +54,7 @@ const EPHEMERAL_APPLICATION_OPTIONS = new Set([
|
|
|
46
54
|
"workspace",
|
|
47
55
|
]);
|
|
48
56
|
const APPLICATION_OPTIONS = new Set([
|
|
57
|
+
"beforeCompaction",
|
|
49
58
|
"additionalInstructions",
|
|
50
59
|
"durabilityId",
|
|
51
60
|
"eventPersistence",
|
|
@@ -58,13 +67,12 @@ const lifecycles = new WeakMap();
|
|
|
58
67
|
/** @internal Binds the package-owned module to the public Cloudflare namespace. */
|
|
59
68
|
export function bindAgent(module, hostAgent = HostAgent) {
|
|
60
69
|
return Object.freeze({
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
return JSON.parse(managedBootstrapPlan(input));
|
|
64
|
-
},
|
|
70
|
+
steerReceipt,
|
|
71
|
+
steerInputKey,
|
|
65
72
|
pruneDurableReceipts: (owner, options) => pruneDurableReceipts(module, owner, options),
|
|
66
73
|
create: (owner, options) => create(module, owner, options, hostAgent),
|
|
67
74
|
createEphemeral: (owner, options) => createEphemeral(module, owner, options),
|
|
75
|
+
checkpoint,
|
|
68
76
|
destroy,
|
|
69
77
|
exportDurabilityState,
|
|
70
78
|
exportDurabilityHead,
|
|
@@ -73,6 +81,11 @@ export function bindAgent(module, hostAgent = HostAgent) {
|
|
|
73
81
|
});
|
|
74
82
|
}
|
|
75
83
|
|
|
84
|
+
/** Copies the latest safe committed boundary as a resumable SessionSnapshot. */
|
|
85
|
+
export function checkpoint(agent) {
|
|
86
|
+
return checkpointAgent(agent);
|
|
87
|
+
}
|
|
88
|
+
|
|
76
89
|
/** Atomically steers an active Cloudflare Agent turn or starts a new turn. */
|
|
77
90
|
export function route(agent, options) {
|
|
78
91
|
return routePrompt(agent, options);
|
|
@@ -117,7 +130,7 @@ export function destroy(owner) {
|
|
|
117
130
|
stateId,
|
|
118
131
|
);
|
|
119
132
|
}
|
|
120
|
-
storage.sql.exec("
|
|
133
|
+
storage.sql.exec("DROP TABLE IF EXISTS nanocodex_cloudflare_fork_resume");
|
|
121
134
|
clearCloudflareEventSocket(context);
|
|
122
135
|
});
|
|
123
136
|
}
|
|
@@ -274,13 +287,111 @@ export async function create(module, owner, options = {}, hostAgent = HostAgent)
|
|
|
274
287
|
}
|
|
275
288
|
lifecycle.creating = true;
|
|
276
289
|
try {
|
|
277
|
-
|
|
290
|
+
const prepare = options?.[INTERNAL_RUNTIME]?.prepare;
|
|
291
|
+
if (prepare === undefined) return await createOwned(module, resolved, options, hostAgent, lifecycle);
|
|
292
|
+
return await createPrepared(module, resolved, options, hostAgent, lifecycle, prepare);
|
|
278
293
|
} finally {
|
|
279
294
|
lifecycle.creating = false;
|
|
280
295
|
}
|
|
281
296
|
}
|
|
282
297
|
|
|
283
|
-
|
|
298
|
+
// Private managed construction barrier. The adapter retains lifecycle, identity,
|
|
299
|
+
// endpoint and socket ownership throughout tool discovery; callers receive only
|
|
300
|
+
// a single-use continuation, never a transport or a session identity.
|
|
301
|
+
async function createPrepared(module, resolved, options, hostAgent, lifecycle, prepare) {
|
|
302
|
+
applicationOptions(options);
|
|
303
|
+
if (typeof prepare !== "function") throw new TypeError("Cloudflare Agent preparation must be a function");
|
|
304
|
+
const configuration = options[INTERNAL_CONFIGURATION];
|
|
305
|
+
validateInternalConfiguration(configuration);
|
|
306
|
+
if (!configuration || !["gpt-6-astra", "gpt-6.1-sol", "gpt-6-luna"].includes(configuration.model)) {
|
|
307
|
+
throw new Error("Cloudflare Agent preparation requires a native root configuration");
|
|
308
|
+
}
|
|
309
|
+
const runtime = options[INTERNAL_RUNTIME];
|
|
310
|
+
if (Object.keys(runtime).some(key => !["prepare", "preparationSignal"].includes(key))) {
|
|
311
|
+
throw new TypeError("Cloudflare Agent preparation owns its native transport policy");
|
|
312
|
+
}
|
|
313
|
+
const signal = runtime.preparationSignal;
|
|
314
|
+
signal?.throwIfAborted();
|
|
315
|
+
createCloudflareDurabilityStore(resolved.context.storage);
|
|
316
|
+
initializeAgentStorage(resolved.context.storage);
|
|
317
|
+
const pristineAtStart = storedSessionId(resolved.context.storage) === undefined
|
|
318
|
+
&& storedStateId(resolved.context.storage) === undefined;
|
|
319
|
+
const initialForkResume = options?.[INTERNAL_FORK_RESUME];
|
|
320
|
+
let initialForkDigest;
|
|
321
|
+
if (initialForkResume !== undefined) {
|
|
322
|
+
if (!initialForkResume || typeof initialForkResume !== "object" || Array.isArray(initialForkResume))
|
|
323
|
+
throw new TypeError("Cloudflare Agent fork resume must be a SessionSnapshot");
|
|
324
|
+
const bytes = new Uint8Array(await crypto.subtle.digest("SHA-256",
|
|
325
|
+
new TextEncoder().encode(JSON.stringify(initialForkResume))));
|
|
326
|
+
initialForkDigest = [...bytes].map(byte => byte.toString(16).padStart(2, "0")).join("");
|
|
327
|
+
}
|
|
328
|
+
const { sessionId, stateId } = durableIdentity(resolved.context.storage, options.durabilityId);
|
|
329
|
+
if (initialForkDigest !== undefined) {
|
|
330
|
+
// Pin the seed before optional tool discovery/transport preparation. A
|
|
331
|
+
// failed preparation can reopen the exact fork with the same snapshot.
|
|
332
|
+
resolved.context.storage.sql.exec(`CREATE TABLE IF NOT EXISTS nanocodex_cloudflare_fork_resume (
|
|
333
|
+
singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
|
|
334
|
+
state_id TEXT NOT NULL, digest TEXT NOT NULL
|
|
335
|
+
)`);
|
|
336
|
+
const previous = resolved.context.storage.sql.exec(
|
|
337
|
+
"SELECT state_id, digest FROM nanocodex_cloudflare_fork_resume WHERE singleton = 1",
|
|
338
|
+
).toArray()[0];
|
|
339
|
+
if ((!pristineAtStart && !previous) || (previous
|
|
340
|
+
&& (previous.state_id !== stateId || previous.digest !== initialForkDigest))) {
|
|
341
|
+
throw new Error("Cloudflare Agent fork resume conflicts with its retained seed");
|
|
342
|
+
}
|
|
343
|
+
resolved.context.storage.sql.exec(
|
|
344
|
+
"INSERT OR IGNORE INTO nanocodex_cloudflare_fork_resume(singleton,state_id,digest) VALUES (1,?,?)",
|
|
345
|
+
stateId, initialForkDigest);
|
|
346
|
+
}
|
|
347
|
+
const endpoint = cloudflareEgress({ binding: scopeCloudflareEgress(resolved.egress, resolved.subject) });
|
|
348
|
+
const connection = prepareConnection(endpoint, sessionId, signal);
|
|
349
|
+
let completing;
|
|
350
|
+
let accepting = true;
|
|
351
|
+
let attempted = false;
|
|
352
|
+
try {
|
|
353
|
+
// Share the realm's engine initialization while the socket and tool metadata
|
|
354
|
+
// load. No Agent or durable runtime owner exists until preparation finishes.
|
|
355
|
+
installHostBridge();
|
|
356
|
+
const initializing = initializeBrowserEngine({ module });
|
|
357
|
+
void initializing.catch(() => {});
|
|
358
|
+
const result = await prepare((prepared) => {
|
|
359
|
+
if (!accepting || attempted) throw new Error("Cloudflare Agent preparation was already completed");
|
|
360
|
+
attempted = true;
|
|
361
|
+
signal?.throwIfAborted();
|
|
362
|
+
completing = initializing.then(() => {
|
|
363
|
+
signal?.throwIfAborted();
|
|
364
|
+
const runtime = prepared?.[INTERNAL_RUNTIME];
|
|
365
|
+
const pinned = prepared?.[INTERNAL_CONFIGURATION];
|
|
366
|
+
validateInternalConfiguration(pinned);
|
|
367
|
+
if (prepared?.durabilityId !== stateId
|
|
368
|
+
|| ["model", "reasoning_mode"].some(key => pinned?.[key] !== configuration[key])
|
|
369
|
+
|| runtime?.prepare !== undefined || runtime?.workersAi !== undefined || runtime?.gateway !== undefined
|
|
370
|
+
|| (runtime?.inferenceForSession !== undefined && runtime?.preserveRootTransport !== true)
|
|
371
|
+
|| (initialForkDigest !== undefined && JSON.stringify(prepared?.[INTERNAL_FORK_RESUME]) !== JSON.stringify(initialForkResume))) {
|
|
372
|
+
throw new Error("Cloudflare Agent preparation changed its pinned transport or configuration");
|
|
373
|
+
}
|
|
374
|
+
return createOwned(module, resolved, prepared, hostAgent, lifecycle, connection,
|
|
375
|
+
pristineAtStart ? { sessionId, stateId } : undefined);
|
|
376
|
+
});
|
|
377
|
+
void completing.catch(() => {});
|
|
378
|
+
return completing;
|
|
379
|
+
});
|
|
380
|
+
if (completing === undefined || result !== await completing) {
|
|
381
|
+
throw new Error("Cloudflare Agent preparation must return its completed Agent");
|
|
382
|
+
}
|
|
383
|
+
return result;
|
|
384
|
+
} catch (error) {
|
|
385
|
+
const agent = await completing?.catch(() => undefined);
|
|
386
|
+
if (agent !== undefined) await agent.session.shutdown();
|
|
387
|
+
throw error;
|
|
388
|
+
} finally {
|
|
389
|
+
accepting = false;
|
|
390
|
+
await connection.dispose();
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
async function createOwned(module, resolved, options, hostAgent, lifecycle, preparedConnection, pristinePreparedIdentity) {
|
|
284
395
|
const { context, egress, subject } = resolved;
|
|
285
396
|
const configured = applicationOptions(options);
|
|
286
397
|
const {
|
|
@@ -288,12 +399,25 @@ async function createOwned(module, resolved, options, hostAgent, lifecycle) {
|
|
|
288
399
|
eventPersistence = "durable",
|
|
289
400
|
[INTERNAL_RUNTIME]: internalRuntime,
|
|
290
401
|
[INTERNAL_CONFIGURATION]: internalConfiguration,
|
|
402
|
+
[INTERNAL_FORK_RESUME]: forkResume,
|
|
291
403
|
...agentOptions
|
|
292
404
|
} = configured;
|
|
293
405
|
if (internalRuntime !== undefined
|
|
294
406
|
&& (!internalRuntime || typeof internalRuntime !== "object" || Array.isArray(internalRuntime))) {
|
|
295
407
|
throw new TypeError("Cloudflare Agent internal runtime options must be an object");
|
|
296
408
|
}
|
|
409
|
+
if (internalRuntime?.onSocketTiming !== undefined
|
|
410
|
+
&& typeof internalRuntime.onSocketTiming !== "function") {
|
|
411
|
+
throw new TypeError("Cloudflare Agent socket timing hook must be a function");
|
|
412
|
+
}
|
|
413
|
+
if (internalRuntime?.onResponseCreateSent !== undefined
|
|
414
|
+
&& typeof internalRuntime.onResponseCreateSent !== "function") {
|
|
415
|
+
throw new TypeError("Cloudflare Agent response.create sent hook must be a function");
|
|
416
|
+
}
|
|
417
|
+
if (internalRuntime?.onRequestShape !== undefined
|
|
418
|
+
&& typeof internalRuntime.onRequestShape !== "function") {
|
|
419
|
+
throw new TypeError("Cloudflare Agent request shape hook must be a function");
|
|
420
|
+
}
|
|
297
421
|
if (internalRuntime?.subagentLifecycle !== undefined
|
|
298
422
|
&& typeof internalRuntime.subagentLifecycle !== "function") {
|
|
299
423
|
throw new TypeError("Cloudflare Agent subagent lifecycle hook must be a function");
|
|
@@ -302,31 +426,174 @@ async function createOwned(module, resolved, options, hostAgent, lifecycle) {
|
|
|
302
426
|
&& typeof internalRuntime.waitForPreconnect !== "boolean") {
|
|
303
427
|
throw new TypeError("Cloudflare Agent internal waitForPreconnect must be a boolean");
|
|
304
428
|
}
|
|
429
|
+
if (internalRuntime?.preserveRootTransport !== undefined
|
|
430
|
+
&& typeof internalRuntime.preserveRootTransport !== "boolean") {
|
|
431
|
+
throw new TypeError("Cloudflare Agent root transport policy must be a boolean");
|
|
432
|
+
}
|
|
433
|
+
if (internalRuntime?.inferenceForSession !== undefined
|
|
434
|
+
&& typeof internalRuntime.inferenceForSession !== "function") {
|
|
435
|
+
throw new TypeError("Cloudflare Agent inference routing must be a function");
|
|
436
|
+
}
|
|
437
|
+
if (internalRuntime?.subagentRouting !== undefined && internalRuntime?.inferenceForSession === undefined) {
|
|
438
|
+
throw new TypeError("Subagent routing requires session-specific inference routing");
|
|
439
|
+
}
|
|
305
440
|
validateInternalConfiguration(internalConfiguration);
|
|
306
441
|
const eventSocket = eventPersistence === "durable"
|
|
307
442
|
? createCloudflareEventSocket(context)
|
|
308
443
|
: undefined;
|
|
309
444
|
if (eventPersistence === "caller") clearCloudflareEventSocket(context);
|
|
310
445
|
const durability = createCloudflareDurabilityStore(context.storage);
|
|
446
|
+
let resumeDigest;
|
|
447
|
+
if (forkResume !== undefined) {
|
|
448
|
+
if (!forkResume || typeof forkResume !== "object" || Array.isArray(forkResume)) {
|
|
449
|
+
throw new TypeError("Cloudflare Agent fork resume must be a SessionSnapshot");
|
|
450
|
+
}
|
|
451
|
+
initializeAgentStorage(context.storage);
|
|
452
|
+
context.storage.sql.exec(`CREATE TABLE IF NOT EXISTS nanocodex_cloudflare_fork_resume (
|
|
453
|
+
singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
|
|
454
|
+
state_id TEXT NOT NULL, digest TEXT NOT NULL
|
|
455
|
+
)`);
|
|
456
|
+
const digestBytes = new Uint8Array(await crypto.subtle.digest("SHA-256",
|
|
457
|
+
new TextEncoder().encode(JSON.stringify(forkResume))));
|
|
458
|
+
resumeDigest = [...digestBytes].map(byte => byte.toString(16).padStart(2, "0")).join("");
|
|
459
|
+
const recordedSeed = context.storage.sql.exec(
|
|
460
|
+
"SELECT state_id, digest FROM nanocodex_cloudflare_fork_resume WHERE singleton = 1",
|
|
461
|
+
).toArray()[0];
|
|
462
|
+
const existingSessionId = storedSessionId(context.storage);
|
|
463
|
+
const existingStateId = storedStateId(context.storage);
|
|
464
|
+
// The preparation barrier may have installed the new identity already. A
|
|
465
|
+
// failed/restarted child construction may retain that identity and a
|
|
466
|
+
// revision-zero owner fence, but still has no executed checkpoint. The
|
|
467
|
+
// durable seed is unchanged; allow that cold retry and reject any real head.
|
|
468
|
+
if ((existingSessionId !== undefined || existingStateId !== undefined)
|
|
469
|
+
&& !((pristinePreparedIdentity?.sessionId === existingSessionId
|
|
470
|
+
&& pristinePreparedIdentity?.stateId === existingStateId)
|
|
471
|
+
|| (existingSessionId !== undefined && existingStateId !== undefined
|
|
472
|
+
&& (durabilityId === undefined || existingStateId === durabilityId)
|
|
473
|
+
&& recordedSeed?.state_id === existingStateId
|
|
474
|
+
&& recordedSeed?.digest === resumeDigest))) {
|
|
475
|
+
throw new Error("Cloudflare Agent fork resume requires a pristine Durable Object");
|
|
476
|
+
}
|
|
477
|
+
if (recordedSeed && (recordedSeed.digest !== resumeDigest
|
|
478
|
+
|| (existingStateId !== undefined && recordedSeed.state_id !== existingStateId))) {
|
|
479
|
+
throw new Error("Cloudflare Agent fork resume conflicts with its retained seed");
|
|
480
|
+
}
|
|
481
|
+
const checkStateId = existingStateId ?? durabilityId;
|
|
482
|
+
if (checkStateId !== undefined && context.storage.sql.exec(
|
|
483
|
+
"SELECT revision, payload FROM nanocodex_durable_states WHERE state_id = ?", checkStateId,
|
|
484
|
+
).toArray().some(row => row.revision !== "0" || row.payload !== null)) {
|
|
485
|
+
throw new Error("Cloudflare Agent fork resume requires pristine durability state");
|
|
486
|
+
}
|
|
487
|
+
}
|
|
311
488
|
const { sessionId, stateId } = durableIdentity(context.storage, durabilityId);
|
|
312
|
-
|
|
489
|
+
if (resumeDigest !== undefined) {
|
|
490
|
+
context.storage.sql.exec(
|
|
491
|
+
"INSERT OR IGNORE INTO nanocodex_cloudflare_fork_resume(singleton,state_id,digest) VALUES (1,?,?)",
|
|
492
|
+
stateId, resumeDigest);
|
|
493
|
+
}
|
|
494
|
+
const workersAi = internalRuntime?.workersAi;
|
|
495
|
+
const gateway = internalRuntime?.gateway;
|
|
496
|
+
if (gateway !== undefined && (workersAi !== undefined
|
|
497
|
+
|| gateway.model !== internalConfiguration?.model
|
|
498
|
+
|| gateway.reasoningEffort !== internalConfiguration?.thinking)) {
|
|
499
|
+
throw new TypeError("Gateway profile must match the pinned model and thinking, with one transport only");
|
|
500
|
+
}
|
|
501
|
+
const routedInference = internalRuntime?.inferenceForSession !== undefined;
|
|
502
|
+
const directInference = workersAi !== undefined || gateway !== undefined
|
|
503
|
+
|| (routedInference && internalRuntime?.preserveRootTransport !== true);
|
|
504
|
+
if (workersAi !== undefined && (internalConfiguration?.model !== "@cf/zai-org/glm-5.3"
|
|
505
|
+
|| workersAi.model !== internalConfiguration.model || workersAi.thinking !== internalConfiguration.thinking)) {
|
|
506
|
+
throw new TypeError("Workers AI profile must match the pinned model and thinking");
|
|
507
|
+
}
|
|
508
|
+
if (["@cf/zai-org/glm-5.3", "kimi-k3", "mimo-v2.6-pro"].includes(internalConfiguration?.model) && !directInference) {
|
|
509
|
+
throw new TypeError("GLM-5.3 requires a Workers AI or gateway transport binding");
|
|
510
|
+
}
|
|
511
|
+
const endpoint = gateway !== undefined ? createGatewayResponses(gateway) : workersAi === undefined ? cloudflareEgress({
|
|
313
512
|
binding: scopeCloudflareEgress(egress, subject),
|
|
314
|
-
});
|
|
513
|
+
}) : createWorkersAiResponses(workersAi.ai);
|
|
514
|
+
const frontierEndpoint = routedInference ? cloudflareEgress({ binding: scopeCloudflareEgress(egress, subject) }) : undefined;
|
|
315
515
|
const startup = deferred();
|
|
316
516
|
const transport = Transport.hostManaged({
|
|
317
517
|
...endpoint,
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
518
|
+
stateless: directInference,
|
|
519
|
+
websocketPreconnect: !directInference,
|
|
520
|
+
async createResponse(url, id, request) {
|
|
521
|
+
let selected = endpoint;
|
|
522
|
+
const body = responseControlsBody(request.body, internalRuntime?.responseControls);
|
|
523
|
+
if (routedInference) {
|
|
524
|
+
// This callback rechecks retained authority on EVERY request, including
|
|
525
|
+
// the root. Never inherit the root provider when a child pin is missing.
|
|
526
|
+
// The transport session is the shared lineage; threadId identifies the
|
|
527
|
+
// actual root/child branch registered by the Rust host bridge.
|
|
528
|
+
const routedSessionId = request.threadId ?? id;
|
|
529
|
+
const profile = await internalRuntime.inferenceForSession(routedSessionId);
|
|
530
|
+
if (profile?.native === true) {
|
|
531
|
+
if (internalRuntime?.preserveRootTransport !== true || profile.gateway !== undefined || profile.workersAi !== undefined) {
|
|
532
|
+
throw new Error("Native inference cannot replace a routed transport");
|
|
533
|
+
}
|
|
534
|
+
const parsed = JSON.parse(body);
|
|
535
|
+
if (!["gpt-6-astra", "gpt-6.1-sol", "gpt-6-luna"].includes(parsed.model)
|
|
536
|
+
|| !["none", "low", "medium", "high", "xhigh", "max"].includes(parsed.reasoning?.effort)) {
|
|
537
|
+
throw new Error("Native inference requires a supported GPT model and thinking");
|
|
538
|
+
}
|
|
539
|
+
// Native roots and children retain Rust-owned live defaults/overrides.
|
|
540
|
+
// Only routed sessions have immutable model/thinking pins.
|
|
541
|
+
selected = frontierEndpoint;
|
|
542
|
+
} else {
|
|
543
|
+
if (!profile || typeof profile.model !== "string" || !["low", "medium", "high"].includes(profile.thinking)) {
|
|
544
|
+
throw new Error("Session inference route is missing or invalid");
|
|
545
|
+
}
|
|
546
|
+
if (routedSessionId === sessionId && (profile.model !== internalConfiguration?.model
|
|
547
|
+
|| profile.thinking !== internalConfiguration?.thinking)) {
|
|
548
|
+
throw new Error("Root inference route conflicts with its pinned configuration");
|
|
549
|
+
}
|
|
550
|
+
const parsed = JSON.parse(body);
|
|
551
|
+
if (parsed.model !== profile.model || parsed.reasoning?.effort !== profile.thinking) {
|
|
552
|
+
throw new Error("Inference request conflicts with the session model or thinking pin");
|
|
553
|
+
}
|
|
554
|
+
if (profile.workersAi !== undefined && profile.gateway !== undefined) {
|
|
555
|
+
throw new Error("Session inference route has multiple transports");
|
|
556
|
+
}
|
|
557
|
+
if (profile.gateway !== undefined) {
|
|
558
|
+
if (profile.gateway.model !== profile.model || profile.gateway.reasoningEffort !== profile.thinking) {
|
|
559
|
+
throw new Error("Gateway profile conflicts with the session pin");
|
|
560
|
+
}
|
|
561
|
+
selected = createGatewayResponses(profile.gateway);
|
|
562
|
+
} else if (profile.workersAi !== undefined) {
|
|
563
|
+
if (profile.model !== "@cf/zai-org/glm-5.3" || profile.workersAi.model !== profile.model
|
|
564
|
+
|| profile.workersAi.thinking !== profile.thinking) {
|
|
565
|
+
throw new Error("Workers AI profile conflicts with the session pin");
|
|
566
|
+
}
|
|
567
|
+
selected = createWorkersAiResponses(profile.workersAi.ai);
|
|
568
|
+
} else {
|
|
569
|
+
if (!["gpt-6-astra", "gpt-6.1-sol", "gpt-6-luna"].includes(profile.model)) {
|
|
570
|
+
throw new Error("Session model requires an explicit inference binding");
|
|
571
|
+
}
|
|
572
|
+
selected = frontierEndpoint;
|
|
573
|
+
}
|
|
574
|
+
if (url !== `${endpoint.apiBaseUrl}/responses`) {
|
|
575
|
+
throw new Error("Routed inference supports only full-history Responses requests");
|
|
576
|
+
}
|
|
577
|
+
url = `${selected.apiBaseUrl}/responses`;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
return selected.createResponse(url, id, { ...request, body });
|
|
324
581
|
},
|
|
325
582
|
async createWebSocket(url, id, request) {
|
|
583
|
+
if (directInference) throw new Error("Direct inference threads require HTTP Responses transport");
|
|
584
|
+
if (routedInference && (request.threadId ?? id) !== sessionId) {
|
|
585
|
+
const profile = await internalRuntime.inferenceForSession(request.threadId ?? id);
|
|
586
|
+
if (profile?.native !== true || profile.gateway !== undefined || profile.workersAi !== undefined) {
|
|
587
|
+
throw new Error("Routed children require full-history HTTP Responses transport");
|
|
588
|
+
}
|
|
589
|
+
}
|
|
326
590
|
try {
|
|
327
|
-
const
|
|
591
|
+
const preparation = request.authorization === "preconnect" ? preparedConnection : undefined;
|
|
592
|
+
if (preparation !== undefined) preparedConnection = undefined;
|
|
593
|
+
const opened = await (preparation === undefined
|
|
594
|
+
? endpoint.createWebSocket(url, id, request) : preparation.take(url, id, request));
|
|
328
595
|
if (request.authorization === "preconnect") startup.resolve();
|
|
329
|
-
return { ...opened, socket: responseControlsSocket(opened.socket, internalRuntime?.responseControls) };
|
|
596
|
+
return { ...opened, socket: responseControlsSocket(opened.socket, internalRuntime?.responseControls, internalRuntime?.onRequestShape, internalRuntime?.onResponseCreateSent) };
|
|
330
597
|
} catch (error) {
|
|
331
598
|
if (request.authorization === "preconnect") startup.reject(error);
|
|
332
599
|
throw error;
|
|
@@ -336,7 +603,6 @@ async function createOwned(module, resolved, options, hostAgent, lifecycle) {
|
|
|
336
603
|
|
|
337
604
|
const sessionReservation = prepareCloudflareAgentSession(sessionId, subject);
|
|
338
605
|
const subagentSessions = cloudflareSubagentSessions(
|
|
339
|
-
context.storage,
|
|
340
606
|
sessionReservation,
|
|
341
607
|
internalRuntime?.subagentLifecycle,
|
|
342
608
|
);
|
|
@@ -353,6 +619,7 @@ async function createOwned(module, resolved, options, hostAgent, lifecycle) {
|
|
|
353
619
|
fastMode: internalConfiguration.fast_mode,
|
|
354
620
|
}),
|
|
355
621
|
module,
|
|
622
|
+
rawApiEvents: internalRuntime?.rawApiEvents,
|
|
356
623
|
toolMode: internalRuntime?.toolMode ?? "direct",
|
|
357
624
|
codeEvaluator: internalRuntime?.codeEvaluator,
|
|
358
625
|
[Symbol.for("nanocodex.browser.internalRuntime")]: {
|
|
@@ -360,17 +627,20 @@ async function createOwned(module, resolved, options, hostAgent, lifecycle) {
|
|
|
360
627
|
subagentsEnabled: internalRuntime?.subagentsEnabled,
|
|
361
628
|
subagentMaxConcurrency: internalRuntime?.subagentMaxConcurrency,
|
|
362
629
|
subagentSessions,
|
|
630
|
+
subagentRouting: internalRuntime?.subagentRouting,
|
|
631
|
+
onSocketTiming: internalRuntime?.onSocketTiming,
|
|
363
632
|
[CLOUDFLARE_SESSION_RESERVATION]: sessionReservation,
|
|
364
633
|
},
|
|
365
634
|
transport,
|
|
366
635
|
sessionId,
|
|
367
636
|
durability,
|
|
368
637
|
durabilityId: stateId,
|
|
638
|
+
...(forkResume === undefined ? {} : { resume: forkResume }),
|
|
369
639
|
});
|
|
370
640
|
// Managed voice needs the durable session before the separate Responses
|
|
371
641
|
// relay is ready. Its preconnection remains owned by the host and a later
|
|
372
642
|
// text turn consumes it through the same credential-checked transport.
|
|
373
|
-
if (internalRuntime?.waitForPreconnect !== false) {
|
|
643
|
+
if (!directInference && internalRuntime?.waitForPreconnect !== false) {
|
|
374
644
|
await withTimeout(
|
|
375
645
|
startup.promise,
|
|
376
646
|
STARTUP_TIMEOUT_MS,
|
|
@@ -554,12 +824,14 @@ function validateInternalConfiguration(configuration) {
|
|
|
554
824
|
"reasoning_mode",
|
|
555
825
|
"fast_mode",
|
|
556
826
|
].includes(key))
|
|
557
|
-
|| !["gpt-
|
|
827
|
+
|| !["gpt-6.1-sol", "gpt-6-luna", "gpt-6-astra", "@cf/zai-org/glm-5.3", "kimi-k3", "mimo-v2.6-pro"]
|
|
558
828
|
.includes(configuration.model)
|
|
559
829
|
|| !["none", "low", "medium", "high", "xhigh", "max"].includes(configuration.thinking)
|
|
560
830
|
|| !["standard", "pro"].includes(configuration.reasoning_mode)
|
|
561
831
|
|| typeof configuration.fast_mode !== "boolean"
|
|
562
|
-
|| (
|
|
832
|
+
|| (["@cf/zai-org/glm-5.3", "kimi-k3", "mimo-v2.6-pro"].includes(configuration.model)
|
|
833
|
+
&& (!(configuration.model === "kimi-k3" ? ["low", "high"] : ["low", "medium", "high"]).includes(configuration.thinking) || configuration.reasoning_mode !== "standard"))
|
|
834
|
+
|| (["gpt-6-astra", "gpt-6.1-sol"].includes(configuration.model) && configuration.thinking === "none")) {
|
|
563
835
|
throw new TypeError("Cloudflare Agent internal configuration is invalid");
|
|
564
836
|
}
|
|
565
837
|
}
|
|
@@ -629,42 +901,28 @@ function initializeAgentStorage(storage) {
|
|
|
629
901
|
state_id TEXT NOT NULL UNIQUE
|
|
630
902
|
)
|
|
631
903
|
`);
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
descriptor_json TEXT NOT NULL,
|
|
637
|
-
host_context_ref TEXT
|
|
638
|
-
)
|
|
639
|
-
`);
|
|
640
|
-
const subagentColumns = storage.sql.exec(
|
|
641
|
-
"PRAGMA table_info('nanocodex_cloudflare_subagents')",
|
|
642
|
-
).toArray();
|
|
643
|
-
if (!subagentColumns.some(({ name }) => name === "host_context_ref")) {
|
|
644
|
-
storage.sql.exec(
|
|
645
|
-
"ALTER TABLE nanocodex_cloudflare_subagents ADD COLUMN host_context_ref TEXT",
|
|
646
|
-
);
|
|
647
|
-
}
|
|
904
|
+
// These tables belonged to durable children. Root records and identity remain
|
|
905
|
+
// untouched; obsolete child data must never restore a task tree on startup.
|
|
906
|
+
storage.sql.exec("DROP TABLE IF EXISTS nanocodex_cloudflare_subagents");
|
|
907
|
+
storage.sql.exec("DROP TABLE IF EXISTS nanocodex_cloudflare_subagent_checkpoints");
|
|
648
908
|
}
|
|
649
909
|
|
|
650
|
-
function cloudflareSubagentSessions(
|
|
651
|
-
const
|
|
910
|
+
function cloudflareSubagentSessions(reservation, lifecycle) {
|
|
911
|
+
const bindings = new Map();
|
|
652
912
|
return Object.freeze({
|
|
653
|
-
|
|
654
|
-
const
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
)
|
|
662
|
-
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
hostContextRef(sessionId) {
|
|
667
|
-
return restoredHostContextRefs.get(sessionId);
|
|
913
|
+
bindingDescriptor(sessionId, descriptor, hostContextRef) {
|
|
914
|
+
const retained = bindings.get(sessionId);
|
|
915
|
+
if (retained === undefined) return descriptor;
|
|
916
|
+
const original = retained.descriptor;
|
|
917
|
+
const attachesInitialProvenance = retained.hostContextRef === undefined
|
|
918
|
+
&& typeof hostContextRef === "string" && descriptor.task === original.task;
|
|
919
|
+
if (original.sessionId !== descriptor.sessionId || original.agentId !== descriptor.agentId
|
|
920
|
+
|| original.parentAgentId !== descriptor.parentAgentId || original.role !== descriptor.role
|
|
921
|
+
|| (retained.hostContextRef !== hostContextRef && !attachesInitialProvenance)) {
|
|
922
|
+
throw new Error("Subagent binding identity or host context changed");
|
|
923
|
+
}
|
|
924
|
+
// Delegation replaces the Rust task, not its spawning-turn authority.
|
|
925
|
+
return original;
|
|
668
926
|
},
|
|
669
927
|
bind(sessionId, descriptor, hostContextRef) {
|
|
670
928
|
if (!mayBindCloudflareSubagentSession(reservation)) return;
|
|
@@ -672,53 +930,36 @@ function cloudflareSubagentSessions(storage, reservation, lifecycle) {
|
|
|
672
930
|
&& (typeof hostContextRef !== "string" || hostContextRef.length === 0)) {
|
|
673
931
|
throw new TypeError("subagent host context ref must be a non-empty string when supplied");
|
|
674
932
|
}
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
`INSERT INTO nanocodex_cloudflare_subagents
|
|
679
|
-
(session_id, agent_id, descriptor_json, host_context_ref) VALUES (?, ?, ?, ?)
|
|
680
|
-
ON CONFLICT (session_id) DO UPDATE SET
|
|
681
|
-
agent_id = excluded.agent_id,
|
|
682
|
-
descriptor_json = excluded.descriptor_json,
|
|
683
|
-
host_context_ref = excluded.host_context_ref`,
|
|
684
|
-
sessionId,
|
|
685
|
-
descriptor.agentId,
|
|
686
|
-
JSON.stringify(descriptor),
|
|
687
|
-
hostContextRef ?? null,
|
|
688
|
-
);
|
|
689
|
-
notifySubagentLifecycle(lifecycle, {
|
|
690
|
-
type,
|
|
691
|
-
rootSessionId: reservation.sessionId,
|
|
692
|
-
sessionId,
|
|
693
|
-
descriptor,
|
|
694
|
-
hostContextRef,
|
|
695
|
-
});
|
|
933
|
+
notifySubagentLifecycle(lifecycle, {
|
|
934
|
+
type: "bind", rootSessionId: reservation.sessionId,
|
|
935
|
+
sessionId, descriptor, hostContextRef,
|
|
696
936
|
});
|
|
697
|
-
|
|
937
|
+
bindings.set(sessionId, { descriptor, hostContextRef });
|
|
698
938
|
},
|
|
699
939
|
release(sessionId, hostContextRef) {
|
|
700
940
|
if (!mayReleaseCloudflareSubagentSession(reservation)) return;
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
941
|
+
const retained = bindings.get(sessionId);
|
|
942
|
+
if (retained === undefined || retained.hostContextRef !== hostContextRef) return;
|
|
943
|
+
// Closing a parent releases its complete live subtree.
|
|
944
|
+
const removed = new Set([retained.descriptor.agentId]);
|
|
945
|
+
let changed;
|
|
946
|
+
do {
|
|
947
|
+
changed = false;
|
|
948
|
+
for (const { descriptor } of bindings.values()) {
|
|
949
|
+
if (removed.has(descriptor.parentAgentId) && !removed.has(descriptor.agentId)) {
|
|
950
|
+
removed.add(descriptor.agentId);
|
|
951
|
+
changed = true;
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
} while (changed);
|
|
955
|
+
for (const { descriptor, hostContextRef: context } of bindings.values()) {
|
|
956
|
+
if (!removed.has(descriptor.agentId)) continue;
|
|
709
957
|
notifySubagentLifecycle(lifecycle, {
|
|
710
|
-
type: "release",
|
|
711
|
-
|
|
712
|
-
sessionId,
|
|
713
|
-
hostContextRef,
|
|
958
|
+
type: "release", rootSessionId: reservation.sessionId,
|
|
959
|
+
sessionId: descriptor.sessionId, hostContextRef: context,
|
|
714
960
|
});
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
WHERE session_id = ? AND host_context_ref IS ?`,
|
|
718
|
-
sessionId,
|
|
719
|
-
hostContextRef ?? null,
|
|
720
|
-
);
|
|
721
|
-
});
|
|
961
|
+
bindings.delete(descriptor.sessionId);
|
|
962
|
+
}
|
|
722
963
|
},
|
|
723
964
|
});
|
|
724
965
|
}
|
|
@@ -788,3 +1029,20 @@ async function withTimeout(promise, timeoutMs, message) {
|
|
|
788
1029
|
clearTimeout(timer);
|
|
789
1030
|
}
|
|
790
1031
|
}
|
|
1032
|
+
|
|
1033
|
+
/** Read a single Rust-owned receipt without loading/fencing the execution. */
|
|
1034
|
+
export function steerReceipt(owner, operationId, messageId) {
|
|
1035
|
+
const { storage } = resolveContext(owner);
|
|
1036
|
+
const tables = storage.sql.exec("SELECT name FROM sqlite_master WHERE type = 'table' AND name IN ('nanocodex_cloudflare_durability', 'nanocodex_durable_states')").toArray();
|
|
1037
|
+
if (tables.length !== 2) return null;
|
|
1038
|
+
const stateId = storedStateId(storage);
|
|
1039
|
+
if (stateId === undefined) return null;
|
|
1040
|
+
const path = `$.nanocodex_durable_state.operations.${JSON.stringify(operationId)}.steer_receipts.${JSON.stringify(messageId)}`;
|
|
1041
|
+
const row = storage.sql.exec("SELECT json_extract(payload, ?) AS receipt FROM nanocodex_durable_states WHERE state_id = ?", path, stateId).toArray()[0];
|
|
1042
|
+
if (row?.receipt == null) return null;
|
|
1043
|
+
const receipt = JSON.parse(row.receipt);
|
|
1044
|
+
if (!/^[a-f0-9]{64}$/.test(receipt.input_key) || !Number.isSafeInteger(receipt.index) || receipt.index < 1 || typeof receipt.withdrawn !== "boolean") {
|
|
1045
|
+
throw new Error("invalid durable steer receipt");
|
|
1046
|
+
}
|
|
1047
|
+
return Object.freeze(receipt);
|
|
1048
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export const AGENT_MODELS: readonly ["gpt-6.1-sol", "gpt-6-luna", "gpt-6-astra", "@cf/zai-org/glm-5.3", "kimi-k3", "mimo-v2.6-pro"];
|
|
2
|
+
export const AGENT_THINKING: readonly ["none", "low", "medium", "high", "xhigh", "max"];
|
|
3
|
+
export const AGENT_REASONING_MODES: readonly ["standard", "pro"];
|
|
4
|
+
export type ManagedAgentSettings = Readonly<{ model: typeof AGENT_MODELS[number]; thinking: typeof AGENT_THINKING[number]; reasoning_mode: typeof AGENT_REASONING_MODES[number]; fast_mode: boolean }>;
|
|
5
|
+
export type ManagedAgentSettingsPatch = Partial<ManagedAgentSettings>;
|
|
6
|
+
export const DEFAULT_AGENT_SETTINGS: ManagedAgentSettings;
|
|
7
|
+
export function parseAgentSettingsQuery(search: URLSearchParams): ManagedAgentSettings;
|
|
8
|
+
export function agentSettingsQuery(settings: ManagedAgentSettings): URLSearchParams;
|
|
9
|
+
export function parseAgentSettingsPatch(value: unknown): ManagedAgentSettingsPatch;
|
|
10
|
+
export function parseCompleteAgentSettings(value: unknown): ManagedAgentSettings;
|
|
11
|
+
export function validateAgentSettings(settings: ManagedAgentSettings): ManagedAgentSettings;
|
|
12
|
+
export function validateAgentAdmissionSettings(settings: ManagedAgentSettings): ManagedAgentSettings;
|
|
13
|
+
export function isAgentModel(value: unknown): value is ManagedAgentSettings["model"];
|
|
14
|
+
export function isAgentThinking(value: unknown): value is ManagedAgentSettings["thinking"];
|
|
15
|
+
export function isAgentReasoningMode(value: unknown): value is ManagedAgentSettings["reasoning_mode"];
|