@lamplitisles/dsh-nanocodex 0.1.0-beta.0
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/LICENSE +65 -0
- package/NOTICE +2 -0
- package/README.md +182 -0
- package/THIRD_PARTY_NOTICES.md +29 -0
- package/cordis.patch.yml +52 -0
- package/dist/index.d.ts +435 -0
- package/dist/index.js +3405 -0
- package/dist/standard-preset.d.ts +7 -0
- package/dist/standard-preset.js +25 -0
- package/engine-release.json +14 -0
- package/package.json +119 -0
- package/presets/standard/agent.cordis.yml +6 -0
- package/presets/standard/preset.yml +2 -0
- package/vendor/nanocodex/README.md +1344 -0
- package/vendor/nanocodex/actions/events.d.mts +10 -0
- package/vendor/nanocodex/actions/events.mjs +163 -0
- package/vendor/nanocodex/actions/index.d.mts +13 -0
- package/vendor/nanocodex/actions/index.mjs +35 -0
- package/vendor/nanocodex/actions/session.d.mts +66 -0
- package/vendor/nanocodex/actions/session.mjs +59 -0
- package/vendor/nanocodex/actions/turn.d.mts +59 -0
- package/vendor/nanocodex/actions/turn.mjs +37 -0
- package/vendor/nanocodex/actions/voice.d.mts +22 -0
- package/vendor/nanocodex/actions/voice.mjs +50 -0
- package/vendor/nanocodex/browser/Agent.d.mts +55 -0
- package/vendor/nanocodex/browser/Agent.mjs +11 -0
- package/vendor/nanocodex/browser/ChatGptSubscription.d.mts +7 -0
- package/vendor/nanocodex/browser/ChatGptSubscription.mjs +19 -0
- package/vendor/nanocodex/browser/InlineAgent.mjs +266 -0
- package/vendor/nanocodex/browser/Transport.d.mts +74 -0
- package/vendor/nanocodex/browser/Transport.mjs +61 -0
- package/vendor/nanocodex/browser/Voice.d.mts +71 -0
- package/vendor/nanocodex/browser/Voice.mjs +291 -0
- package/vendor/nanocodex/browser/VoiceSession.mjs +688 -0
- package/vendor/nanocodex/browser/WorkerAgent.d.mts +65 -0
- package/vendor/nanocodex/browser/WorkerAgent.mjs +1472 -0
- package/vendor/nanocodex/browser/agent.worker.mjs +3 -0
- package/vendor/nanocodex/browser/config.d.mts +36 -0
- package/vendor/nanocodex/browser/config.mjs +439 -0
- package/vendor/nanocodex/browser/engine.mjs +13 -0
- package/vendor/nanocodex/browser/harness.mjs +55 -0
- package/vendor/nanocodex/browser/host.d.mts +83 -0
- package/vendor/nanocodex/browser/host.mjs +555 -0
- package/vendor/nanocodex/browser/hostManagedWebSocket.d.mts +14 -0
- package/vendor/nanocodex/browser/hostManagedWebSocket.mjs +110 -0
- package/vendor/nanocodex/browser/index.d.mts +92 -0
- package/vendor/nanocodex/browser/index.mjs +23 -0
- package/vendor/nanocodex/browser/indexeddb-durability-store.mjs +197 -0
- package/vendor/nanocodex/browser/workspace.d.mts +16 -0
- package/vendor/nanocodex/browser/workspace.mjs +107 -0
- package/vendor/nanocodex/cloud/Client.d.mts +83 -0
- package/vendor/nanocodex/cloud/Client.mjs +231 -0
- package/vendor/nanocodex/cloud/Decorator.d.mts +31 -0
- package/vendor/nanocodex/cloud/Decorator.mjs +37 -0
- package/vendor/nanocodex/cloud/Dialog.d.mts +113 -0
- package/vendor/nanocodex/cloud/Dialog.mjs +366 -0
- package/vendor/nanocodex/cloud/Errors.d.mts +22 -0
- package/vendor/nanocodex/cloud/Errors.mjs +39 -0
- package/vendor/nanocodex/cloud/Principal.d.mts +26 -0
- package/vendor/nanocodex/cloud/Principal.mjs +88 -0
- package/vendor/nanocodex/cloud/RemoteProvider.mjs +88 -0
- package/vendor/nanocodex/cloud/Transport.d.mts +47 -0
- package/vendor/nanocodex/cloud/Transport.mjs +454 -0
- package/vendor/nanocodex/cloud/actions/account.d.mts +9 -0
- package/vendor/nanocodex/cloud/actions/account.mjs +19 -0
- package/vendor/nanocodex/cloud/actions/agent.d.mts +18 -0
- package/vendor/nanocodex/cloud/actions/agent.mjs +428 -0
- package/vendor/nanocodex/cloud/actions/connection.d.mts +133 -0
- package/vendor/nanocodex/cloud/actions/connection.mjs +580 -0
- package/vendor/nanocodex/cloud/actions/grant.d.mts +10 -0
- package/vendor/nanocodex/cloud/actions/grant.mjs +12 -0
- package/vendor/nanocodex/cloud/actions/index.d.mts +7 -0
- package/vendor/nanocodex/cloud/actions/index.mjs +7 -0
- package/vendor/nanocodex/cloud/actions/machineUsd.d.mts +23 -0
- package/vendor/nanocodex/cloud/actions/machineUsd.mjs +37 -0
- package/vendor/nanocodex/cloud/actions/model.d.mts +12 -0
- package/vendor/nanocodex/cloud/actions/model.mjs +50 -0
- package/vendor/nanocodex/cloud/actions/mpp.d.mts +28 -0
- package/vendor/nanocodex/cloud/actions/mpp.mjs +39 -0
- package/vendor/nanocodex/cloud/index.d.mts +30 -0
- package/vendor/nanocodex/cloud/index.mjs +9 -0
- package/vendor/nanocodex/cloud/internal.mjs +533 -0
- package/vendor/nanocodex/cloud/mercator.mjs +108 -0
- package/vendor/nanocodex/cloud/server/HostPrincipal.d.mts +34 -0
- package/vendor/nanocodex/cloud/server/HostPrincipal.mjs +202 -0
- package/vendor/nanocodex/cloud/server/index.d.mts +1 -0
- package/vendor/nanocodex/cloud/server/index.mjs +1 -0
- package/vendor/nanocodex/cloud/types.d.mts +211 -0
- package/vendor/nanocodex/cloudflare/Agent.d.mts +133 -0
- package/vendor/nanocodex/cloudflare/Agent.mjs +811 -0
- package/vendor/nanocodex/cloudflare/egress-subject.mjs +19 -0
- package/vendor/nanocodex/cloudflare/egress.d.mts +41 -0
- package/vendor/nanocodex/cloudflare/egress.mjs +119 -0
- package/vendor/nanocodex/cloudflare/event-socket.mjs +316 -0
- package/vendor/nanocodex/cloudflare/index.d.mts +7 -0
- package/vendor/nanocodex/cloudflare/index.mjs +6 -0
- package/vendor/nanocodex/host/Agent.d.mts +55 -0
- package/vendor/nanocodex/host/Agent.mjs +1 -0
- package/vendor/nanocodex/host/index.d.mts +82 -0
- package/vendor/nanocodex/host/index.mjs +10 -0
- package/vendor/nanocodex/index.d.mts +81 -0
- package/vendor/nanocodex/index.mjs +12 -0
- package/vendor/nanocodex/internal.mjs +1275 -0
- package/vendor/nanocodex/managed/Agent.d.mts +380 -0
- package/vendor/nanocodex/managed/Agent.mjs +1694 -0
- package/vendor/nanocodex/managed/ManagedError.d.mts +9 -0
- package/vendor/nanocodex/managed/ManagedError.mjs +8 -0
- package/vendor/nanocodex/managed/README.md +241 -0
- package/vendor/nanocodex/managed/Voice.mjs +202 -0
- package/vendor/nanocodex/managed/index.d.mts +20 -0
- package/vendor/nanocodex/managed/index.mjs +2 -0
- package/vendor/nanocodex/managed/internal.mjs +119 -0
- package/vendor/nanocodex/node/Agent.d.mts +50 -0
- package/vendor/nanocodex/node/Agent.mjs +203 -0
- package/vendor/nanocodex/node/ChatGptSubscription.d.mts +9 -0
- package/vendor/nanocodex/node/ChatGptSubscription.mjs +17 -0
- package/vendor/nanocodex/node/Transport.d.mts +44 -0
- package/vendor/nanocodex/node/Transport.mjs +46 -0
- package/vendor/nanocodex/node/host.mjs +731 -0
- package/vendor/nanocodex/node/index.d.mts +73 -0
- package/vendor/nanocodex/node/index.mjs +17 -0
- package/vendor/nanocodex/node/workspace.d.mts +10 -0
- package/vendor/nanocodex/node/workspace.mjs +198 -0
- package/vendor/nanocodex/package.json +200 -0
- package/vendor/nanocodex/pkg-node/nanocodex.d.ts +675 -0
- package/vendor/nanocodex/pkg-node/nanocodex.js +2628 -0
- package/vendor/nanocodex/pkg-node/nanocodex_bg.wasm.d.ts +105 -0
- package/vendor/nanocodex/pkg-node/package.json +1 -0
- package/vendor/nanocodex/pkg-web/.nanocodex-bindgen-stamp +6 -0
- package/vendor/nanocodex/pkg-web/nanocodex-build.json +1 -0
- package/vendor/nanocodex/pkg-web/nanocodex.d.ts +805 -0
- package/vendor/nanocodex/pkg-web/nanocodex.js +2714 -0
- package/vendor/nanocodex/pkg-web/nanocodex_bg.js +2615 -0
- package/vendor/nanocodex/pkg-web/nanocodex_bg.wasm +0 -0
- package/vendor/nanocodex/pkg-web/nanocodex_bg.wasm.d.ts +105 -0
- package/vendor/nanocodex/pkg-web/nanocodex_worker.js +9 -0
- package/vendor/nanocodex/pkg-web/package.json +1 -0
- package/vendor/nanocodex/runtime/chatgpt-subscription.mjs +212 -0
- package/vendor/nanocodex/runtime/cloudflare-durability-store.d.mts +26 -0
- package/vendor/nanocodex/runtime/cloudflare-durability-store.mjs +168 -0
- package/vendor/nanocodex/runtime/code-evaluator.worker.mjs +137 -0
- package/vendor/nanocodex/runtime/code-runtime.mjs +1 -0
- package/vendor/nanocodex/runtime/durability-store.d.mts +85 -0
- package/vendor/nanocodex/runtime/durability-store.mjs +631 -0
- package/vendor/nanocodex/runtime/durability.mjs +153 -0
- package/vendor/nanocodex/runtime/managed-transport.mjs +460 -0
- package/vendor/nanocodex/runtime/mcp-runtime.mjs +534 -0
- package/vendor/nanocodex/runtime/postgres-durability-store.d.mts +64 -0
- package/vendor/nanocodex/runtime/postgres-durability-store.mjs +532 -0
- package/vendor/nanocodex/runtime/quickjs-evaluator.d.mts +17 -0
- package/vendor/nanocodex/runtime/quickjs-evaluator.mjs +236 -0
- package/vendor/nanocodex/runtime/responses-transport.mjs +22 -0
- package/vendor/nanocodex/runtime/subagents.d.mts +107 -0
- package/vendor/nanocodex/runtime/subagents.mjs +54 -0
- package/vendor/nanocodex/runtime/subscription-store.d.mts +8 -0
- package/vendor/nanocodex/runtime/subscription-store.mjs +42 -0
- package/vendor/nanocodex/runtime/tempo-provider.d.mts +125 -0
- package/vendor/nanocodex/runtime/tempo-provider.mjs +264 -0
- package/vendor/nanocodex/runtime/tool-configuration.mjs +1 -0
- package/vendor/nanocodex/runtime/tool-router.mjs +1 -0
- package/vendor/nanocodex/runtime/utf8.mjs +1 -0
- package/vendor/nanocodex/runtime/worker-evaluator.mjs +162 -0
- package/vendor/nanocodex/runtime/workspace.d.mts +1 -0
- package/vendor/nanocodex/runtime/workspace.mjs +1 -0
- package/vendor/nanocodex/tools/Tools.d.mts +56 -0
- package/vendor/nanocodex/tools/Tools.mjs +136 -0
- package/vendor/nanocodex/tools/artifact.d.mts +1 -0
- package/vendor/nanocodex/tools/artifact.mjs +1 -0
- package/vendor/nanocodex/tools/attachment.mjs +1 -0
- package/vendor/nanocodex/tools/bash.d.mts +1 -0
- package/vendor/nanocodex/tools/bash.mjs +1 -0
- package/vendor/nanocodex/tools/browser/accountInfo.mjs +795 -0
- package/vendor/nanocodex/tools/browser/browserBuffer.mjs +18 -0
- package/vendor/nanocodex/tools/browser/browserCompiler.mjs +155 -0
- package/vendor/nanocodex/tools/browser/browserEgress.mjs +161 -0
- package/vendor/nanocodex/tools/browser/browserPython.mjs +133 -0
- package/vendor/nanocodex/tools/browser/browserShell.mjs +1075 -0
- package/vendor/nanocodex/tools/browser/browserSprintf.mjs +4 -0
- package/vendor/nanocodex/tools/browser/browserSsh.mjs +201 -0
- package/vendor/nanocodex/tools/browser/browserZlib.mjs +136 -0
- package/vendor/nanocodex/tools/browser/compiler.worker.mjs +160 -0
- package/vendor/nanocodex/tools/browser/devTunnelsSshBrowser.mjs +13 -0
- package/vendor/nanocodex/tools/browser/index.d.mts +209 -0
- package/vendor/nanocodex/tools/browser/index.mjs +174 -0
- package/vendor/nanocodex/tools/browser/opfsGit.mjs +248 -0
- package/vendor/nanocodex/tools/browser/python.worker.mjs +114 -0
- package/vendor/nanocodex/tools/browser/threadGit.mjs +197 -0
- package/vendor/nanocodex/tools/browser/unsupportedNodeRsa.mjs +5 -0
- package/vendor/nanocodex/tools/browser/workspace.mjs +83 -0
- package/vendor/nanocodex/tools/dataset.d.mts +1 -0
- package/vendor/nanocodex/tools/dataset.mjs +1 -0
- package/vendor/nanocodex/tools/datasetContract.mjs +1 -0
- package/vendor/nanocodex/tools/datasetEngine.mjs +1 -0
- package/vendor/nanocodex/tools/hostedCatalog.d.mts +1 -0
- package/vendor/nanocodex/tools/hostedCatalog.mjs +1 -0
- package/vendor/nanocodex/tools/index.d.mts +49 -0
- package/vendor/nanocodex/tools/index.mjs +19 -0
- package/vendor/nanocodex/tools/namedTool.mjs +1 -0
- package/vendor/nanocodex/tools/repository-workspace.d.mts +1 -0
- package/vendor/nanocodex/tools/repository-workspace.mjs +1 -0
- package/vendor/nanocodex/tools/ssh.d.mts +1 -0
- package/vendor/nanocodex/tools/ssh.mjs +1 -0
- package/vendor/nanocodex/tools/standard.mjs +1 -0
- package/vendor/nanocodex/tools/standardDescriptions.mjs +1 -0
- package/vendor/nanocodex/types.d.mts +663 -0
- package/vendor/nanocodex/wasm.d.mts +4 -0
- package/vendor/nanocodex/worker/ChatGptSubscription.d.mts +9 -0
- package/vendor/nanocodex/worker/ChatGptSubscription.mjs +29 -0
- package/vendor/nanocodex/worker/index.d.mts +18 -0
- package/vendor/nanocodex/worker/index.mjs +5 -0
- package/vendor/nanocodex-tools/README.md +65 -0
- package/vendor/nanocodex-tools/dist/hosted/app-tool-catalog.d.ts +7 -0
- package/vendor/nanocodex-tools/dist/hosted/app-tool-catalog.js +14 -0
- package/vendor/nanocodex-tools/dist/hosted/broker-core.d.ts +172 -0
- package/vendor/nanocodex-tools/dist/hosted/broker-core.js +1278 -0
- package/vendor/nanocodex-tools/dist/hosted/index.d.ts +3 -0
- package/vendor/nanocodex-tools/dist/hosted/index.js +3 -0
- package/vendor/nanocodex-tools/dist/hosted/protocol.d.ts +120 -0
- package/vendor/nanocodex-tools/dist/hosted/protocol.js +415 -0
- package/vendor/nanocodex-tools/dist/index.d.ts +19 -0
- package/vendor/nanocodex-tools/dist/index.js +14 -0
- package/vendor/nanocodex-tools/dist/memory.d.ts +204 -0
- package/vendor/nanocodex-tools/dist/memory.js +411 -0
- package/vendor/nanocodex-tools/dist/namespace.d.ts +87 -0
- package/vendor/nanocodex-tools/dist/namespace.js +338 -0
- package/vendor/nanocodex-tools/dist/repository-archive.d.ts +4 -0
- package/vendor/nanocodex-tools/dist/repository-archive.js +104 -0
- package/vendor/nanocodex-tools/dist/runtime.d.ts +30 -0
- package/vendor/nanocodex-tools/dist/runtime.js +37 -0
- package/vendor/nanocodex-tools/dist/session.d.ts +167 -0
- package/vendor/nanocodex-tools/dist/session.js +286 -0
- package/vendor/nanocodex-tools/dist/shell.d.ts +51 -0
- package/vendor/nanocodex-tools/dist/shell.js +553 -0
- package/vendor/nanocodex-tools/dist/ssh.d.ts +29 -0
- package/vendor/nanocodex-tools/dist/ssh.js +66 -0
- package/vendor/nanocodex-tools/dist/workspace.d.ts +45 -0
- package/vendor/nanocodex-tools/dist/workspace.js +237 -0
- package/vendor/nanocodex-tools/package.json +122 -0
- package/vendor/nanocodex-tools/runtime/code-runtime.mjs +817 -0
- package/vendor/nanocodex-tools/runtime/tool-configuration.mjs +50 -0
- package/vendor/nanocodex-tools/runtime/tool-router.mjs +730 -0
- package/vendor/nanocodex-tools/runtime/utf8.d.mts +2 -0
- package/vendor/nanocodex-tools/runtime/utf8.mjs +25 -0
- package/vendor/nanocodex-tools/tools/artifact.d.mts +69 -0
- package/vendor/nanocodex-tools/tools/artifact.mjs +270 -0
- package/vendor/nanocodex-tools/tools/attachment.mjs +603 -0
- package/vendor/nanocodex-tools/tools/bash.d.mts +90 -0
- package/vendor/nanocodex-tools/tools/bash.mjs +742 -0
- package/vendor/nanocodex-tools/tools/dataset.d.mts +9 -0
- package/vendor/nanocodex-tools/tools/dataset.mjs +47 -0
- package/vendor/nanocodex-tools/tools/datasetContract.mjs +54 -0
- package/vendor/nanocodex-tools/tools/datasetEngine.mjs +1167 -0
- package/vendor/nanocodex-tools/tools/execution-contract.d.mts +5 -0
- package/vendor/nanocodex-tools/tools/execution-contract.mjs +63 -0
- package/vendor/nanocodex-tools/tools/hostedCatalog.d.mts +20 -0
- package/vendor/nanocodex-tools/tools/hostedCatalog.mjs +67 -0
- package/vendor/nanocodex-tools/tools/hostedMachine.d.mts +10 -0
- package/vendor/nanocodex-tools/tools/hostedMachine.mjs +56 -0
- package/vendor/nanocodex-tools/tools/just-bash-browser.d.mts +1 -0
- package/vendor/nanocodex-tools/tools/just-bash-browser.mjs +1 -0
- package/vendor/nanocodex-tools/tools/namedTool.d.mts +3 -0
- package/vendor/nanocodex-tools/tools/namedTool.mjs +3 -0
- package/vendor/nanocodex-tools/tools/nodeProcess.d.mts +10 -0
- package/vendor/nanocodex-tools/tools/nodeProcess.mjs +314 -0
- package/vendor/nanocodex-tools/tools/processOutput.mjs +56 -0
- package/vendor/nanocodex-tools/tools/repository-workspace.d.mts +55 -0
- package/vendor/nanocodex-tools/tools/repository-workspace.mjs +397 -0
- package/vendor/nanocodex-tools/tools/ssh.d.mts +56 -0
- package/vendor/nanocodex-tools/tools/ssh.mjs +503 -0
- package/vendor/nanocodex-tools/tools/standard.mjs +458 -0
- package/vendor/nanocodex-tools/tools/standardDescriptions.mjs +3 -0
- package/vendor/nanocodex-tools/tools/types.d.mts +83 -0
- package/vendor/nanocodex-tools/tools/workspace.d.mts +25 -0
- package/vendor/nanocodex-tools/tools/workspace.mjs +282 -0
- package/vendor/nanocodex-tools/tools/x.d.mts +30 -0
- package/vendor/nanocodex-tools/tools/x.mjs +91 -0
|
@@ -0,0 +1,1694 @@
|
|
|
1
|
+
import { ManagedError } from "./ManagedError.mjs";
|
|
2
|
+
import { registerManagedAgent } from "./internal.mjs";
|
|
3
|
+
|
|
4
|
+
const API_KEY = /^ncx_live_[A-Za-z0-9_-]{12}_[A-Za-z0-9_-]{43}$/;
|
|
5
|
+
const CURSOR = /^(?:0|[1-9][0-9]*)$/;
|
|
6
|
+
const LATEST_CURSOR = "latest";
|
|
7
|
+
const IDEMPOTENCY_KEY = /^[\x21-\x7e]{1,256}$/;
|
|
8
|
+
const SESSION_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
9
|
+
const TURN_ID = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
10
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
11
|
+
const UTF8 = new TextEncoder();
|
|
12
|
+
const TERMINAL_TYPES = new Set([
|
|
13
|
+
"turn_completed",
|
|
14
|
+
"turn_cancelled",
|
|
15
|
+
"turn_failed",
|
|
16
|
+
]);
|
|
17
|
+
const TERMINAL_CACHE_CAPACITY = 256;
|
|
18
|
+
const TERMINAL_CACHE_BYTES = 8 * 1024 * 1024;
|
|
19
|
+
const SUBSCRIBER_QUEUE_CAPACITY = 4_096;
|
|
20
|
+
const SUBSCRIBER_QUEUE_BYTES = 32 * 1024 * 1024;
|
|
21
|
+
// Managed logical events are bounded to 14 MiB; retain envelope allowance for
|
|
22
|
+
// cursor, turn, SSE, and JSON framing on the client boundary.
|
|
23
|
+
const EVENT_STREAM_FRAME_BYTES = 16 * 1024 * 1024;
|
|
24
|
+
const EVENT_STREAM_INACTIVITY_TIMEOUT_MS = 45_000;
|
|
25
|
+
const TURN_SUBMISSION_TIMEOUT_MS = 10_000;
|
|
26
|
+
const TURN_STATE_POLL_INITIAL_MS = 1_000;
|
|
27
|
+
const TURN_STATE_POLL_MAX_MS = 5_000;
|
|
28
|
+
const TURN_STATE_READ_TIMEOUT_MS = 2_000;
|
|
29
|
+
const ALLOWED_OPTIONS = new Set(["apiKey", "baseUrl", "fetch", "toolsTransport"]);
|
|
30
|
+
const CREATE_SETTINGS = new Set(["model", "thinking", "reasoningMode", "fastMode"]);
|
|
31
|
+
const SETTINGS_PATCH = CREATE_SETTINGS;
|
|
32
|
+
const MODELS = new Set(["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-6-astra"]);
|
|
33
|
+
const THINKING = new Set(["none", "low", "medium", "high", "xhigh", "max"]);
|
|
34
|
+
const REASONING_MODES = new Set(["standard", "pro"]);
|
|
35
|
+
const eventEncoder = new TextEncoder();
|
|
36
|
+
|
|
37
|
+
/** Create a new managed agent owned by the authenticated account. */
|
|
38
|
+
export async function create(options = {}) {
|
|
39
|
+
const { clientOptions, requestBody } = managedCreateOptions(options);
|
|
40
|
+
const client = managedClient(clientOptions);
|
|
41
|
+
const idempotencyKey = `managed-create:${globalThis.crypto.randomUUID()}`;
|
|
42
|
+
let receipt;
|
|
43
|
+
let failure;
|
|
44
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
45
|
+
try {
|
|
46
|
+
receipt = await client.json("/v1/agents", {
|
|
47
|
+
method: "POST",
|
|
48
|
+
idempotencyKey,
|
|
49
|
+
...(requestBody === undefined ? {} : { body: requestBody }),
|
|
50
|
+
});
|
|
51
|
+
break;
|
|
52
|
+
} catch (error) {
|
|
53
|
+
failure = error;
|
|
54
|
+
if (!(error instanceof ManagedError)
|
|
55
|
+
|| (error.code !== "network_error"
|
|
56
|
+
&& error.status !== 408
|
|
57
|
+
&& error.status !== 429
|
|
58
|
+
&& !(error.status >= 500))
|
|
59
|
+
|| attempt === 7) {
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
await new Promise((resolve) => setTimeout(
|
|
63
|
+
resolve,
|
|
64
|
+
createRetryDelayMs(attempt),
|
|
65
|
+
));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (!receipt) throw failure;
|
|
69
|
+
return agentHandle(client, requiredString(receipt, "agent_id"));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function managedCreateOptions(options) {
|
|
73
|
+
if (!options || typeof options !== "object" || Array.isArray(options)) {
|
|
74
|
+
throw new TypeError("managed agent options must be an object");
|
|
75
|
+
}
|
|
76
|
+
const { settings, ...clientOptions } = options;
|
|
77
|
+
if (settings === undefined) return { clientOptions, requestBody: undefined };
|
|
78
|
+
const keys = settings && typeof settings === "object" && !Array.isArray(settings)
|
|
79
|
+
? Object.keys(settings)
|
|
80
|
+
: [];
|
|
81
|
+
if (!settings || typeof settings !== "object" || Array.isArray(settings)
|
|
82
|
+
|| keys.length !== 4
|
|
83
|
+
|| keys.some((key) => !CREATE_SETTINGS.has(key))
|
|
84
|
+
|| !MODELS.has(settings.model)
|
|
85
|
+
|| !THINKING.has(settings.thinking)
|
|
86
|
+
|| !REASONING_MODES.has(settings.reasoningMode)
|
|
87
|
+
|| typeof settings.fastMode !== "boolean") {
|
|
88
|
+
throw new TypeError("managed agent creation settings are invalid");
|
|
89
|
+
}
|
|
90
|
+
if (settings.model === "gpt-6-astra" && settings.thinking === "none") {
|
|
91
|
+
throw new TypeError("GPT-6 Astra requires low, medium, high, xhigh, or max thinking");
|
|
92
|
+
}
|
|
93
|
+
if (settings.model === "gpt-6-astra" && settings.reasoningMode === "pro") {
|
|
94
|
+
throw new TypeError("GPT-6 Astra does not support pro reasoning mode");
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
clientOptions,
|
|
98
|
+
requestBody: JSON.stringify({
|
|
99
|
+
settings: {
|
|
100
|
+
model: settings.model,
|
|
101
|
+
thinking: settings.thinking,
|
|
102
|
+
reasoning_mode: settings.reasoningMode,
|
|
103
|
+
fast_mode: settings.fastMode,
|
|
104
|
+
},
|
|
105
|
+
}),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function createRetryDelayMs(attempt) {
|
|
110
|
+
const ceiling = Math.min(2_000, 250 * 2 ** attempt);
|
|
111
|
+
return Math.floor(Math.random() * (ceiling + 1));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** List handles for every managed agent owned by the authenticated account. */
|
|
115
|
+
export async function list(options = {}) {
|
|
116
|
+
const client = managedClient(options);
|
|
117
|
+
const body = await client.json("/v1/agents");
|
|
118
|
+
if (!body || !Array.isArray(body.data) || body.data.some((id) => typeof id !== "string")) {
|
|
119
|
+
throw new ManagedError("invalid_response", "managed agent list is malformed");
|
|
120
|
+
}
|
|
121
|
+
const summaries = body.summaries === undefined ? {} : body.summaries;
|
|
122
|
+
if (!summaries || typeof summaries !== "object" || Array.isArray(summaries)) {
|
|
123
|
+
throw new ManagedError("invalid_response", "managed agent summaries are malformed");
|
|
124
|
+
}
|
|
125
|
+
return Object.freeze(body.data.map((id) => agentHandle(
|
|
126
|
+
client,
|
|
127
|
+
id,
|
|
128
|
+
Object.hasOwn(summaries, id) ? managedSummary(summaries[id]) : undefined,
|
|
129
|
+
)));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Resolve one owned managed agent and verify that it exists. */
|
|
133
|
+
export async function get(id, options = {}) {
|
|
134
|
+
validateAgentId(id);
|
|
135
|
+
const client = managedClient(options);
|
|
136
|
+
await client.json(agentPath(id));
|
|
137
|
+
return agentHandle(client, id);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Open a managed agent handle without probing retained state first. */
|
|
141
|
+
export function open(id, options = {}) {
|
|
142
|
+
validateAgentId(id);
|
|
143
|
+
return agentHandle(managedClient(options), id);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Delete one owned managed agent and all of its retained state. */
|
|
147
|
+
export async function remove(id, options = {}) {
|
|
148
|
+
validateAgentId(id);
|
|
149
|
+
const client = managedClient(options);
|
|
150
|
+
await client.empty(agentPath(id), { method: "DELETE" });
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export { remove as delete };
|
|
154
|
+
|
|
155
|
+
/** Find candidate completed sessions owned by the authenticated account. */
|
|
156
|
+
export async function findSessions(request, options = {}) {
|
|
157
|
+
validateFindSessionsRequest(request);
|
|
158
|
+
const body = await managedClient(options).json("/v1/history/sessions/search", {
|
|
159
|
+
method: "POST",
|
|
160
|
+
body: JSON.stringify(request),
|
|
161
|
+
});
|
|
162
|
+
return managedFindSessionsResponse(body);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Read exact projected turns from one completed account session. */
|
|
166
|
+
export async function readSession(request, options = {}) {
|
|
167
|
+
validateReadSessionRequest(request);
|
|
168
|
+
const body = await managedClient(options).json(
|
|
169
|
+
`/v1/history/sessions/${encodeURIComponent(request.session_id)}/read`,
|
|
170
|
+
{
|
|
171
|
+
method: "POST",
|
|
172
|
+
body: JSON.stringify(request.turn_ids === undefined ? {} : { turn_ids: request.turn_ids }),
|
|
173
|
+
},
|
|
174
|
+
);
|
|
175
|
+
return managedReadSessionResponse(body);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** List the authenticated account's hosted durable memory. */
|
|
179
|
+
export async function listMemories(options = {}) {
|
|
180
|
+
const body = await managedClient(options).json("/v1/memory");
|
|
181
|
+
if (!body || typeof body !== "object" || Array.isArray(body) || !Array.isArray(body.memories)) {
|
|
182
|
+
throw new ManagedError("invalid_response", "managed memory list is malformed");
|
|
183
|
+
}
|
|
184
|
+
return Object.freeze(body.memories.map(managedMemoryRecord));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Compare-and-swap delete one account-owned hosted memory. */
|
|
188
|
+
export async function deleteMemory(key, options = {}) {
|
|
189
|
+
validateMemoryKey(key);
|
|
190
|
+
await managedClient(options).empty(
|
|
191
|
+
`/v1/memory/${key.id}?version=${key.version}`,
|
|
192
|
+
{ method: "DELETE" },
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Run one atomic durable-memory operation in the authenticated account scope. */
|
|
197
|
+
export async function memory(operation, options = {}) {
|
|
198
|
+
validateMemoryOperation(operation);
|
|
199
|
+
const body = await managedClient(options).json("/v1/memory", {
|
|
200
|
+
method: "POST",
|
|
201
|
+
body: JSON.stringify(operation),
|
|
202
|
+
});
|
|
203
|
+
return managedMemoryResponse(body, operation.operation);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Read metadata for the authenticated account's organization. */
|
|
207
|
+
export async function getOrganization(options = {}) {
|
|
208
|
+
return managedOrganization(await managedClient(options).json("/v1/organization"));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Update metadata for the authenticated account's organization. */
|
|
212
|
+
export async function updateOrganization(request, options = {}) {
|
|
213
|
+
validateOrganizationUpdate(request);
|
|
214
|
+
const body = await managedClient(options).json("/v1/organization", {
|
|
215
|
+
method: "PATCH",
|
|
216
|
+
body: JSON.stringify(request),
|
|
217
|
+
});
|
|
218
|
+
return managedOrganization(body);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function agentHandle(client, id, summary) {
|
|
222
|
+
validateAgentId(id);
|
|
223
|
+
const eventStream = replayableEventStream(client, id);
|
|
224
|
+
const events = Object.freeze({
|
|
225
|
+
page: (options = {}) => eventHistoryPage(client, id, options),
|
|
226
|
+
watch: (options = {}) => eventStream.subscribe(options),
|
|
227
|
+
});
|
|
228
|
+
const agent = Object.freeze({
|
|
229
|
+
type: "managed",
|
|
230
|
+
id,
|
|
231
|
+
...(summary === undefined ? {} : { summary }),
|
|
232
|
+
events,
|
|
233
|
+
turn: Object.freeze({
|
|
234
|
+
prompt: (options) => managedTurn(client, id, eventStream, options),
|
|
235
|
+
}),
|
|
236
|
+
settings: Object.freeze({
|
|
237
|
+
read: async () => managedSettings((await client.json(agentPath(id))).settings),
|
|
238
|
+
update: async (patch) => managedSettings((await client.json(`${agentPath(id)}/settings`, {
|
|
239
|
+
method: "PATCH",
|
|
240
|
+
body: managedSettingsPatch(patch),
|
|
241
|
+
})).settings),
|
|
242
|
+
}),
|
|
243
|
+
triggers: Object.freeze({
|
|
244
|
+
list: async () => {
|
|
245
|
+
const body = await client.json(`${agentPath(id)}/triggers`);
|
|
246
|
+
if (!body || !Array.isArray(body.data)) throw new ManagedError("invalid_response", "managed triggers are malformed");
|
|
247
|
+
return Object.freeze(body.data.map(managedCronTrigger));
|
|
248
|
+
},
|
|
249
|
+
get: async (triggerId) => managedCronTrigger(await client.json(cronTriggerPath(id, triggerId))),
|
|
250
|
+
put: async (triggerId, config) => managedCronTrigger(await client.json(cronTriggerPath(id, triggerId), {
|
|
251
|
+
method: "PUT", body: cronTriggerBody(config),
|
|
252
|
+
})),
|
|
253
|
+
delete: async (triggerId) => { await client.empty(cronTriggerPath(id, triggerId), { method: "DELETE" }); },
|
|
254
|
+
}),
|
|
255
|
+
toolsTarget: () => client.toolsTarget(id),
|
|
256
|
+
state: () => client.json(agentPath(id)),
|
|
257
|
+
delete: async () => {
|
|
258
|
+
await client.empty(agentPath(id), { method: "DELETE" });
|
|
259
|
+
eventStream.close();
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
registerManagedAgent(agent, client, id, {
|
|
263
|
+
voiceTransport: managedVoiceTransport(client, id),
|
|
264
|
+
});
|
|
265
|
+
return agent;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function cronTriggerPath(agentId, triggerId) {
|
|
269
|
+
if (typeof triggerId !== "string" || !/^[A-Za-z0-9_-]{1,64}$/.test(triggerId)) {
|
|
270
|
+
throw new TypeError("trigger id must be 1-64 letters, digits, underscores or hyphens");
|
|
271
|
+
}
|
|
272
|
+
return `${agentPath(agentId)}/triggers/${triggerId}`;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function cronTriggerBody(config) {
|
|
276
|
+
if (!config || typeof config !== "object" || Array.isArray(config)
|
|
277
|
+
|| Object.keys(config).some((key) => !["cron", "timezone", "input", "enabled", "session_mode"].includes(key))
|
|
278
|
+
|| typeof config.cron !== "string" || config.cron.length > 256
|
|
279
|
+
|| config.cron.trim().split(/\s+/).length !== 5
|
|
280
|
+
|| typeof config.input !== "string" || config.input.trim().length === 0
|
|
281
|
+
|| UTF8.encode(config.input).byteLength > 64 * 1024
|
|
282
|
+
|| (config.session_mode !== undefined && config.session_mode !== "new" && config.session_mode !== "continue")
|
|
283
|
+
|| (config.timezone !== undefined && typeof config.timezone !== "string")
|
|
284
|
+
|| (config.enabled !== undefined && typeof config.enabled !== "boolean")) {
|
|
285
|
+
throw new TypeError("invalid cron trigger configuration");
|
|
286
|
+
}
|
|
287
|
+
return JSON.stringify(config);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function managedCronTrigger(value) {
|
|
291
|
+
if (!value || typeof value !== "object" || Array.isArray(value)
|
|
292
|
+
|| typeof value.id !== "string" || !/^[A-Za-z0-9_-]{1,64}$/.test(value.id)
|
|
293
|
+
|| typeof value.cron !== "string" || typeof value.timezone !== "string"
|
|
294
|
+
|| typeof value.input !== "string" || typeof value.enabled !== "boolean"
|
|
295
|
+
|| (value.session_mode !== undefined && value.session_mode !== "new" && value.session_mode !== "continue")
|
|
296
|
+
|| (value.last_agent_id != null && (typeof value.last_agent_id !== "string" || !UUID.test(value.last_agent_id)))
|
|
297
|
+
|| ![value.created_at, value.updated_at].every((n) => Number.isSafeInteger(n) && n >= 0)
|
|
298
|
+
|| ![value.next_run_at, value.last_run_at, value.last_skipped_at].every((n) => n === null || (Number.isSafeInteger(n) && n >= 0))
|
|
299
|
+
|| (value.last_turn_id !== null && (typeof value.last_turn_id !== "string" || !TURN_ID.test(value.last_turn_id)))) {
|
|
300
|
+
throw new ManagedError("invalid_response", "managed cron trigger is malformed");
|
|
301
|
+
}
|
|
302
|
+
return Object.freeze({
|
|
303
|
+
id: value.id, cron: value.cron, timezone: value.timezone, input: value.input, enabled: value.enabled,
|
|
304
|
+
session_mode: value.session_mode ?? "continue", last_agent_id: value.last_agent_id ?? null,
|
|
305
|
+
next_run_at: value.next_run_at, last_run_at: value.last_run_at, last_turn_id: value.last_turn_id,
|
|
306
|
+
last_skipped_at: value.last_skipped_at, created_at: value.created_at, updated_at: value.updated_at,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function managedSettingsPatch(patch) {
|
|
311
|
+
const keys = patch && typeof patch === "object" && !Array.isArray(patch)
|
|
312
|
+
? Object.keys(patch)
|
|
313
|
+
: [];
|
|
314
|
+
if (!patch || typeof patch !== "object" || Array.isArray(patch)
|
|
315
|
+
|| keys.length === 0 || keys.some((key) => !SETTINGS_PATCH.has(key))
|
|
316
|
+
|| (Object.hasOwn(patch, "model") && !MODELS.has(patch.model))
|
|
317
|
+
|| (Object.hasOwn(patch, "thinking") && !THINKING.has(patch.thinking))
|
|
318
|
+
|| (Object.hasOwn(patch, "reasoningMode") && !REASONING_MODES.has(patch.reasoningMode))
|
|
319
|
+
|| (Object.hasOwn(patch, "fastMode") && typeof patch.fastMode !== "boolean")) {
|
|
320
|
+
throw new TypeError("managed agent settings patch is invalid");
|
|
321
|
+
}
|
|
322
|
+
if (patch.model === "gpt-6-astra" && patch.thinking === "none") {
|
|
323
|
+
throw new TypeError("GPT-6 Astra requires low, medium, high, xhigh, or max thinking");
|
|
324
|
+
}
|
|
325
|
+
if (patch.model === "gpt-6-astra" && patch.reasoningMode === "pro") {
|
|
326
|
+
throw new TypeError("GPT-6 Astra does not support pro reasoning mode");
|
|
327
|
+
}
|
|
328
|
+
return JSON.stringify({
|
|
329
|
+
...(Object.hasOwn(patch, "model") ? { model: patch.model } : {}),
|
|
330
|
+
...(Object.hasOwn(patch, "thinking") ? { thinking: patch.thinking } : {}),
|
|
331
|
+
...(Object.hasOwn(patch, "reasoningMode") ? { reasoning_mode: patch.reasoningMode } : {}),
|
|
332
|
+
...(Object.hasOwn(patch, "fastMode") ? { fast_mode: patch.fastMode } : {}),
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function managedSettings(value) {
|
|
337
|
+
if (!value || typeof value !== "object" || Array.isArray(value)
|
|
338
|
+
|| !MODELS.has(value.model) || !THINKING.has(value.thinking)
|
|
339
|
+
|| !REASONING_MODES.has(value.reasoning_mode) || typeof value.fast_mode !== "boolean"
|
|
340
|
+
|| (value.model === "gpt-6-astra" && value.thinking === "none")
|
|
341
|
+
|| (value.model === "gpt-6-astra" && value.reasoning_mode === "pro")) {
|
|
342
|
+
throw new ManagedError("invalid_response", "managed agent settings are malformed");
|
|
343
|
+
}
|
|
344
|
+
return Object.freeze({
|
|
345
|
+
model: value.model,
|
|
346
|
+
thinking: value.thinking,
|
|
347
|
+
reasoningMode: value.reasoning_mode,
|
|
348
|
+
fastMode: value.fast_mode,
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function managedVoiceTransport(client, agentId) {
|
|
353
|
+
const origin = new URL(client.baseUrl).origin;
|
|
354
|
+
const realtimePath = `${agentPath(agentId)}/realtime`;
|
|
355
|
+
let voiceSessionId;
|
|
356
|
+
return Object.freeze({
|
|
357
|
+
origin,
|
|
358
|
+
sameOrigin: true,
|
|
359
|
+
call(body, signal) {
|
|
360
|
+
let envelope;
|
|
361
|
+
try { envelope = JSON.parse(body); }
|
|
362
|
+
catch { throw new TypeError("managed voice call body is invalid"); }
|
|
363
|
+
if (!envelope || typeof envelope !== "object" || Array.isArray(envelope)
|
|
364
|
+
|| envelope.managed_agent_id !== agentId
|
|
365
|
+
|| typeof envelope.call_body !== "string"
|
|
366
|
+
|| typeof envelope.realtime_session_id !== "string") {
|
|
367
|
+
throw new TypeError("managed voice call body is invalid");
|
|
368
|
+
}
|
|
369
|
+
voiceSessionId = envelope.realtime_session_id;
|
|
370
|
+
return client.response(`${realtimePath}/calls`, {
|
|
371
|
+
method: "POST",
|
|
372
|
+
voiceSessionId,
|
|
373
|
+
body: envelope.call_body,
|
|
374
|
+
signal,
|
|
375
|
+
});
|
|
376
|
+
},
|
|
377
|
+
sidebandUrl(callId) {
|
|
378
|
+
if (!voiceSessionId) throw new Error("managed voice call must open before its sideband");
|
|
379
|
+
const url = new URL(`${realtimePath}/sideband`, client.baseUrl);
|
|
380
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
381
|
+
url.searchParams.set("call_id", callId);
|
|
382
|
+
url.searchParams.set("voice_session_id", voiceSessionId);
|
|
383
|
+
return url;
|
|
384
|
+
},
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function managedSummary(value) {
|
|
389
|
+
if (!value || typeof value !== "object" || Array.isArray(value)
|
|
390
|
+
|| typeof value.title !== "string"
|
|
391
|
+
|| !nonnegativeNumber(value.created_at)
|
|
392
|
+
|| !nonnegativeNumber(value.updated_at)
|
|
393
|
+
|| !Number.isSafeInteger(value.turn_count) || value.turn_count < 0) {
|
|
394
|
+
throw new ManagedError("invalid_response", "managed agent summary is malformed");
|
|
395
|
+
}
|
|
396
|
+
return Object.freeze({
|
|
397
|
+
title: value.title,
|
|
398
|
+
createdAt: value.created_at,
|
|
399
|
+
updatedAt: value.updated_at,
|
|
400
|
+
turnCount: value.turn_count,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function nonnegativeNumber(value) {
|
|
405
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
async function eventHistoryPage(client, agentId, options) {
|
|
409
|
+
if (!options || typeof options !== "object" || Array.isArray(options)) {
|
|
410
|
+
throw new TypeError("managed event history options must be an object");
|
|
411
|
+
}
|
|
412
|
+
const before = options.before;
|
|
413
|
+
if (before !== undefined && (typeof before !== "string" || !CURSOR.test(before) || before === "0")) {
|
|
414
|
+
throw new TypeError("managed event history cursor must be a positive decimal string");
|
|
415
|
+
}
|
|
416
|
+
const limit = options.limit ?? 128;
|
|
417
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 256) {
|
|
418
|
+
throw new TypeError("managed event history limit must be an integer from 1 through 256");
|
|
419
|
+
}
|
|
420
|
+
const query = new URLSearchParams({ limit: String(limit) });
|
|
421
|
+
if (before !== undefined) query.set("before", before);
|
|
422
|
+
const body = await client.json(`${agentPath(agentId)}/events/history?${query}`, {
|
|
423
|
+
signal: options.signal,
|
|
424
|
+
});
|
|
425
|
+
if (!body || !Array.isArray(body.data) || typeof body.has_more !== "boolean") {
|
|
426
|
+
throw new ManagedError("invalid_response", "managed event history is malformed");
|
|
427
|
+
}
|
|
428
|
+
const latestCursor = requiredCursor(body, "latest_cursor");
|
|
429
|
+
const data = body.data.map((event) => managedEvent(event));
|
|
430
|
+
if (data.length > limit || data.some((event, index) =>
|
|
431
|
+
(index > 0 && !cursorBefore(data[index - 1].cursor, event.cursor))
|
|
432
|
+
|| (before !== undefined && !cursorBefore(event.cursor, before)))) {
|
|
433
|
+
throw new ManagedError("invalid_response", "managed event history ordering is malformed");
|
|
434
|
+
}
|
|
435
|
+
return Object.freeze({ data: Object.freeze(data), hasMore: body.has_more, latestCursor });
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function managedTurn(client, agentId, eventStream, options) {
|
|
439
|
+
if (!options || typeof options !== "object" || Array.isArray(options)) {
|
|
440
|
+
throw new TypeError("managed prompt options must be an object");
|
|
441
|
+
}
|
|
442
|
+
const { id, input, signal } = options;
|
|
443
|
+
if (id !== undefined && (typeof id !== "string" || !TURN_ID.test(id))) {
|
|
444
|
+
throw new TypeError("managed turn id must be 1-128 safe ASCII characters");
|
|
445
|
+
}
|
|
446
|
+
const idempotencyKey = options.idempotencyKey ?? generatedIdempotencyKey();
|
|
447
|
+
if (typeof idempotencyKey !== "string" || !IDEMPOTENCY_KEY.test(idempotencyKey)) {
|
|
448
|
+
throw new TypeError("managed idempotency key must be 1-256 visible ASCII characters");
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const submission = retrySubmission(client, agentId, {
|
|
452
|
+
id,
|
|
453
|
+
idempotencyKey,
|
|
454
|
+
input,
|
|
455
|
+
signal,
|
|
456
|
+
});
|
|
457
|
+
void submission.catch(() => {});
|
|
458
|
+
let result;
|
|
459
|
+
let steeringTail = Promise.resolve();
|
|
460
|
+
const turn = {
|
|
461
|
+
idempotencyKey,
|
|
462
|
+
accepted: async () => requiredString(await submission, "turn_id"),
|
|
463
|
+
state: async () => {
|
|
464
|
+
const accepted = await submission;
|
|
465
|
+
return client.json(turnPath(agentId, requiredString(accepted, "turn_id")), { signal });
|
|
466
|
+
},
|
|
467
|
+
steer: async ({ input }) => {
|
|
468
|
+
const body = JSON.stringify({ input });
|
|
469
|
+
// Preserve this turn's correction order across concurrent HTTP requests.
|
|
470
|
+
// Cancellation deliberately bypasses this queue.
|
|
471
|
+
const steering = steeringTail.then(async () => {
|
|
472
|
+
const accepted = await submission;
|
|
473
|
+
return client.json(`${turnPath(agentId, requiredString(accepted, "turn_id"))}/steer`, {
|
|
474
|
+
method: "POST",
|
|
475
|
+
body,
|
|
476
|
+
signal,
|
|
477
|
+
});
|
|
478
|
+
});
|
|
479
|
+
steeringTail = steering.then(() => {}, () => {});
|
|
480
|
+
return steering;
|
|
481
|
+
},
|
|
482
|
+
cancel: async () => {
|
|
483
|
+
const turnId = id ?? requiredString(await submission, "turn_id");
|
|
484
|
+
return client.json(`${turnPath(agentId, turnId)}/cancel`, {
|
|
485
|
+
method: "POST",
|
|
486
|
+
});
|
|
487
|
+
},
|
|
488
|
+
result: (options = {}) => {
|
|
489
|
+
if (!options || typeof options !== "object" || Array.isArray(options)) {
|
|
490
|
+
throw new TypeError("managed turn result options must be an object");
|
|
491
|
+
}
|
|
492
|
+
const observerSignal = options.signal;
|
|
493
|
+
if (observerSignal !== undefined && !(observerSignal instanceof AbortSignal)) {
|
|
494
|
+
throw new TypeError("managed turn result signal must be an AbortSignal");
|
|
495
|
+
}
|
|
496
|
+
return observerSignal === undefined
|
|
497
|
+
? result ??= waitForResult(client, agentId, eventStream, submission, signal)
|
|
498
|
+
: waitForResult(client, agentId, eventStream, submission, observerSignal);
|
|
499
|
+
},
|
|
500
|
+
};
|
|
501
|
+
return Object.freeze(turn);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
async function retrySubmission(client, agentId, options) {
|
|
505
|
+
const body = JSON.stringify({
|
|
506
|
+
...(options.id === undefined ? {} : { id: options.id }),
|
|
507
|
+
input: options.input,
|
|
508
|
+
});
|
|
509
|
+
let failure;
|
|
510
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
511
|
+
try {
|
|
512
|
+
return await boundedSubmission(client, `${agentPath(agentId)}/turns`, {
|
|
513
|
+
method: "POST",
|
|
514
|
+
body,
|
|
515
|
+
idempotencyKey: options.idempotencyKey,
|
|
516
|
+
}, options.signal);
|
|
517
|
+
} catch (error) {
|
|
518
|
+
if (options.signal?.aborted
|
|
519
|
+
|| (error instanceof ManagedError && error.code !== "network_error")) throw error;
|
|
520
|
+
failure = error;
|
|
521
|
+
if (attempt < 2) await delay(150 * (attempt + 1), options.signal);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
throw failure;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
async function boundedSubmission(client, path, init, signal) {
|
|
528
|
+
const attempt = new AbortController();
|
|
529
|
+
let rejectBoundary;
|
|
530
|
+
const boundary = new Promise((_, reject) => { rejectBoundary = reject; });
|
|
531
|
+
const interrupt = (reason) => {
|
|
532
|
+
if (attempt.signal.aborted) return;
|
|
533
|
+
attempt.abort(reason);
|
|
534
|
+
rejectBoundary(reason);
|
|
535
|
+
};
|
|
536
|
+
const aborted = () => interrupt(abortError(signal.reason));
|
|
537
|
+
if (signal?.aborted) aborted();
|
|
538
|
+
else signal?.addEventListener("abort", aborted, { once: true });
|
|
539
|
+
const timeout = setTimeout(() => interrupt(new ManagedError(
|
|
540
|
+
"network_error",
|
|
541
|
+
"Managed agent submission was not acknowledged. Retrying the same durable turn.",
|
|
542
|
+
)), TURN_SUBMISSION_TIMEOUT_MS);
|
|
543
|
+
timeout.unref?.();
|
|
544
|
+
const request = client.json(path, { ...init, signal: attempt.signal });
|
|
545
|
+
void request.catch(() => {});
|
|
546
|
+
try {
|
|
547
|
+
return await Promise.race([request, boundary]);
|
|
548
|
+
} finally {
|
|
549
|
+
clearTimeout(timeout);
|
|
550
|
+
signal?.removeEventListener("abort", aborted);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
async function waitForResult(client, agentId, eventStream, submission, signal) {
|
|
555
|
+
const accepted = await observePromise(submission, signal);
|
|
556
|
+
const turnId = requiredString(accepted, "turn_id");
|
|
557
|
+
if (accepted.terminal) return terminalResult(turnId, accepted.terminal, accepted.terminal_cursor);
|
|
558
|
+
const cursor = requiredCursor(accepted, "accepted_cursor");
|
|
559
|
+
const events = eventStream.subscribe({ cursor, signal });
|
|
560
|
+
const pollController = new AbortController();
|
|
561
|
+
const abortPolling = () => pollController.abort(signal?.reason);
|
|
562
|
+
if (signal?.aborted) abortPolling();
|
|
563
|
+
else signal?.addEventListener("abort", abortPolling, { once: true });
|
|
564
|
+
try {
|
|
565
|
+
const retained = eventStream.terminal(turnId, cursor);
|
|
566
|
+
if (retained) return terminalResult(turnId, retained.data, retained.cursor);
|
|
567
|
+
let eventObservation = observedEvent(events);
|
|
568
|
+
let statePollMs = TURN_STATE_POLL_INITIAL_MS;
|
|
569
|
+
let stateObservation = observedTurnState(
|
|
570
|
+
client,
|
|
571
|
+
agentId,
|
|
572
|
+
turnId,
|
|
573
|
+
statePollMs,
|
|
574
|
+
pollController.signal,
|
|
575
|
+
);
|
|
576
|
+
while (true) {
|
|
577
|
+
const observation = await Promise.race([eventObservation, stateObservation]);
|
|
578
|
+
if (observation.kind === "state") {
|
|
579
|
+
if (observation.value?.terminal) {
|
|
580
|
+
return terminalResult(
|
|
581
|
+
turnId,
|
|
582
|
+
observation.value.terminal,
|
|
583
|
+
observation.value.terminal_cursor,
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
// A failed read says nothing about the durable turn. Retry it at the
|
|
587
|
+
// initial cadence; only a successful nonterminal observation backs off.
|
|
588
|
+
statePollMs = observation.value
|
|
589
|
+
? Math.min(statePollMs * 2, TURN_STATE_POLL_MAX_MS)
|
|
590
|
+
: TURN_STATE_POLL_INITIAL_MS;
|
|
591
|
+
stateObservation = observedTurnState(
|
|
592
|
+
client,
|
|
593
|
+
agentId,
|
|
594
|
+
turnId,
|
|
595
|
+
statePollMs,
|
|
596
|
+
pollController.signal,
|
|
597
|
+
);
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
600
|
+
if (observation.kind === "state_abort") throw observation.error;
|
|
601
|
+
if (observation.kind === "event_error") throw observation.error;
|
|
602
|
+
if (observation.value.done) break;
|
|
603
|
+
eventObservation = observedEvent(events);
|
|
604
|
+
const event = observation.value.value;
|
|
605
|
+
const data = event.data;
|
|
606
|
+
if (data.type === "stream_failed") {
|
|
607
|
+
throw new ManagedError("stream_failed", stringOr(data.error, "managed event stream failed"));
|
|
608
|
+
}
|
|
609
|
+
if (data.turn_id !== turnId && data.id !== turnId) continue;
|
|
610
|
+
if (TERMINAL_TYPES.has(data.type)) return terminalResult(turnId, data, event.cursor);
|
|
611
|
+
}
|
|
612
|
+
} finally {
|
|
613
|
+
signal?.removeEventListener("abort", abortPolling);
|
|
614
|
+
pollController.abort();
|
|
615
|
+
await events.return();
|
|
616
|
+
}
|
|
617
|
+
if (signal?.aborted) throw abortError(signal.reason);
|
|
618
|
+
throw new ManagedError("event_stream_ended", "managed event stream ended before the turn completed");
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function observedEvent(events) {
|
|
622
|
+
return events.next().then(
|
|
623
|
+
(value) => ({ kind: "event", value }),
|
|
624
|
+
(error) => ({ kind: "event_error", error }),
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
async function observedTurnState(client, agentId, turnId, milliseconds, signal) {
|
|
629
|
+
await delay(milliseconds, signal);
|
|
630
|
+
const attempt = new AbortController();
|
|
631
|
+
let rejectBoundary;
|
|
632
|
+
const boundary = new Promise((_, reject) => { rejectBoundary = reject; });
|
|
633
|
+
const interrupt = (reason) => {
|
|
634
|
+
if (attempt.signal.aborted) return;
|
|
635
|
+
attempt.abort(reason);
|
|
636
|
+
rejectBoundary(reason);
|
|
637
|
+
};
|
|
638
|
+
const aborted = () => interrupt(abortError(signal.reason));
|
|
639
|
+
if (signal.aborted) aborted();
|
|
640
|
+
else signal.addEventListener("abort", aborted, { once: true });
|
|
641
|
+
const timeout = setTimeout(() => interrupt(new ManagedError(
|
|
642
|
+
"network_error",
|
|
643
|
+
"Managed turn state did not settle. Retrying the authoritative read.",
|
|
644
|
+
)), TURN_STATE_READ_TIMEOUT_MS);
|
|
645
|
+
timeout.unref?.();
|
|
646
|
+
const request = client.json(turnPath(agentId, turnId), { signal: attempt.signal });
|
|
647
|
+
void request.catch(() => {});
|
|
648
|
+
try {
|
|
649
|
+
const value = await Promise.race([request, boundary]);
|
|
650
|
+
return { kind: "state", value };
|
|
651
|
+
} catch (error) {
|
|
652
|
+
if (signal.aborted) return { kind: "state_abort", error: abortError(signal.reason) };
|
|
653
|
+
// The cursor stream remains the primary low-latency path. State reads are
|
|
654
|
+
// an authoritative recovery path and must not make a healthy stream fail.
|
|
655
|
+
return { kind: "state", value: undefined };
|
|
656
|
+
} finally {
|
|
657
|
+
clearTimeout(timeout);
|
|
658
|
+
signal.removeEventListener("abort", aborted);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function observePromise(promise, signal) {
|
|
663
|
+
if (!signal) return promise;
|
|
664
|
+
if (signal.aborted) return Promise.reject(abortError(signal.reason));
|
|
665
|
+
return new Promise((resolve, reject) => {
|
|
666
|
+
const aborted = () => {
|
|
667
|
+
signal.removeEventListener("abort", aborted);
|
|
668
|
+
reject(abortError(signal.reason));
|
|
669
|
+
};
|
|
670
|
+
signal.addEventListener("abort", aborted, { once: true });
|
|
671
|
+
Promise.resolve(promise).then(
|
|
672
|
+
(value) => {
|
|
673
|
+
signal.removeEventListener("abort", aborted);
|
|
674
|
+
resolve(value);
|
|
675
|
+
},
|
|
676
|
+
(error) => {
|
|
677
|
+
signal.removeEventListener("abort", aborted);
|
|
678
|
+
reject(error);
|
|
679
|
+
},
|
|
680
|
+
);
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function terminalResult(turnId, terminal, cursor) {
|
|
685
|
+
if (!terminal || typeof terminal !== "object") {
|
|
686
|
+
throw new ManagedError("invalid_response", "managed terminal turn is malformed");
|
|
687
|
+
}
|
|
688
|
+
if (terminal.type === "turn_completed") {
|
|
689
|
+
if (typeof terminal.final_message !== "string") {
|
|
690
|
+
throw new ManagedError("invalid_response", "managed completed turn has no final message");
|
|
691
|
+
}
|
|
692
|
+
return Object.freeze({
|
|
693
|
+
turnId,
|
|
694
|
+
finalMessage: terminal.final_message,
|
|
695
|
+
usage: terminal.usage ?? null,
|
|
696
|
+
citations: managedCitations(terminal.citations ?? []),
|
|
697
|
+
...(typeof terminal.usage_error === "string" ? { usageError: terminal.usage_error } : {}),
|
|
698
|
+
...(typeof cursor === "string" ? { cursor } : {}),
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
const code = typeof terminal.type === "string" ? terminal.type : "turn_failed";
|
|
702
|
+
const message = stringOr(terminal.error, `managed ${code.replaceAll("_", " ")}`);
|
|
703
|
+
throw new ManagedError(code, message);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function managedFindSessionsResponse(value) {
|
|
707
|
+
if (!value || typeof value !== "object" || Array.isArray(value)
|
|
708
|
+
|| typeof value.query !== "string") {
|
|
709
|
+
throw new ManagedError("invalid_response", "managed find sessions response is malformed");
|
|
710
|
+
}
|
|
711
|
+
return Object.freeze({
|
|
712
|
+
query: value.query,
|
|
713
|
+
results: managedSessionSearchHits(value.results),
|
|
714
|
+
citations: managedCitations(value.citations),
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function managedSessionSearchHits(value) {
|
|
719
|
+
if (!Array.isArray(value)) {
|
|
720
|
+
throw new ManagedError("invalid_response", "managed history results are malformed");
|
|
721
|
+
}
|
|
722
|
+
return Object.freeze(value.map((result) => {
|
|
723
|
+
if (!result || typeof result !== "object" || Array.isArray(result)
|
|
724
|
+
|| typeof result.session_id !== "string"
|
|
725
|
+
|| typeof result.title !== "string"
|
|
726
|
+
|| typeof result.turn_id !== "string"
|
|
727
|
+
|| typeof result.cursor !== "string" || !CURSOR.test(result.cursor)
|
|
728
|
+
|| typeof result.score !== "number" || !Number.isFinite(result.score)
|
|
729
|
+
|| typeof result.snippet !== "string") {
|
|
730
|
+
throw new ManagedError("invalid_response", "managed history search result is malformed");
|
|
731
|
+
}
|
|
732
|
+
return Object.freeze({
|
|
733
|
+
session_id: result.session_id,
|
|
734
|
+
title: result.title,
|
|
735
|
+
turn_id: result.turn_id,
|
|
736
|
+
cursor: result.cursor,
|
|
737
|
+
score: result.score,
|
|
738
|
+
snippet: result.snippet,
|
|
739
|
+
});
|
|
740
|
+
}));
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function managedReadSessionResponse(value) {
|
|
744
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || !Array.isArray(value.turns)) {
|
|
745
|
+
throw new ManagedError("invalid_response", "managed read session response is malformed");
|
|
746
|
+
}
|
|
747
|
+
const turns = value.turns.map((turn) => {
|
|
748
|
+
if (!turn || typeof turn !== "object" || Array.isArray(turn)
|
|
749
|
+
|| typeof turn.session_id !== "string"
|
|
750
|
+
|| typeof turn.title !== "string"
|
|
751
|
+
|| typeof turn.turn_id !== "string"
|
|
752
|
+
|| typeof turn.cursor !== "string" || !CURSOR.test(turn.cursor)
|
|
753
|
+
|| typeof turn.user !== "string"
|
|
754
|
+
|| typeof turn.assistant !== "string") {
|
|
755
|
+
throw new ManagedError("invalid_response", "managed session turn is malformed");
|
|
756
|
+
}
|
|
757
|
+
return Object.freeze({
|
|
758
|
+
session_id: turn.session_id,
|
|
759
|
+
title: turn.title,
|
|
760
|
+
turn_id: turn.turn_id,
|
|
761
|
+
cursor: turn.cursor,
|
|
762
|
+
user: turn.user,
|
|
763
|
+
assistant: turn.assistant,
|
|
764
|
+
});
|
|
765
|
+
});
|
|
766
|
+
return Object.freeze({
|
|
767
|
+
turns: Object.freeze(turns),
|
|
768
|
+
citations: managedCitations(value.citations),
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
function managedCitations(value) {
|
|
773
|
+
if (!Array.isArray(value)) {
|
|
774
|
+
throw new ManagedError("invalid_response", "managed citations are malformed");
|
|
775
|
+
}
|
|
776
|
+
return Object.freeze(value.map((citation) => {
|
|
777
|
+
if (!citation || typeof citation !== "object" || Array.isArray(citation)
|
|
778
|
+
|| typeof citation.thread_id !== "string"
|
|
779
|
+
|| typeof citation.title !== "string"
|
|
780
|
+
|| !Array.isArray(citation.sources)) {
|
|
781
|
+
throw new ManagedError("invalid_response", "managed citation is malformed");
|
|
782
|
+
}
|
|
783
|
+
const sources = citation.sources.map((source) => {
|
|
784
|
+
if (!source || typeof source !== "object" || Array.isArray(source)
|
|
785
|
+
|| typeof source.turn_id !== "string"
|
|
786
|
+
|| typeof source.cursor !== "string" || !CURSOR.test(source.cursor)) {
|
|
787
|
+
throw new ManagedError("invalid_response", "managed citation source is malformed");
|
|
788
|
+
}
|
|
789
|
+
return Object.freeze({ turn_id: source.turn_id, cursor: source.cursor });
|
|
790
|
+
});
|
|
791
|
+
return Object.freeze({
|
|
792
|
+
thread_id: citation.thread_id,
|
|
793
|
+
title: citation.title,
|
|
794
|
+
sources: Object.freeze(sources),
|
|
795
|
+
});
|
|
796
|
+
}));
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
function managedMemoryResponse(value, expectedOperation) {
|
|
800
|
+
if (!value || typeof value !== "object" || Array.isArray(value)
|
|
801
|
+
|| value.operation !== expectedOperation) {
|
|
802
|
+
throw new ManagedError("invalid_response", "managed memory response is malformed");
|
|
803
|
+
}
|
|
804
|
+
if (value.operation === "scan") {
|
|
805
|
+
if (typeof value.abstained !== "boolean" || !Array.isArray(value.candidates)
|
|
806
|
+
|| value.candidates.length > 5
|
|
807
|
+
|| value.abstained !== (value.candidates.length === 0)) {
|
|
808
|
+
throw new ManagedError("invalid_response", "managed memory scan response is malformed");
|
|
809
|
+
}
|
|
810
|
+
return Object.freeze({
|
|
811
|
+
operation: "scan",
|
|
812
|
+
abstained: value.abstained,
|
|
813
|
+
candidates: Object.freeze(value.candidates.map(managedMemoryCandidate)),
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
if (value.operation === "read") {
|
|
817
|
+
if (!Array.isArray(value.memories)) {
|
|
818
|
+
throw new ManagedError("invalid_response", "managed memory read response is malformed");
|
|
819
|
+
}
|
|
820
|
+
return Object.freeze({
|
|
821
|
+
operation: "read",
|
|
822
|
+
memories: Object.freeze(value.memories.map(managedMemoryRecord)),
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
if (value.operation === "put") {
|
|
826
|
+
if (typeof value.replaced !== "boolean") {
|
|
827
|
+
throw new ManagedError("invalid_response", "managed memory put response is malformed");
|
|
828
|
+
}
|
|
829
|
+
return Object.freeze({
|
|
830
|
+
operation: "put",
|
|
831
|
+
memory: managedMemoryRecord(value.memory),
|
|
832
|
+
replaced: value.replaced,
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
if (value.operation === "delete") {
|
|
836
|
+
return Object.freeze({ operation: "delete", key: managedMemoryKey(value.key) });
|
|
837
|
+
}
|
|
838
|
+
throw new ManagedError("invalid_response", "managed memory response is malformed");
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
function managedMemoryCandidate(value) {
|
|
842
|
+
if (!value || typeof value !== "object" || Array.isArray(value)
|
|
843
|
+
|| typeof value.preview !== "string"
|
|
844
|
+
|| UTF8.encode(value.preview).byteLength > 64
|
|
845
|
+
|| typeof value.score !== "number" || !Number.isFinite(value.score) || value.score <= 0) {
|
|
846
|
+
throw new ManagedError("invalid_response", "managed memory candidate is malformed");
|
|
847
|
+
}
|
|
848
|
+
return Object.freeze({
|
|
849
|
+
key: managedMemoryKey(value.key),
|
|
850
|
+
preview: value.preview,
|
|
851
|
+
score: value.score,
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
function managedMemoryRecord(value) {
|
|
856
|
+
if (!value || typeof value !== "object" || Array.isArray(value)
|
|
857
|
+
|| typeof value.content !== "string" || !value.content.trim()
|
|
858
|
+
|| UTF8.encode(value.content).byteLength > 1_024
|
|
859
|
+
|| !nonnegativeSafeInteger(value.created_at_ms)
|
|
860
|
+
|| !nonnegativeSafeInteger(value.updated_at_ms)
|
|
861
|
+
|| !nullableNonnegativeSafeInteger(value.last_scanned_at_ms)
|
|
862
|
+
|| !nonnegativeSafeInteger(value.scan_count)
|
|
863
|
+
|| !nullableNonnegativeSafeInteger(value.last_used_at_ms)
|
|
864
|
+
|| !nonnegativeSafeInteger(value.use_count)
|
|
865
|
+
|| !nullableNonnegativeSafeInteger(value.probation_until_ms)) {
|
|
866
|
+
throw new ManagedError("invalid_response", "managed memory record is malformed");
|
|
867
|
+
}
|
|
868
|
+
return Object.freeze({
|
|
869
|
+
key: managedMemoryKey(value.key),
|
|
870
|
+
content: value.content,
|
|
871
|
+
created_at_ms: value.created_at_ms,
|
|
872
|
+
updated_at_ms: value.updated_at_ms,
|
|
873
|
+
last_scanned_at_ms: value.last_scanned_at_ms,
|
|
874
|
+
scan_count: value.scan_count,
|
|
875
|
+
last_used_at_ms: value.last_used_at_ms,
|
|
876
|
+
use_count: value.use_count,
|
|
877
|
+
probation_until_ms: value.probation_until_ms,
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
function managedMemoryKey(value) {
|
|
882
|
+
if (!value || typeof value !== "object" || Array.isArray(value)
|
|
883
|
+
|| !positiveSafeInteger(value.id) || !positiveSafeInteger(value.version)) {
|
|
884
|
+
throw new ManagedError("invalid_response", "managed memory key is malformed");
|
|
885
|
+
}
|
|
886
|
+
return Object.freeze({ id: value.id, version: value.version });
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
function managedOrganization(value) {
|
|
890
|
+
if (!value || typeof value !== "object" || Array.isArray(value)
|
|
891
|
+
|| typeof value.id !== "string" || !UUID.test(value.id)
|
|
892
|
+
|| !managedOrganizationName(value.name)
|
|
893
|
+
|| !value.rootTeam || typeof value.rootTeam !== "object" || Array.isArray(value.rootTeam)
|
|
894
|
+
|| typeof value.rootTeam.id !== "string" || !UUID.test(value.rootTeam.id)
|
|
895
|
+
|| !managedOrganizationName(value.rootTeam.name)
|
|
896
|
+
|| !positiveSafeInteger(value.authorizationEpoch)
|
|
897
|
+
|| !nonnegativeSafeInteger(value.createdAt)
|
|
898
|
+
|| !nonnegativeSafeInteger(value.updatedAt)) {
|
|
899
|
+
throw new ManagedError("invalid_response", "managed organization response is malformed");
|
|
900
|
+
}
|
|
901
|
+
return Object.freeze({
|
|
902
|
+
id: value.id,
|
|
903
|
+
name: value.name,
|
|
904
|
+
rootTeam: Object.freeze({ id: value.rootTeam.id, name: value.rootTeam.name }),
|
|
905
|
+
authorizationEpoch: value.authorizationEpoch,
|
|
906
|
+
createdAt: value.createdAt,
|
|
907
|
+
updatedAt: value.updatedAt,
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
function managedOrganizationName(value) {
|
|
912
|
+
return value === null
|
|
913
|
+
|| (typeof value === "string" && value === value.trim() && value.length <= 120);
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
function replayableEventStream(client, agentId) {
|
|
917
|
+
const subscribers = new Set();
|
|
918
|
+
const joiningSubscribers = new Set();
|
|
919
|
+
const terminals = new Map();
|
|
920
|
+
let terminalBytes = 0;
|
|
921
|
+
let connection;
|
|
922
|
+
let nextGeneration = 0;
|
|
923
|
+
let closed = false;
|
|
924
|
+
let stopOnlineReconnect;
|
|
925
|
+
|
|
926
|
+
const retireConnection = (restart = true) => {
|
|
927
|
+
const retired = connection;
|
|
928
|
+
if (!retired) return;
|
|
929
|
+
connection = undefined;
|
|
930
|
+
retired.controller.abort();
|
|
931
|
+
if (restart) start();
|
|
932
|
+
};
|
|
933
|
+
|
|
934
|
+
const watchOnlineReconnect = () => {
|
|
935
|
+
if (stopOnlineReconnect || typeof globalThis.addEventListener !== "function") return;
|
|
936
|
+
const reconnect = () => retireConnection();
|
|
937
|
+
globalThis.addEventListener("online", reconnect);
|
|
938
|
+
stopOnlineReconnect = () => {
|
|
939
|
+
globalThis.removeEventListener?.("online", reconnect);
|
|
940
|
+
stopOnlineReconnect = undefined;
|
|
941
|
+
};
|
|
942
|
+
};
|
|
943
|
+
|
|
944
|
+
const remove = (subscriber) => {
|
|
945
|
+
const removed = subscribers.delete(subscriber) || joiningSubscribers.delete(subscriber);
|
|
946
|
+
if (!removed) return;
|
|
947
|
+
subscriber.boundaryController?.abort();
|
|
948
|
+
subscriber.signal?.removeEventListener("abort", subscriber.onAbort);
|
|
949
|
+
if (subscribers.size === 0 && joiningSubscribers.size === 0) {
|
|
950
|
+
stopOnlineReconnect?.();
|
|
951
|
+
retireConnection(false);
|
|
952
|
+
}
|
|
953
|
+
};
|
|
954
|
+
|
|
955
|
+
const finish = (subscriber, error) => {
|
|
956
|
+
if (subscriber.done) return;
|
|
957
|
+
subscriber.done = true;
|
|
958
|
+
subscriber.error = error;
|
|
959
|
+
if (subscriber.pending) {
|
|
960
|
+
const pending = subscriber.pending;
|
|
961
|
+
subscriber.pending = undefined;
|
|
962
|
+
if (error) pending.reject(error);
|
|
963
|
+
else pending.resolve({ value: undefined, done: true });
|
|
964
|
+
}
|
|
965
|
+
};
|
|
966
|
+
|
|
967
|
+
const unsubscribe = (subscriber) => {
|
|
968
|
+
finish(subscriber);
|
|
969
|
+
remove(subscriber);
|
|
970
|
+
subscriber.queue.length = 0;
|
|
971
|
+
subscriber.bufferedBytes = 0;
|
|
972
|
+
return Promise.resolve({ value: undefined, done: true });
|
|
973
|
+
};
|
|
974
|
+
|
|
975
|
+
const attach = (subscriber) => {
|
|
976
|
+
if (subscriber.done || closed) return;
|
|
977
|
+
joiningSubscribers.delete(subscriber);
|
|
978
|
+
subscriber.boundaryController = undefined;
|
|
979
|
+
subscribers.add(subscriber);
|
|
980
|
+
watchOnlineReconnect();
|
|
981
|
+
if (
|
|
982
|
+
connection?.cursor !== undefined
|
|
983
|
+
&& (
|
|
984
|
+
(connection.startCursor === LATEST_CURSOR && subscriber.cursor !== LATEST_CURSOR)
|
|
985
|
+
|| cursorBefore(subscriber.cursor, connection.cursor)
|
|
986
|
+
)
|
|
987
|
+
) {
|
|
988
|
+
retireConnection();
|
|
989
|
+
} else {
|
|
990
|
+
start();
|
|
991
|
+
}
|
|
992
|
+
};
|
|
993
|
+
|
|
994
|
+
const start = () => {
|
|
995
|
+
if (closed || connection || subscribers.size === 0) return;
|
|
996
|
+
const cursor = [...subscribers].reduce(
|
|
997
|
+
(lowest, subscriber) => cursorBefore(subscriber.cursor, lowest) ? subscriber.cursor : lowest,
|
|
998
|
+
[...subscribers][0].cursor,
|
|
999
|
+
);
|
|
1000
|
+
const controller = new AbortController();
|
|
1001
|
+
const current = {
|
|
1002
|
+
controller,
|
|
1003
|
+
cursor,
|
|
1004
|
+
startCursor: cursor,
|
|
1005
|
+
generation: nextGeneration += 1,
|
|
1006
|
+
running: undefined,
|
|
1007
|
+
};
|
|
1008
|
+
connection = current;
|
|
1009
|
+
const running = (async () => {
|
|
1010
|
+
try {
|
|
1011
|
+
for await (const event of readEvents(
|
|
1012
|
+
client,
|
|
1013
|
+
agentId,
|
|
1014
|
+
cursor,
|
|
1015
|
+
controller.signal,
|
|
1016
|
+
(controlCursor) => {
|
|
1017
|
+
if (connection !== current) return;
|
|
1018
|
+
current.cursor = controlCursor;
|
|
1019
|
+
for (const subscriber of subscribers) {
|
|
1020
|
+
if (subscriber.cursor !== LATEST_CURSOR) continue;
|
|
1021
|
+
subscriber.cursor = controlCursor;
|
|
1022
|
+
subscriber.deliveredCursor = controlCursor;
|
|
1023
|
+
}
|
|
1024
|
+
},
|
|
1025
|
+
)) {
|
|
1026
|
+
if (connection !== current) return;
|
|
1027
|
+
current.cursor = event.cursor;
|
|
1028
|
+
const turnId = event.data.turn_id ?? event.data.id;
|
|
1029
|
+
if (typeof turnId === "string" && TERMINAL_TYPES.has(event.data.type)) {
|
|
1030
|
+
const retained = terminals.get(turnId);
|
|
1031
|
+
if (retained) {
|
|
1032
|
+
if (sameTerminal(retained.event.data, event.data)) continue;
|
|
1033
|
+
throw new ManagedError(
|
|
1034
|
+
"conflicting_terminal",
|
|
1035
|
+
`managed turn ${turnId} published conflicting terminal events`,
|
|
1036
|
+
);
|
|
1037
|
+
}
|
|
1038
|
+
const bytes = encodedBytes(event);
|
|
1039
|
+
if (bytes <= TERMINAL_CACHE_BYTES) {
|
|
1040
|
+
terminals.set(turnId, { bytes, event });
|
|
1041
|
+
terminalBytes += bytes;
|
|
1042
|
+
while (
|
|
1043
|
+
terminals.size > TERMINAL_CACHE_CAPACITY
|
|
1044
|
+
|| terminalBytes > TERMINAL_CACHE_BYTES
|
|
1045
|
+
) {
|
|
1046
|
+
const oldestTurnId = terminals.keys().next().value;
|
|
1047
|
+
const oldest = terminals.get(oldestTurnId);
|
|
1048
|
+
terminals.delete(oldestTurnId);
|
|
1049
|
+
terminalBytes -= oldest.bytes;
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
for (const subscriber of subscribers) {
|
|
1054
|
+
if (!eventAfter(subscriber.cursor, event.cursor)) continue;
|
|
1055
|
+
subscriber.cursor = event.cursor;
|
|
1056
|
+
if (subscriber.pending) {
|
|
1057
|
+
const pending = subscriber.pending;
|
|
1058
|
+
subscriber.pending = undefined;
|
|
1059
|
+
subscriber.deliveredCursor = event.cursor;
|
|
1060
|
+
pending.resolve({ value: event, done: false });
|
|
1061
|
+
} else {
|
|
1062
|
+
const bytes = encodedBytes(event.data);
|
|
1063
|
+
if (
|
|
1064
|
+
subscriber.queue.length >= SUBSCRIBER_QUEUE_CAPACITY
|
|
1065
|
+
|| subscriber.bufferedBytes + bytes > SUBSCRIBER_QUEUE_BYTES
|
|
1066
|
+
) {
|
|
1067
|
+
subscriber.overflowed = true;
|
|
1068
|
+
subscriber.done = true;
|
|
1069
|
+
remove(subscriber);
|
|
1070
|
+
continue;
|
|
1071
|
+
}
|
|
1072
|
+
subscriber.queue.push({ bytes, event });
|
|
1073
|
+
subscriber.bufferedBytes += bytes;
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
} catch (error) {
|
|
1078
|
+
if (!controller.signal.aborted) {
|
|
1079
|
+
for (const subscriber of subscribers) finish(subscriber, error);
|
|
1080
|
+
for (const subscriber of [...subscribers]) remove(subscriber);
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
})();
|
|
1084
|
+
current.running = running;
|
|
1085
|
+
void running.finally(() => {
|
|
1086
|
+
if (connection !== current) return;
|
|
1087
|
+
connection = undefined;
|
|
1088
|
+
start();
|
|
1089
|
+
});
|
|
1090
|
+
};
|
|
1091
|
+
|
|
1092
|
+
const subscribe = (options = {}) => {
|
|
1093
|
+
if (!options || typeof options !== "object" || Array.isArray(options)) {
|
|
1094
|
+
throw new TypeError("managed event options must be an object");
|
|
1095
|
+
}
|
|
1096
|
+
const cursor = options.cursor ?? "0";
|
|
1097
|
+
if (typeof cursor !== "string" || (cursor !== LATEST_CURSOR && !CURSOR.test(cursor))) {
|
|
1098
|
+
throw new TypeError("managed event cursor must be an unsigned decimal string or latest");
|
|
1099
|
+
}
|
|
1100
|
+
if (closed) throw new ManagedError("agent_closed", "managed agent event stream is closed");
|
|
1101
|
+
|
|
1102
|
+
const subscriber = {
|
|
1103
|
+
cursor,
|
|
1104
|
+
deliveredCursor: cursor,
|
|
1105
|
+
queue: [],
|
|
1106
|
+
bufferedBytes: 0,
|
|
1107
|
+
pending: undefined,
|
|
1108
|
+
done: false,
|
|
1109
|
+
error: undefined,
|
|
1110
|
+
overflowed: false,
|
|
1111
|
+
overflowReported: false,
|
|
1112
|
+
boundaryController: undefined,
|
|
1113
|
+
signal: options.signal,
|
|
1114
|
+
onAbort: undefined,
|
|
1115
|
+
};
|
|
1116
|
+
const iterator = {
|
|
1117
|
+
next() {
|
|
1118
|
+
if (subscriber.queue.length > 0) {
|
|
1119
|
+
const entry = subscriber.queue.shift();
|
|
1120
|
+
subscriber.bufferedBytes -= entry.bytes;
|
|
1121
|
+
subscriber.deliveredCursor = entry.event.cursor;
|
|
1122
|
+
return Promise.resolve({ value: entry.event, done: false });
|
|
1123
|
+
}
|
|
1124
|
+
if (subscriber.error) return Promise.reject(subscriber.error);
|
|
1125
|
+
if (subscriber.overflowed && !subscriber.overflowReported) {
|
|
1126
|
+
subscriber.overflowReported = true;
|
|
1127
|
+
return Promise.reject(new ManagedError(
|
|
1128
|
+
"event_backlog_exceeded",
|
|
1129
|
+
`managed event iterator exceeded its private buffer of ${SUBSCRIBER_QUEUE_CAPACITY} events or `
|
|
1130
|
+
+ `${SUBSCRIBER_QUEUE_BYTES} encoded bytes; reconnect with events.watch({ cursor: `
|
|
1131
|
+
+ `"${subscriber.deliveredCursor}" })`,
|
|
1132
|
+
));
|
|
1133
|
+
}
|
|
1134
|
+
if (subscriber.done) return Promise.resolve({ value: undefined, done: true });
|
|
1135
|
+
if (subscriber.pending) {
|
|
1136
|
+
return Promise.reject(new TypeError("managed event iterator already has a pending read"));
|
|
1137
|
+
}
|
|
1138
|
+
return new Promise((resolve, reject) => { subscriber.pending = { resolve, reject }; });
|
|
1139
|
+
},
|
|
1140
|
+
return: () => unsubscribe(subscriber),
|
|
1141
|
+
throw(error) {
|
|
1142
|
+
unsubscribe(subscriber);
|
|
1143
|
+
return Promise.reject(error);
|
|
1144
|
+
},
|
|
1145
|
+
[Symbol.asyncIterator]() { return this; },
|
|
1146
|
+
};
|
|
1147
|
+
if (subscriber.signal?.aborted) {
|
|
1148
|
+
subscriber.done = true;
|
|
1149
|
+
return Object.freeze(iterator);
|
|
1150
|
+
}
|
|
1151
|
+
subscriber.onAbort = () => unsubscribe(subscriber);
|
|
1152
|
+
subscriber.signal?.addEventListener("abort", subscriber.onAbort, { once: true });
|
|
1153
|
+
if (cursor === LATEST_CURSOR && (connection !== undefined || subscribers.size > 0)) {
|
|
1154
|
+
subscriber.boundaryController = new AbortController();
|
|
1155
|
+
joiningSubscribers.add(subscriber);
|
|
1156
|
+
watchOnlineReconnect();
|
|
1157
|
+
void resolveLatestCursor(client, agentId, subscriber.boundaryController.signal).then(
|
|
1158
|
+
(boundary) => {
|
|
1159
|
+
subscriber.cursor = boundary;
|
|
1160
|
+
subscriber.deliveredCursor = boundary;
|
|
1161
|
+
attach(subscriber);
|
|
1162
|
+
},
|
|
1163
|
+
(error) => {
|
|
1164
|
+
if (subscriber.done) return;
|
|
1165
|
+
finish(subscriber, error);
|
|
1166
|
+
remove(subscriber);
|
|
1167
|
+
},
|
|
1168
|
+
);
|
|
1169
|
+
} else {
|
|
1170
|
+
attach(subscriber);
|
|
1171
|
+
}
|
|
1172
|
+
return Object.freeze(iterator);
|
|
1173
|
+
};
|
|
1174
|
+
|
|
1175
|
+
return Object.freeze({
|
|
1176
|
+
subscribe,
|
|
1177
|
+
terminal(turnId, afterCursor) {
|
|
1178
|
+
const event = terminals.get(turnId)?.event;
|
|
1179
|
+
return event && cursorBefore(afterCursor, event.cursor) ? event : undefined;
|
|
1180
|
+
},
|
|
1181
|
+
close() {
|
|
1182
|
+
if (closed) return;
|
|
1183
|
+
closed = true;
|
|
1184
|
+
stopOnlineReconnect?.();
|
|
1185
|
+
retireConnection(false);
|
|
1186
|
+
for (const subscriber of subscribers) finish(subscriber);
|
|
1187
|
+
for (const subscriber of [...subscribers]) remove(subscriber);
|
|
1188
|
+
for (const subscriber of joiningSubscribers) finish(subscriber);
|
|
1189
|
+
for (const subscriber of [...joiningSubscribers]) remove(subscriber);
|
|
1190
|
+
terminals.clear();
|
|
1191
|
+
terminalBytes = 0;
|
|
1192
|
+
},
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
async function resolveLatestCursor(client, agentId, signal) {
|
|
1197
|
+
const controller = new AbortController();
|
|
1198
|
+
const aborted = () => controller.abort(signal?.reason);
|
|
1199
|
+
signal?.addEventListener("abort", aborted, { once: true });
|
|
1200
|
+
let boundary;
|
|
1201
|
+
try {
|
|
1202
|
+
for await (const _event of readEvents(
|
|
1203
|
+
client,
|
|
1204
|
+
agentId,
|
|
1205
|
+
LATEST_CURSOR,
|
|
1206
|
+
controller.signal,
|
|
1207
|
+
(cursor) => {
|
|
1208
|
+
boundary = cursor;
|
|
1209
|
+
controller.abort();
|
|
1210
|
+
},
|
|
1211
|
+
)) {
|
|
1212
|
+
if (boundary !== undefined) break;
|
|
1213
|
+
}
|
|
1214
|
+
} finally {
|
|
1215
|
+
signal?.removeEventListener("abort", aborted);
|
|
1216
|
+
}
|
|
1217
|
+
if (boundary !== undefined) return boundary;
|
|
1218
|
+
if (signal?.aborted) throw abortError(signal.reason);
|
|
1219
|
+
throw new ManagedError(
|
|
1220
|
+
"event_stream_ended",
|
|
1221
|
+
"managed event stream ended before establishing the latest cursor boundary",
|
|
1222
|
+
);
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
async function* readEvents(client, agentId, initialCursor, signal, onControlCursor) {
|
|
1226
|
+
let cursor = initialCursor;
|
|
1227
|
+
let reconnectDelay = 1_000;
|
|
1228
|
+
|
|
1229
|
+
while (!signal?.aborted) {
|
|
1230
|
+
let response;
|
|
1231
|
+
try {
|
|
1232
|
+
response = await client.response(`${agentPath(agentId)}/events?cursor=${encodeURIComponent(cursor)}`, {
|
|
1233
|
+
accept: "text/event-stream",
|
|
1234
|
+
signal,
|
|
1235
|
+
});
|
|
1236
|
+
} catch (error) {
|
|
1237
|
+
if (signal?.aborted) return;
|
|
1238
|
+
await delay(reconnectDelay, signal);
|
|
1239
|
+
continue;
|
|
1240
|
+
}
|
|
1241
|
+
if (signal?.aborted) {
|
|
1242
|
+
void response.body?.cancel().catch(() => {});
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
if (!response.ok) {
|
|
1246
|
+
const error = await responseError(response);
|
|
1247
|
+
if (response.status !== 429 && response.status < 500) throw error;
|
|
1248
|
+
await delay(reconnectDelay, signal);
|
|
1249
|
+
continue;
|
|
1250
|
+
}
|
|
1251
|
+
if (!response.body) throw new ManagedError("invalid_response", "managed event stream has no body");
|
|
1252
|
+
|
|
1253
|
+
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
|
|
1254
|
+
let buffer = "";
|
|
1255
|
+
try {
|
|
1256
|
+
while (!signal?.aborted) {
|
|
1257
|
+
let chunk;
|
|
1258
|
+
try {
|
|
1259
|
+
chunk = await readEventChunk(reader, signal);
|
|
1260
|
+
} catch {
|
|
1261
|
+
if (signal?.aborted) return;
|
|
1262
|
+
break;
|
|
1263
|
+
}
|
|
1264
|
+
if (chunk.done) break;
|
|
1265
|
+
buffer += chunk.value.replaceAll("\r\n", "\n").replaceAll("\r", "\n");
|
|
1266
|
+
while (true) {
|
|
1267
|
+
const boundary = buffer.indexOf("\n\n");
|
|
1268
|
+
if (boundary < 0) break;
|
|
1269
|
+
const frame = buffer.slice(0, boundary);
|
|
1270
|
+
assertEventFrameSize(frame);
|
|
1271
|
+
const parsed = parseEventFrame(frame);
|
|
1272
|
+
buffer = buffer.slice(boundary + 2);
|
|
1273
|
+
if (!parsed) continue;
|
|
1274
|
+
if (parsed.retry !== undefined) reconnectDelay = parsed.retry;
|
|
1275
|
+
if (parsed.controlCursor !== undefined) {
|
|
1276
|
+
cursor = parsed.controlCursor;
|
|
1277
|
+
onControlCursor?.(cursor);
|
|
1278
|
+
}
|
|
1279
|
+
if (!parsed.data) continue;
|
|
1280
|
+
if (parsed.id !== undefined) cursor = parsed.id;
|
|
1281
|
+
const data = parseEventData(parsed.data);
|
|
1282
|
+
const eventCursor = parsed.id ?? requiredCursor(data, "cursor");
|
|
1283
|
+
cursor = eventCursor;
|
|
1284
|
+
yield managedEvent(data, eventCursor, parsed.event);
|
|
1285
|
+
}
|
|
1286
|
+
// Only the incomplete trailing frame remains here. Complete frames are
|
|
1287
|
+
// bounded independently above because one network read may coalesce
|
|
1288
|
+
// several valid SSE frames.
|
|
1289
|
+
assertEventFrameSize(buffer);
|
|
1290
|
+
}
|
|
1291
|
+
} finally {
|
|
1292
|
+
void reader.cancel().catch(() => {});
|
|
1293
|
+
}
|
|
1294
|
+
if (!signal?.aborted) await delay(reconnectDelay, signal);
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
function assertEventFrameSize(frame) {
|
|
1299
|
+
if (encodedBytes(frame) <= EVENT_STREAM_FRAME_BYTES) return;
|
|
1300
|
+
throw new ManagedError(
|
|
1301
|
+
"event_frame_too_large",
|
|
1302
|
+
`managed event frame exceeds ${EVENT_STREAM_FRAME_BYTES} decoded bytes`,
|
|
1303
|
+
);
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
function encodedBytes(value) {
|
|
1307
|
+
const encoded = typeof value === "string" ? value : JSON.stringify(value);
|
|
1308
|
+
return eventEncoder.encode(encoded).byteLength;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
function sameTerminal(left, right) {
|
|
1312
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
async function readEventChunk(reader, signal) {
|
|
1316
|
+
let timeout;
|
|
1317
|
+
let onAbort;
|
|
1318
|
+
const pending = reader.read();
|
|
1319
|
+
void pending.catch(() => {});
|
|
1320
|
+
const interrupted = new Promise((_, reject) => {
|
|
1321
|
+
timeout = setTimeout(() => {
|
|
1322
|
+
reject(new ManagedError(
|
|
1323
|
+
"event_stream_inactive",
|
|
1324
|
+
`managed event stream was inactive for ${EVENT_STREAM_INACTIVITY_TIMEOUT_MS}ms`,
|
|
1325
|
+
));
|
|
1326
|
+
}, EVENT_STREAM_INACTIVITY_TIMEOUT_MS);
|
|
1327
|
+
onAbort = () => reject(abortError(signal?.reason));
|
|
1328
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1329
|
+
});
|
|
1330
|
+
try {
|
|
1331
|
+
return await Promise.race([pending, interrupted]);
|
|
1332
|
+
} finally {
|
|
1333
|
+
clearTimeout(timeout);
|
|
1334
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
function managedEvent(data, cursor = requiredCursor(data, "cursor"), fallbackType = "message") {
|
|
1339
|
+
if (!data || typeof data !== "object" || Array.isArray(data) || typeof data.type !== "string") {
|
|
1340
|
+
throw new ManagedError("invalid_response", "managed event history contains a malformed event");
|
|
1341
|
+
}
|
|
1342
|
+
return Object.freeze({
|
|
1343
|
+
cursor,
|
|
1344
|
+
createdAt: typeof data.created_at === "number" ? data.created_at : undefined,
|
|
1345
|
+
turnId: typeof data.turn_id === "string" ? data.turn_id : null,
|
|
1346
|
+
type: typeof data.type === "string" ? data.type : fallbackType,
|
|
1347
|
+
data: Object.freeze(data),
|
|
1348
|
+
});
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
function cursorBefore(left, right) {
|
|
1352
|
+
if (left === LATEST_CURSOR) return false;
|
|
1353
|
+
if (right === LATEST_CURSOR) return true;
|
|
1354
|
+
return left.length !== right.length ? left.length < right.length : left < right;
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
function eventAfter(cursor, eventCursor) {
|
|
1358
|
+
return cursor === LATEST_CURSOR || cursorBefore(cursor, eventCursor);
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
function parseEventFrame(frame) {
|
|
1362
|
+
let event = "message";
|
|
1363
|
+
let id;
|
|
1364
|
+
let retry;
|
|
1365
|
+
let controlCursor;
|
|
1366
|
+
const data = [];
|
|
1367
|
+
for (const line of frame.split("\n")) {
|
|
1368
|
+
if (!line) continue;
|
|
1369
|
+
if (line.startsWith(":")) {
|
|
1370
|
+
const comment = line.slice(1).trimStart();
|
|
1371
|
+
if (comment.startsWith("cursor ")) {
|
|
1372
|
+
const value = comment.slice("cursor ".length);
|
|
1373
|
+
if (CURSOR.test(value)) controlCursor = value;
|
|
1374
|
+
}
|
|
1375
|
+
continue;
|
|
1376
|
+
}
|
|
1377
|
+
const separator = line.indexOf(":");
|
|
1378
|
+
const field = separator < 0 ? line : line.slice(0, separator);
|
|
1379
|
+
let value = separator < 0 ? "" : line.slice(separator + 1);
|
|
1380
|
+
if (value.startsWith(" ")) value = value.slice(1);
|
|
1381
|
+
if (field === "event") event = value;
|
|
1382
|
+
else if (field === "id" && !value.includes("\0") && CURSOR.test(value)) id = value;
|
|
1383
|
+
else if (field === "retry" && /^[0-9]+$/.test(value)) retry = Number(value);
|
|
1384
|
+
else if (field === "data") data.push(value);
|
|
1385
|
+
}
|
|
1386
|
+
if (data.length === 0 && retry === undefined && controlCursor === undefined) return undefined;
|
|
1387
|
+
return { event, id, retry, controlCursor, data: data.length === 0 ? undefined : data.join("\n") };
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
function parseEventData(encoded) {
|
|
1391
|
+
try {
|
|
1392
|
+
const data = JSON.parse(encoded);
|
|
1393
|
+
if (!data || typeof data !== "object" || Array.isArray(data) || typeof data.type !== "string") {
|
|
1394
|
+
throw new Error("event is not an object");
|
|
1395
|
+
}
|
|
1396
|
+
return data;
|
|
1397
|
+
} catch (error) {
|
|
1398
|
+
throw new ManagedError("invalid_event", "managed event data is malformed", { cause: error });
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
function managedClient(options) {
|
|
1403
|
+
validateOptions(options);
|
|
1404
|
+
const baseUrl = managedBaseUrl(options.baseUrl);
|
|
1405
|
+
const apiKey = options.apiKey;
|
|
1406
|
+
if (apiKey !== undefined && (typeof apiKey !== "string" || !API_KEY.test(apiKey))) {
|
|
1407
|
+
throw new TypeError("managed API key must be an ncx_live bearer key");
|
|
1408
|
+
}
|
|
1409
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
1410
|
+
if (typeof fetchImpl !== "function") throw new Error("fetch is unavailable in this runtime");
|
|
1411
|
+
const toolsTransport = options.toolsTransport;
|
|
1412
|
+
if (toolsTransport !== undefined
|
|
1413
|
+
&& typeof toolsTransport !== "function"
|
|
1414
|
+
&& typeof toolsTransport?.connect !== "function") {
|
|
1415
|
+
throw new TypeError("managed toolsTransport must be a function or provide connect(target, options)");
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
const response = async (path, init = {}) => {
|
|
1419
|
+
const headers = new Headers();
|
|
1420
|
+
if (init.body !== undefined) headers.set("content-type", "application/json");
|
|
1421
|
+
if (init.accept) headers.set("accept", init.accept);
|
|
1422
|
+
if (init.idempotencyKey) headers.set("idempotency-key", init.idempotencyKey);
|
|
1423
|
+
if (init.voiceSessionId) headers.set("x-nanocodex-voice-session-id", init.voiceSessionId);
|
|
1424
|
+
if (apiKey) headers.set("authorization", `Bearer ${apiKey}`);
|
|
1425
|
+
try {
|
|
1426
|
+
return await fetchImpl(new URL(path, baseUrl), {
|
|
1427
|
+
method: init.method ?? "GET",
|
|
1428
|
+
headers,
|
|
1429
|
+
credentials: apiKey ? "omit" : "include",
|
|
1430
|
+
...(init.body === undefined ? {} : { body: init.body }),
|
|
1431
|
+
...(init.signal === undefined ? {} : { signal: init.signal }),
|
|
1432
|
+
});
|
|
1433
|
+
} catch (error) {
|
|
1434
|
+
if (init.signal?.aborted) throw abortError(init.signal.reason);
|
|
1435
|
+
throw new ManagedError(
|
|
1436
|
+
"network_error",
|
|
1437
|
+
"Managed agent connection was interrupted. Check your network and retry.",
|
|
1438
|
+
{ cause: error },
|
|
1439
|
+
);
|
|
1440
|
+
}
|
|
1441
|
+
};
|
|
1442
|
+
return Object.freeze({
|
|
1443
|
+
baseUrl,
|
|
1444
|
+
response,
|
|
1445
|
+
toolsTarget(agentId) {
|
|
1446
|
+
const url = new URL(`${agentPath(agentId)}/tool-host`, baseUrl);
|
|
1447
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
1448
|
+
return Object.freeze({
|
|
1449
|
+
endpoint: url,
|
|
1450
|
+
transport: Object.freeze({
|
|
1451
|
+
async connect() {
|
|
1452
|
+
if (toolsTransport !== undefined) {
|
|
1453
|
+
const connect = typeof toolsTransport === "function"
|
|
1454
|
+
? toolsTransport
|
|
1455
|
+
: toolsTransport.connect.bind(toolsTransport);
|
|
1456
|
+
return connect(url, Object.freeze(apiKey
|
|
1457
|
+
? { headers: Object.freeze({ authorization: `Bearer ${apiKey}` }) }
|
|
1458
|
+
: { credentials: "include" }));
|
|
1459
|
+
}
|
|
1460
|
+
if (apiKey) {
|
|
1461
|
+
throw new Error("managed tools attachment with an API key requires toolsTransport so Authorization remains in the WebSocket handshake");
|
|
1462
|
+
}
|
|
1463
|
+
const WebSocketImpl = globalThis.WebSocket;
|
|
1464
|
+
if (typeof WebSocketImpl !== "function") {
|
|
1465
|
+
throw new Error("WebSocket is unavailable; configure managed Agent toolsTransport");
|
|
1466
|
+
}
|
|
1467
|
+
return new WebSocketImpl(url);
|
|
1468
|
+
},
|
|
1469
|
+
}),
|
|
1470
|
+
});
|
|
1471
|
+
},
|
|
1472
|
+
async json(path, init) {
|
|
1473
|
+
const result = await response(path, init);
|
|
1474
|
+
if (!result.ok) throw await responseError(result);
|
|
1475
|
+
try {
|
|
1476
|
+
return await result.json();
|
|
1477
|
+
} catch (error) {
|
|
1478
|
+
throw new ManagedError("invalid_response", "managed response is not valid JSON", {
|
|
1479
|
+
status: result.status,
|
|
1480
|
+
cause: error,
|
|
1481
|
+
});
|
|
1482
|
+
}
|
|
1483
|
+
},
|
|
1484
|
+
async empty(path, init) {
|
|
1485
|
+
const result = await response(path, init);
|
|
1486
|
+
if (!result.ok) throw await responseError(result);
|
|
1487
|
+
await result.body?.cancel();
|
|
1488
|
+
},
|
|
1489
|
+
});
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
async function responseError(response) {
|
|
1493
|
+
let body;
|
|
1494
|
+
try { body = await response.json(); } catch { body = undefined; }
|
|
1495
|
+
const code = typeof body?.error === "string" ? body.error : `http_${response.status}`;
|
|
1496
|
+
const message = typeof body?.message === "string" ? body.message : `managed request failed (${response.status})`;
|
|
1497
|
+
return new ManagedError(code, message, { status: response.status });
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
function validateOptions(options) {
|
|
1501
|
+
if (!options || typeof options !== "object" || Array.isArray(options)) {
|
|
1502
|
+
throw new TypeError("managed agent options must be an object");
|
|
1503
|
+
}
|
|
1504
|
+
const unsupported = Object.keys(options).find((key) => !ALLOWED_OPTIONS.has(key));
|
|
1505
|
+
if (unsupported) throw new TypeError(`managed agents do not accept ${unsupported}`);
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
function managedBaseUrl(value) {
|
|
1509
|
+
const fallback = globalThis.location?.origin;
|
|
1510
|
+
if (value === undefined && !fallback) {
|
|
1511
|
+
throw new TypeError("managed Agent requires baseUrl outside a browser");
|
|
1512
|
+
}
|
|
1513
|
+
const url = new URL(value ?? fallback);
|
|
1514
|
+
if (!/^https?:$/.test(url.protocol) || url.username || url.password || url.search || url.hash
|
|
1515
|
+
|| (url.pathname !== "/" && url.pathname !== "")) {
|
|
1516
|
+
throw new TypeError("managed baseUrl must be an HTTP(S) origin");
|
|
1517
|
+
}
|
|
1518
|
+
url.pathname = "/";
|
|
1519
|
+
return url;
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
function agentPath(id) {
|
|
1523
|
+
return `/v1/agents/${encodeURIComponent(id)}`;
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
function turnPath(agentId, turnId) {
|
|
1527
|
+
return `${agentPath(agentId)}/turns/${encodeURIComponent(turnId)}`;
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
function validateAgentId(id) {
|
|
1531
|
+
if (typeof id !== "string" || !TURN_ID.test(id)) {
|
|
1532
|
+
throw new TypeError("managed agent id is invalid");
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
function validateFindSessionsRequest(request) {
|
|
1537
|
+
if (!request || typeof request !== "object" || Array.isArray(request)) {
|
|
1538
|
+
throw new TypeError("managed find sessions request must be an object");
|
|
1539
|
+
}
|
|
1540
|
+
const unsupported = Object.keys(request).find((key) => !["query", "limit"].includes(key));
|
|
1541
|
+
if (unsupported) throw new TypeError(`managed find sessions does not accept ${unsupported}`);
|
|
1542
|
+
if (typeof request.query !== "string" || !request.query.trim()) {
|
|
1543
|
+
throw new TypeError("managed find sessions query must be a non-empty string");
|
|
1544
|
+
}
|
|
1545
|
+
if (request.limit !== undefined
|
|
1546
|
+
&& (!Number.isSafeInteger(request.limit) || request.limit < 1 || request.limit > 20)) {
|
|
1547
|
+
throw new TypeError("managed find sessions limit must be an integer from 1 through 20");
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
function validateReadSessionRequest(request) {
|
|
1552
|
+
if (!request || typeof request !== "object" || Array.isArray(request)) {
|
|
1553
|
+
throw new TypeError("managed read session request must be an object");
|
|
1554
|
+
}
|
|
1555
|
+
const unsupported = Object.keys(request).find((key) => !["session_id", "turn_ids"].includes(key));
|
|
1556
|
+
if (unsupported) throw new TypeError(`managed read session does not accept ${unsupported}`);
|
|
1557
|
+
if (typeof request.session_id !== "string" || !SESSION_ID.test(request.session_id)) {
|
|
1558
|
+
throw new TypeError("managed session id is invalid");
|
|
1559
|
+
}
|
|
1560
|
+
if (request.turn_ids !== undefined && (!Array.isArray(request.turn_ids)
|
|
1561
|
+
|| request.turn_ids.length > 20
|
|
1562
|
+
|| request.turn_ids.some((id) => typeof id !== "string" || !TURN_ID.test(id)))) {
|
|
1563
|
+
throw new TypeError("managed read session turn_ids must contain at most 20 valid turn ids");
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
function validateMemoryOperation(value) {
|
|
1568
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1569
|
+
throw new TypeError("managed memory operation must be an object");
|
|
1570
|
+
}
|
|
1571
|
+
if (typeof value.operation !== "string") {
|
|
1572
|
+
throw new TypeError("managed memory operation is required");
|
|
1573
|
+
}
|
|
1574
|
+
if (value.operation === "scan") {
|
|
1575
|
+
assertOnlyFields(value, ["operation", "query", "limit"], "managed memory scan");
|
|
1576
|
+
if (typeof value.query !== "string" || !value.query.trim()
|
|
1577
|
+
|| UTF8.encode(value.query).byteLength > 512) {
|
|
1578
|
+
throw new TypeError("managed memory scan query must be 1-512 UTF-8 bytes");
|
|
1579
|
+
}
|
|
1580
|
+
if (value.limit !== undefined
|
|
1581
|
+
&& (!Number.isSafeInteger(value.limit) || value.limit < 1 || value.limit > 5)) {
|
|
1582
|
+
throw new TypeError("managed memory scan limit must be an integer from 1 through 5");
|
|
1583
|
+
}
|
|
1584
|
+
return;
|
|
1585
|
+
}
|
|
1586
|
+
if (value.operation === "read") {
|
|
1587
|
+
assertOnlyFields(value, ["operation", "keys"], "managed memory read");
|
|
1588
|
+
if (!Array.isArray(value.keys) || value.keys.length === 0 || value.keys.length > 20) {
|
|
1589
|
+
throw new TypeError("managed memory read requires from 1 through 20 keys");
|
|
1590
|
+
}
|
|
1591
|
+
value.keys.forEach(validateMemoryKey);
|
|
1592
|
+
return;
|
|
1593
|
+
}
|
|
1594
|
+
if (value.operation === "put") {
|
|
1595
|
+
assertOnlyFields(value, ["operation", "content", "replace"], "managed memory put");
|
|
1596
|
+
if (typeof value.content !== "string" || !value.content.trim()
|
|
1597
|
+
|| UTF8.encode(value.content).byteLength > 1_024) {
|
|
1598
|
+
throw new TypeError("managed memory content must be 1-1024 UTF-8 bytes");
|
|
1599
|
+
}
|
|
1600
|
+
if (value.replace !== undefined) validateMemoryKey(value.replace);
|
|
1601
|
+
return;
|
|
1602
|
+
}
|
|
1603
|
+
if (value.operation === "delete") {
|
|
1604
|
+
assertOnlyFields(value, ["operation", "key"], "managed memory delete");
|
|
1605
|
+
validateMemoryKey(value.key);
|
|
1606
|
+
return;
|
|
1607
|
+
}
|
|
1608
|
+
throw new TypeError("managed memory operation must be scan, read, put, or delete");
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
function validateMemoryKey(value) {
|
|
1612
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1613
|
+
throw new TypeError("managed memory key must be an object");
|
|
1614
|
+
}
|
|
1615
|
+
assertOnlyFields(value, ["id", "version"], "managed memory key");
|
|
1616
|
+
if (!positiveSafeInteger(value.id) || !positiveSafeInteger(value.version)) {
|
|
1617
|
+
throw new TypeError("managed memory key id and version must be positive safe integers");
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
function validateOrganizationUpdate(value) {
|
|
1622
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1623
|
+
throw new TypeError("managed organization update must be an object");
|
|
1624
|
+
}
|
|
1625
|
+
assertOnlyFields(value, ["name"], "managed organization update");
|
|
1626
|
+
if (!Object.hasOwn(value, "name")
|
|
1627
|
+
|| (value.name !== null && (typeof value.name !== "string" || value.name.trim().length > 120))) {
|
|
1628
|
+
throw new TypeError("managed organization name must be null or at most 120 characters");
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
function assertOnlyFields(value, fields, label) {
|
|
1633
|
+
const unsupported = Object.keys(value).find((field) => !fields.includes(field));
|
|
1634
|
+
if (unsupported) throw new TypeError(`${label} does not accept ${unsupported}`);
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
function positiveSafeInteger(value) {
|
|
1638
|
+
return Number.isSafeInteger(value) && value > 0;
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
function nonnegativeSafeInteger(value) {
|
|
1642
|
+
return Number.isSafeInteger(value) && value >= 0;
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
function nullableNonnegativeSafeInteger(value) {
|
|
1646
|
+
return value === null || nonnegativeSafeInteger(value);
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
function requiredString(value, field) {
|
|
1650
|
+
const result = value?.[field];
|
|
1651
|
+
if (typeof result !== "string" || result.length === 0) {
|
|
1652
|
+
throw new ManagedError("invalid_response", `managed response has no ${field}`);
|
|
1653
|
+
}
|
|
1654
|
+
return result;
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
function requiredCursor(value, field) {
|
|
1658
|
+
const cursor = value?.[field];
|
|
1659
|
+
if (typeof cursor !== "string" || !CURSOR.test(cursor)) {
|
|
1660
|
+
throw new ManagedError("invalid_response", `managed response has no valid ${field}`);
|
|
1661
|
+
}
|
|
1662
|
+
return cursor;
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
function generatedIdempotencyKey() {
|
|
1666
|
+
if (typeof globalThis.crypto?.randomUUID !== "function") {
|
|
1667
|
+
throw new TypeError("managed prompt requires idempotencyKey when crypto.randomUUID is unavailable");
|
|
1668
|
+
}
|
|
1669
|
+
return `ncx-${globalThis.crypto.randomUUID()}`;
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
function stringOr(value, fallback) {
|
|
1673
|
+
return typeof value === "string" && value ? value : fallback;
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
function delay(milliseconds, signal) {
|
|
1677
|
+
if (signal?.aborted) return Promise.reject(abortError(signal.reason));
|
|
1678
|
+
return new Promise((resolve, reject) => {
|
|
1679
|
+
const onAbort = () => {
|
|
1680
|
+
clearTimeout(timeout);
|
|
1681
|
+
reject(abortError(signal.reason));
|
|
1682
|
+
};
|
|
1683
|
+
const timeout = setTimeout(() => {
|
|
1684
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1685
|
+
resolve();
|
|
1686
|
+
}, milliseconds);
|
|
1687
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1688
|
+
});
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
function abortError(reason) {
|
|
1692
|
+
if (reason instanceof Error) return reason;
|
|
1693
|
+
return new DOMException("The operation was aborted", "AbortError");
|
|
1694
|
+
}
|