agents 0.22.0 → 0.23.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/README.md +20 -1
- package/dist/{agent-routing-CnkaHb-v.d.ts → agent-routing-DE5zmCQ8.d.ts} +1038 -394
- package/dist/agent-routing.d.ts +1 -1
- package/dist/agent-tool-types.d.ts +26 -26
- package/dist/{agent-tools-C0XJqWzB.d.ts → agent-tools-DtXMTDGM.d.ts} +7 -7
- package/dist/agent-tools.d.ts +21 -21
- package/dist/browser/ai.js +4 -1
- package/dist/browser/ai.js.map +1 -1
- package/dist/browser/tanstack-ai.js.map +1 -1
- package/dist/callable-decorator-DP__HhBA.d.ts +72 -0
- package/dist/callable-decorator.d.ts +18 -0
- package/dist/callable-decorator.js +71 -0
- package/dist/callable-decorator.js.map +1 -0
- package/dist/capability-BjSKYpzg.js.map +1 -1
- package/dist/capability-runner-Be_-PLR1.d.ts +459 -0
- package/dist/channel-Bnm4S7T2.d.ts +491 -0
- package/dist/channels/ai-sdk.d.ts +46 -0
- package/dist/channels/ai-sdk.js +120 -0
- package/dist/channels/ai-sdk.js.map +1 -0
- package/dist/channels/email.d.ts +95 -0
- package/dist/channels/email.js +323 -0
- package/dist/channels/email.js.map +1 -0
- package/dist/channels/index.d.ts +233 -0
- package/dist/channels/index.js +608 -0
- package/dist/channels/index.js.map +1 -0
- package/dist/channels/slack.d.ts +140 -0
- package/dist/channels/slack.js +614 -0
- package/dist/channels/slack.js.map +1 -0
- package/dist/channels/tanstack-ai.d.ts +39 -0
- package/dist/channels/tanstack-ai.js +17 -0
- package/dist/channels/tanstack-ai.js.map +1 -0
- package/dist/channels/telegram.d.ts +106 -0
- package/dist/channels/telegram.js +427 -0
- package/dist/channels/telegram.js.map +1 -0
- package/dist/channels/voice.d.ts +45 -0
- package/dist/channels/voice.js +122 -0
- package/dist/channels/voice.js.map +1 -0
- package/dist/chat/index.d.ts +2325 -2019
- package/dist/chat/index.js +888 -518
- package/dist/chat/index.js.map +1 -1
- package/dist/chat-sdk/index.d.ts +7 -7
- package/dist/chat-sdk/index.js +1 -1
- package/dist/client.d.ts +1 -1
- package/dist/context/index.d.ts +216 -0
- package/dist/context/index.js +454 -0
- package/dist/context/index.js.map +1 -0
- package/dist/{current-agent-CuMErtly.d.ts → current-agent-Da_C9a3b.d.ts} +90 -107
- package/dist/current-agent-DhoDkSnH.js.map +1 -1
- package/dist/{diagnostics-CaBjfz4J.js → diagnostics-BzvaX2UT.js} +5 -1
- package/dist/diagnostics-BzvaX2UT.js.map +1 -0
- package/dist/diagnostics-C4jcz3VK.js +360 -0
- package/dist/diagnostics-C4jcz3VK.js.map +1 -0
- package/dist/index-BB0kqhIz.d.ts +101 -0
- package/dist/index-BVVgDSdq.d.ts +1 -0
- package/dist/index-XDkuQ7zm.d.ts +89 -0
- package/dist/{index-DcSAZKsB.d.ts → index-YSKgfgg9.d.ts} +3 -1
- package/dist/index.d.ts +91 -82
- package/dist/index.js +3 -2
- package/dist/ingress-BfetZbMO.js +83 -0
- package/dist/ingress-BfetZbMO.js.map +1 -0
- package/dist/internal-CYlgHl1l.js +59 -0
- package/dist/internal-CYlgHl1l.js.map +1 -0
- package/dist/internal_context.d.ts +1 -1
- package/dist/lifecycle/index.d.ts +35 -17
- package/dist/lifecycle/index.js +1 -1
- package/dist/lifecycle-CMRGjZdw.js +1299 -0
- package/dist/lifecycle-CMRGjZdw.js.map +1 -0
- package/dist/mcp/client/index.d.ts +20 -20
- package/dist/mcp/index.d.ts +35 -35
- package/dist/mcp/index.js +1 -1
- package/dist/observability/index.d.ts +1 -1
- package/dist/observability/index.js +1 -1
- package/dist/react.d.ts +4 -4
- package/dist/{retries-CAvxtG9d.d.ts → retries-D9Ds-1lz.d.ts} +17 -6
- package/dist/retries.d.ts +8 -6
- package/dist/retries.js +13 -1
- package/dist/retries.js.map +1 -1
- package/dist/routing/index.d.ts +137 -0
- package/dist/routing/index.js +244 -0
- package/dist/routing/index.js.map +1 -0
- package/dist/sanitize-D9TujEK8.js +79 -0
- package/dist/sanitize-D9TujEK8.js.map +1 -0
- package/dist/scheduler-DD9NdYbF.js +665 -0
- package/dist/scheduler-DD9NdYbF.js.map +1 -0
- package/dist/{scheduler-DQoTGoAW.d.ts → scheduler-Dwh85ZGl.d.ts} +21 -22
- package/dist/schedules/index.d.ts +1 -1
- package/dist/schedules/index.js +1 -1
- package/dist/sentence-chunker-BAidJ4DA.d.ts +68 -0
- package/dist/serializable.d.ts +1 -1
- package/dist/sessions/index.d.ts +441 -0
- package/dist/sessions/index.js +2063 -0
- package/dist/sessions/index.js.map +1 -0
- package/dist/skills/index.d.ts +99 -0
- package/dist/skills/index.js +254 -5
- package/dist/skills/index.js.map +1 -1
- package/dist/{src-5W6JNKVb.js → src-DlSHshb2.js} +1460 -1110
- package/dist/src-DlSHshb2.js.map +1 -0
- package/dist/streams/index.d.ts +120 -0
- package/dist/streams/index.js +107 -0
- package/dist/streams/index.js.map +1 -0
- package/dist/streams-D6tJ0NN9.d.ts +370 -0
- package/dist/streams-DZKgAj9b.js +709 -0
- package/dist/streams-DZKgAj9b.js.map +1 -0
- package/dist/sub-routing.d.ts +12 -12
- package/dist/surface-bZZJqBka.js +17 -0
- package/dist/surface-bZZJqBka.js.map +1 -0
- package/dist/tasks/index.d.ts +64 -0
- package/dist/tasks/index.js +2 -0
- package/dist/tasks-BRJ5zgya.d.ts +517 -0
- package/dist/tasks-ylZgBjhj.js +1656 -0
- package/dist/tasks-ylZgBjhj.js.map +1 -0
- package/dist/text-segment-joiner-BtAFQSA_.js +57 -0
- package/dist/text-segment-joiner-BtAFQSA_.js.map +1 -0
- package/dist/text-stream-CpdiKrJB.js +272 -0
- package/dist/text-stream-CpdiKrJB.js.map +1 -0
- package/dist/tokens-nHAKcN6M.js +52 -0
- package/dist/tokens-nHAKcN6M.js.map +1 -0
- package/dist/tool-schema-CBjGPrsQ.js +31 -0
- package/dist/tool-schema-CBjGPrsQ.js.map +1 -0
- package/dist/types-B7LojTe4.d.ts +202 -0
- package/dist/types-_Faxb570.d.ts +439 -0
- package/dist/voice/client.d.ts +226 -0
- package/dist/voice/client.js +932 -0
- package/dist/voice/client.js.map +1 -0
- package/dist/voice/errors.d.ts +43 -0
- package/dist/voice/errors.js +41 -0
- package/dist/voice/errors.js.map +1 -0
- package/dist/voice/index.d.ts +271 -0
- package/dist/voice/index.js +1812 -0
- package/dist/voice/index.js.map +1 -0
- package/dist/voice/react.d.ts +167 -0
- package/dist/voice/react.js +234 -0
- package/dist/voice/react.js.map +1 -0
- package/dist/voice/sfu.d.ts +71 -0
- package/dist/voice/sfu.js +157 -0
- package/dist/voice/sfu.js.map +1 -0
- package/dist/voice/text.d.ts +6 -0
- package/dist/voice/text.js +2 -0
- package/dist/voice/types.d.ts +58 -0
- package/dist/voice/types.js +18 -0
- package/dist/voice/types.js.map +1 -0
- package/dist/voice/workers-ai.d.ts +136 -0
- package/dist/voice/workers-ai.js +568 -0
- package/dist/voice/workers-ai.js.map +1 -0
- package/dist/websockets/index.d.ts +192 -0
- package/dist/websockets/index.js +2 -0
- package/dist/websockets-DUfRHPRq.js +502 -0
- package/dist/websockets-DUfRHPRq.js.map +1 -0
- package/dist/workflow-types.d.ts +25 -25
- package/dist/workflows.d.ts +21 -21
- package/dist/workflows.js +1 -1
- package/docs/agent-class.md +2 -2
- package/docs/agent-tools.md +2 -1
- package/docs/channels.md +323 -0
- package/docs/chat-agents.md +6 -13
- package/docs/context.md +131 -0
- package/docs/index.md +15 -12
- package/docs/lifecycle.md +102 -55
- package/docs/long-running-agents.md +2 -2
- package/docs/mcp-servers.md +5 -1
- package/docs/resumable-streaming.md +1 -1
- package/docs/routing.md +105 -0
- package/docs/sessions.md +237 -871
- package/docs/streams.md +213 -0
- package/docs/sub-agents.md +184 -124
- package/docs/tasks.md +246 -0
- package/docs/voice.md +745 -0
- package/package.json +115 -13
- package/dist/capability-runner-CvHGZqUu.d.ts +0 -150
- package/dist/compaction-helpers-iiKMr2TQ.js +0 -340
- package/dist/compaction-helpers-iiKMr2TQ.js.map +0 -1
- package/dist/compaction-helpers-wUz6M3us.d.ts +0 -621
- package/dist/diagnostics-CaBjfz4J.js.map +0 -1
- package/dist/durable-object-lifecycle-D6nNQJJd.js +0 -862
- package/dist/durable-object-lifecycle-D6nNQJJd.js.map +0 -1
- package/dist/experimental/memory/session/index.d.ts +0 -671
- package/dist/experimental/memory/session/index.js +0 -2379
- package/dist/experimental/memory/session/index.js.map +0 -1
- package/dist/experimental/memory/utils/index.d.ts +0 -96
- package/dist/experimental/memory/utils/index.js +0 -79
- package/dist/experimental/memory/utils/index.js.map +0 -1
- package/dist/scheduler-CR9RHGos.js +0 -857
- package/dist/scheduler-CR9RHGos.js.map +0 -1
- package/dist/src-5W6JNKVb.js.map +0 -1
- package/dist/tool-output-truncation-CNnnGZQ3.js +0 -98
- package/dist/tool-output-truncation-CNnnGZQ3.js.map +0 -1
package/dist/retries.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"retries.js","names":[],"sources":["../src/retries.ts"],"sourcesContent":["/**\n * Retry options for schedule(), scheduleEvery(), queue(), and this.retry().\n */\nexport interface RetryOptions {\n /** Max number of attempts (including the first). Default: 3 */\n maxAttempts?: number;\n /** Base delay in ms for exponential backoff. Default: 100 */\n baseDelayMs?: number;\n /** Max delay cap in ms. Default: 3000 */\n maxDelayMs?: number;\n}\n\n/**\n * Internal options for tryN -- extends RetryOptions with a shouldRetry predicate.\n */\ninterface TryNOptions extends RetryOptions {\n /**\n * Predicate to determine if an error should be retried.\n * Receives the error and the next attempt number (so callers can\n * make attempt-aware decisions).\n * If not provided, all errors are retried.\n */\n shouldRetry?: (err: unknown, nextAttempt: number) => boolean;\n}\n\n/**\n * Validate retry options eagerly so invalid config fails at enqueue/schedule time\n * rather than at execution time. Checks individual field ranges, enforces integer\n * maxAttempts, and validates cross-field constraints after resolving against\n * defaults when provided.\n */\nexport function validateRetryOptions(\n options: RetryOptions,\n defaults?: Required<RetryOptions>\n): void {\n if (options.maxAttempts !== undefined) {\n if (!Number.isFinite(options.maxAttempts) || options.maxAttempts < 1) {\n throw new Error(\"retry.maxAttempts must be >= 1\");\n }\n if (!Number.isInteger(options.maxAttempts)) {\n throw new Error(\"retry.maxAttempts must be an integer\");\n }\n }\n if (options.baseDelayMs !== undefined) {\n if (!Number.isFinite(options.baseDelayMs) || options.baseDelayMs <= 0) {\n throw new Error(\"retry.baseDelayMs must be > 0\");\n }\n }\n if (options.maxDelayMs !== undefined) {\n if (!Number.isFinite(options.maxDelayMs) || options.maxDelayMs <= 0) {\n throw new Error(\"retry.maxDelayMs must be > 0\");\n }\n }\n\n // Resolve against defaults (when provided) so that cross-field checks\n // catch e.g. { baseDelayMs: 5000 } against default maxDelayMs: 3000.\n const resolvedBase = options.baseDelayMs ?? defaults?.baseDelayMs;\n const resolvedMax = options.maxDelayMs ?? defaults?.maxDelayMs;\n if (\n resolvedBase !== undefined &&\n resolvedMax !== undefined &&\n resolvedBase > resolvedMax\n ) {\n throw new Error(\"retry.baseDelayMs must be <= retry.maxDelayMs\");\n }\n}\n\n/**\n * Returns the number of milliseconds to wait before retrying a request.\n * Uses the \"Full Jitter\" approach from\n * https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/\n *\n * @param attempt The current attempt number (1-indexed).\n * @param baseDelayMs Base delay multiplier in ms.\n * @param maxDelayMs Maximum delay cap in ms.\n * @returns Milliseconds to wait before retrying.\n */\nexport function jitterBackoff(\n attempt: number,\n baseDelayMs: number,\n maxDelayMs: number\n): number {\n const upperBoundMs = Math.min(2 ** attempt * baseDelayMs, maxDelayMs);\n return Math.floor(Math.random() * upperBoundMs);\n}\n\n/**\n * Retry an async function up to `n` total attempts with jittered exponential backoff.\n *\n * @param n Total number of attempts (must be a finite integer >= 1).\n * @param fn The async function to retry. Receives the current attempt number (1-indexed).\n * @param options Retry configuration.\n * @returns The result of `fn` on success.\n * @throws The last error if all attempts fail or `shouldRetry` returns false.\n */\nexport async function tryN<T>(\n n: number,\n fn: (attempt: number) => Promise<T>,\n options?: TryNOptions\n): Promise<T> {\n if (!Number.isFinite(n) || n < 1) {\n throw new Error(\"retry.maxAttempts must be >= 1\");\n }\n n = Math.floor(n);\n\n const rawBase = options?.baseDelayMs ?? 100;\n const rawMax = options?.maxDelayMs ?? 3000;\n\n if (!Number.isFinite(rawBase) || rawBase <= 0) {\n throw new Error(\"retry.baseDelayMs must be > 0\");\n }\n if (!Number.isFinite(rawMax) || rawMax <= 0) {\n throw new Error(\"retry.maxDelayMs must be > 0\");\n }\n\n const baseDelayMs = Math.floor(rawBase);\n const maxDelayMs = Math.floor(rawMax);\n\n if (baseDelayMs > maxDelayMs) {\n throw new Error(\"retry.baseDelayMs must be <= retry.maxDelayMs\");\n }\n\n let attempt = 1;\n while (true) {\n try {\n return await fn(attempt);\n } catch (err) {\n const nextAttempt = attempt + 1;\n if (\n nextAttempt > n ||\n (options?.shouldRetry && !options.shouldRetry(err, nextAttempt))\n ) {\n throw err;\n }\n const delay = jitterBackoff(attempt, baseDelayMs, maxDelayMs);\n await new Promise((resolve) => setTimeout(resolve, delay));\n attempt = nextAttempt;\n }\n }\n}\n\n/**\n * Returns true if the given error is retryable according to Durable Object error handling.\n * See https://developers.cloudflare.com/durable-objects/best-practices/error-handling/\n *\n * An error is retryable if it has `retryable: true` but is NOT an overloaded error.\n */\nexport function isErrorRetryable(err: unknown): boolean {\n if (typeof err !== \"object\" || err === null) {\n return false;\n }\n const msg = String(err);\n const typed = err as { retryable?: boolean; overloaded?: boolean };\n return (\n Boolean(typed.retryable) &&\n !typed.overloaded &&\n !msg.includes(\"Durable Object is overloaded\")\n );\n}\n\n/**\n * The \"superseded isolate\" platform messages — the invocation is running on an\n * isolate the platform has replaced with a new version (a deploy / code\n * update). For the rest of that invocation every operation throws the same\n * error (code never reloads mid-invocation), so in-process retries are futile;\n * but the next fresh invocation runs the new code and succeeds.\n *\n * workerd surfaces this as a plain `Error` with one of a few messages, all the\n * same failure class — a message match is the only signal:\n * - \"Durable Object reset because its code was updated.\" (DO storage op on a\n * superseded isolate / deploy bounce)\n * - \"This script has been upgraded. Please send a new request to connect to\n * the new version.\" (a stub/connection to a superseded script; the message\n * literally instructs the caller to retry on the new version)\n *\n * The match stays close to the verbatim platform strings (rather than a loose\n * \"upgraded\"/\"reset\" substring) so an ordinary application error that happens\n * to mention those words is NOT misclassified as a supersede.\n */\nconst SUPERSEDED_ISOLATE_PATTERN =\n /reset because its code was updated|this script has been upgraded/i;\n\n/**\n * The \"Network connection lost.\" platform transient — the connection between\n * the isolate and its storage (or another DO) dropped. Unlike a supersede this\n * MAY succeed on an in-process retry (a momentary blip), so it must not skip\n * the in-process retry budget — but during a deploy-reset window it never\n * succeeds in-process and surfaces interleaved with the supersede messages\n * (SQL ops throw `SqlError: SQL query failed: Network connection lost.` while\n * KV ops throw the reset message), so on retry exhaustion it must be treated\n * as the platform's failure, not the callback's.\n */\nconst CONNECTION_LOST_PATTERN = /network connection lost/i;\n\n/**\n * The exact Durable Object storage-reset platform signal. Keep this narrow:\n * ordinary SQL and generic internal errors are application failures. This is a\n * transient storage reset, not a memory-limit poison pill.\n */\nconst STORAGE_RESET_PATTERN =\n /Internal error in Durable Object storage caused object to be reset/i;\n\n/**\n * The Durable Object memory-limit reset — the isolate exceeded its 128 MB limit\n * and was reset by the platform (workerd surfaces this verbatim as\n * \"Durable Object's isolate exceeded its memory limit and was reset.\"; the D1\n * sibling is \"D1 DB's isolate exceeded its memory limit and was reset.\").\n *\n * The match is the broad shared fragment \"exceeded its memory limit\" rather than\n * the full \"...and was reset\" sentence: real-world surfacings truncate or reword\n * the tail (some log pipelines clip the message; D1/storage wrappers re-prefix\n * it), and a customer-reported loop (#1825) showed lines carrying only the\n * \"exceeded its memory limit\" fragment. Missing a surfacing here means the\n * circuit breaker never engages, so we err toward the broader match — and even a\n * false positive is fail-safe (a tightly-bounded retry-then-seal, not data loss).\n *\n * This is DELIBERATELY a separate class from `SUPERSEDED_ISOLATE_PATTERN` /\n * {@link isPlatformTransientError}, and is NOT folded into them. A supersede or\n * connection-lost transient means \"re-run the same work and it succeeds on a\n * healthy isolate\" — those classes can be deferred and retried *indefinitely*. A\n * memory-limit reset is the opposite: re-running the SAME memory-heavy work\n * deterministically re-OOMs (the footprint, not the platform, is the cause), so\n * deferring it indefinitely would PRESERVE the one-shot row and re-run the\n * doomed work forever (amplifying the loop and cost — see #1825). It is a\n * poison-pill signal: callers must bound retries tightly and then SEAL.\n *\n * Accordingly the schedule executor (`_executeScheduleCallback`) and the\n * alarm-boundary circuit breaker (`Agent.alarm`) treat it as its OWN class: a\n * memory-limit reset is re-thrown (row preserved) so it reaches the breaker,\n * which tolerates a few strikes (`maxAlarmMemoryLimitStrikes`) and then seals +\n * purges the looping row — i.e. *bounded* deferral, never the unbounded deferral\n * the transient classes get.\n */\nconst MEMORY_LIMIT_RESET_PATTERN = /exceeded its memory limit/i;\n\nfunction errorMessageOf(error: unknown): string {\n return error instanceof Error\n ? error.message\n : typeof error === \"string\"\n ? error\n : \"\";\n}\n\n/**\n * Iterate an error and its `cause` chain (depth-limited so a cyclic chain\n * can't spin). Wrappers like `SqlError` carry the original platform error in\n * `cause` and may not propagate signal properties (e.g. the CF `retryable`\n * flag), so classification must look through them.\n */\nfunction* selfAndCauses(error: unknown): Generator<unknown> {\n let current = error;\n for (let depth = 0; depth < 8 && current != null; depth++) {\n yield current;\n current =\n typeof current === \"object\"\n ? (current as { cause?: unknown }).cause\n : undefined;\n }\n}\n\n/**\n * Whether an error (or anything in its `cause` chain) is a transient\n * \"superseded isolate\" failure — see `SUPERSEDED_ISOLATE_PATTERN`. In-process\n * retries are futile for this class; the work must be deferred to a fresh\n * invocation, which runs the new code and succeeds.\n */\nexport function isDurableObjectCodeUpdateReset(error: unknown): boolean {\n for (const e of selfAndCauses(error)) {\n if (SUPERSEDED_ISOLATE_PATTERN.test(errorMessageOf(e))) return true;\n }\n return false;\n}\n\n/**\n * Whether an error (or anything in its `cause` chain) carries the exact\n * Durable Object storage-reset platform fragment. Generic SQL/internal errors\n * deliberately do not qualify.\n */\nexport function isDurableObjectStorageReset(error: unknown): boolean {\n for (const e of selfAndCauses(error)) {\n if (STORAGE_RESET_PATTERN.test(errorMessageOf(e))) return true;\n }\n return false;\n}\n\n/**\n * Whether an error (or anything in its `cause` chain, or a raw error-message\n * string) is a Durable Object memory-limit reset — see\n * {@link MEMORY_LIMIT_RESET_PATTERN}. Unlike {@link isPlatformTransientError},\n * re-running the same work re-OOMs deterministically, so callers must NOT defer\n * it like a transient; they should bound retries tightly and then seal (#1825).\n */\nexport function isDurableObjectMemoryLimitReset(error: unknown): boolean {\n for (const e of selfAndCauses(error)) {\n if (MEMORY_LIMIT_RESET_PATTERN.test(errorMessageOf(e))) return true;\n }\n return false;\n}\n\n/**\n * Whether an error (or anything in its `cause` chain) is a transient failure\n * of the PLATFORM rather than of the code that threw it:\n *\n * - a superseded-isolate reset (\"reset because its code was updated\" /\n * \"this script has been upgraded\") — a deploy replaced the isolate;\n * - an error the platform itself flags `retryable: true` (excluding\n * overloaded errors, where retrying the same object won't help) — see\n * `isErrorRetryable`;\n * - \"Network connection lost.\" — the storage/stub connection dropped. The\n * CF `retryable` flag does not survive error wrappers (e.g. `SqlError`\n * copies only the message + `cause`) and is absent in some local-dev\n * shapes, so the verbatim message is matched as well;\n * - the exact \"Internal error in Durable Object storage caused object to be\n * reset\" platform fragment. Generic internal and SQL errors remain fatal.\n *\n * Used to decide whether failed work should be RE-RUN LATER (platform\n * transient — the same work succeeds once the platform recovers, typically\n * seconds after a deploy) versus ABANDONED as genuinely failing (application\n * error — re-running yields the same failure). A genuine application error\n * carries none of these signals, so it is never misclassified by this check.\n */\nexport function isPlatformTransientError(error: unknown): boolean {\n for (const e of selfAndCauses(error)) {\n const message = errorMessageOf(e);\n if (SUPERSEDED_ISOLATE_PATTERN.test(message)) return true;\n if (CONNECTION_LOST_PATTERN.test(message)) return true;\n if (STORAGE_RESET_PATTERN.test(message)) return true;\n if (isErrorRetryable(e)) return true;\n }\n return false;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"retries.js","names":[],"sources":["../src/retries.ts"],"sourcesContent":["/**\n * Retry options for schedule(), scheduleEvery(), queue(), and this.retry().\n */\nexport interface RetryOptions {\n /** Max number of attempts (including the first). Default: 3 */\n maxAttempts?: number;\n /** Base delay in ms for exponential backoff. Default: 100 */\n baseDelayMs?: number;\n /** Max delay cap in ms. Default: 3000 */\n maxDelayMs?: number;\n}\n\n/**\n * Internal options for tryN -- extends RetryOptions with a shouldRetry predicate.\n */\ninterface TryNOptions extends RetryOptions {\n /**\n * Predicate to determine if an error should be retried.\n * Receives the error and the next attempt number (so callers can\n * make attempt-aware decisions).\n * If not provided, all errors are retried.\n */\n shouldRetry?: (err: unknown, nextAttempt: number) => boolean;\n}\n\n/**\n * Validate retry options eagerly so invalid config fails at enqueue/schedule time\n * rather than at execution time. Checks individual field ranges, enforces integer\n * maxAttempts, and validates cross-field constraints after resolving against\n * defaults when provided.\n */\nexport function validateRetryOptions(\n options: RetryOptions,\n defaults?: Required<RetryOptions>\n): void {\n if (options.maxAttempts !== undefined) {\n if (!Number.isFinite(options.maxAttempts) || options.maxAttempts < 1) {\n throw new Error(\"retry.maxAttempts must be >= 1\");\n }\n if (!Number.isInteger(options.maxAttempts)) {\n throw new Error(\"retry.maxAttempts must be an integer\");\n }\n }\n if (options.baseDelayMs !== undefined) {\n if (!Number.isFinite(options.baseDelayMs) || options.baseDelayMs <= 0) {\n throw new Error(\"retry.baseDelayMs must be > 0\");\n }\n }\n if (options.maxDelayMs !== undefined) {\n if (!Number.isFinite(options.maxDelayMs) || options.maxDelayMs <= 0) {\n throw new Error(\"retry.maxDelayMs must be > 0\");\n }\n }\n\n // Resolve against defaults (when provided) so that cross-field checks\n // catch e.g. { baseDelayMs: 5000 } against default maxDelayMs: 3000.\n const resolvedBase = options.baseDelayMs ?? defaults?.baseDelayMs;\n const resolvedMax = options.maxDelayMs ?? defaults?.maxDelayMs;\n if (\n resolvedBase !== undefined &&\n resolvedMax !== undefined &&\n resolvedBase > resolvedMax\n ) {\n throw new Error(\"retry.baseDelayMs must be <= retry.maxDelayMs\");\n }\n}\n\n/**\n * Returns the number of milliseconds to wait before retrying a request.\n * Uses the \"Full Jitter\" approach from\n * https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/\n *\n * @param attempt The current attempt number (1-indexed).\n * @param baseDelayMs Base delay multiplier in ms.\n * @param maxDelayMs Maximum delay cap in ms.\n * @returns Milliseconds to wait before retrying.\n */\nexport function jitterBackoff(\n attempt: number,\n baseDelayMs: number,\n maxDelayMs: number\n): number {\n const upperBoundMs = Math.min(2 ** attempt * baseDelayMs, maxDelayMs);\n return Math.floor(Math.random() * upperBoundMs);\n}\n\n/**\n * Retry an async function up to `n` total attempts with jittered exponential backoff.\n *\n * @param n Total number of attempts (must be a finite integer >= 1).\n * @param fn The async function to retry. Receives the current attempt number (1-indexed).\n * @param options Retry configuration.\n * @returns The result of `fn` on success.\n * @throws The last error if all attempts fail or `shouldRetry` returns false.\n */\nexport async function tryN<T>(\n n: number,\n fn: (attempt: number) => Promise<T>,\n options?: TryNOptions\n): Promise<T> {\n if (!Number.isFinite(n) || n < 1) {\n throw new Error(\"retry.maxAttempts must be >= 1\");\n }\n n = Math.floor(n);\n\n const rawBase = options?.baseDelayMs ?? 100;\n const rawMax = options?.maxDelayMs ?? 3000;\n\n if (!Number.isFinite(rawBase) || rawBase <= 0) {\n throw new Error(\"retry.baseDelayMs must be > 0\");\n }\n if (!Number.isFinite(rawMax) || rawMax <= 0) {\n throw new Error(\"retry.maxDelayMs must be > 0\");\n }\n\n const baseDelayMs = Math.floor(rawBase);\n const maxDelayMs = Math.floor(rawMax);\n\n if (baseDelayMs > maxDelayMs) {\n throw new Error(\"retry.baseDelayMs must be <= retry.maxDelayMs\");\n }\n\n let attempt = 1;\n while (true) {\n try {\n return await fn(attempt);\n } catch (err) {\n const nextAttempt = attempt + 1;\n if (\n nextAttempt > n ||\n (options?.shouldRetry && !options.shouldRetry(err, nextAttempt))\n ) {\n throw err;\n }\n const delay = jitterBackoff(attempt, baseDelayMs, maxDelayMs);\n await new Promise((resolve) => setTimeout(resolve, delay));\n attempt = nextAttempt;\n }\n }\n}\n\n/**\n * Returns true if the given error is retryable according to Durable Object error handling.\n * See https://developers.cloudflare.com/durable-objects/best-practices/error-handling/\n *\n * An error is retryable if it has `retryable: true` but is NOT an overloaded error.\n */\nexport function isErrorRetryable(err: unknown): boolean {\n if (typeof err !== \"object\" || err === null) {\n return false;\n }\n const msg = String(err);\n const typed = err as { retryable?: boolean; overloaded?: boolean };\n return (\n Boolean(typed.retryable) &&\n !typed.overloaded &&\n !msg.includes(\"Durable Object is overloaded\")\n );\n}\n\n/**\n * The \"superseded isolate\" platform messages — the invocation is running on an\n * isolate the platform has replaced with a new version (a deploy / code\n * update). For the rest of that invocation every operation throws the same\n * error (code never reloads mid-invocation), so in-process retries are futile;\n * but the next fresh invocation runs the new code and succeeds.\n *\n * workerd surfaces this as a plain `Error` with one of a few messages, all the\n * same failure class — a message match is the only signal:\n * - \"Durable Object reset because its code was updated.\" (DO storage op on a\n * superseded isolate / deploy bounce)\n * - \"This script has been upgraded. Please send a new request to connect to\n * the new version.\" (a stub/connection to a superseded script; the message\n * literally instructs the caller to retry on the new version)\n *\n * The match stays close to the verbatim platform strings (rather than a loose\n * \"upgraded\"/\"reset\" substring) so an ordinary application error that happens\n * to mention those words is NOT misclassified as a supersede.\n */\nconst SUPERSEDED_ISOLATE_PATTERN =\n /reset because its code was updated|this script has been upgraded/i;\n\n/**\n * The \"Network connection lost.\" platform transient — the connection between\n * the isolate and its storage (or another DO) dropped. Unlike a supersede this\n * MAY succeed on an in-process retry (a momentary blip), so it must not skip\n * the in-process retry budget — but during a deploy-reset window it never\n * succeeds in-process and surfaces interleaved with the supersede messages\n * (SQL ops throw `SqlError: SQL query failed: Network connection lost.` while\n * KV ops throw the reset message), so on retry exhaustion it must be treated\n * as the platform's failure, not the callback's.\n */\nconst CONNECTION_LOST_PATTERN = /network connection lost/i;\n\n/**\n * The exact Durable Object storage-reset platform signal. Keep this narrow:\n * ordinary SQL and generic internal errors are application failures. This is a\n * transient storage reset, not a memory-limit poison pill.\n */\nconst STORAGE_RESET_PATTERN =\n /Internal error in Durable Object storage caused object to be reset/i;\n\n/**\n * The Durable Object memory-limit reset — the isolate exceeded its 128 MB limit\n * and was reset by the platform (workerd surfaces this verbatim as\n * \"Durable Object's isolate exceeded its memory limit and was reset.\"; the D1\n * sibling is \"D1 DB's isolate exceeded its memory limit and was reset.\").\n *\n * The match is the broad shared fragment \"exceeded its memory limit\" rather than\n * the full \"...and was reset\" sentence: real-world surfacings truncate or reword\n * the tail (some log pipelines clip the message; D1/storage wrappers re-prefix\n * it), and a customer-reported loop (#1825) showed lines carrying only the\n * \"exceeded its memory limit\" fragment. Missing a surfacing here means the\n * circuit breaker never engages, so we err toward the broader match — and even a\n * false positive is fail-safe (a tightly-bounded retry-then-seal, not data loss).\n *\n * This is DELIBERATELY a separate class from `SUPERSEDED_ISOLATE_PATTERN` /\n * {@link isPlatformTransientError}, and is NOT folded into them. A supersede or\n * connection-lost transient means \"re-run the same work and it succeeds on a\n * healthy isolate\" — those classes can be deferred and retried *indefinitely*. A\n * memory-limit reset is the opposite: re-running the SAME memory-heavy work\n * deterministically re-OOMs (the footprint, not the platform, is the cause), so\n * deferring it indefinitely would PRESERVE the one-shot row and re-run the\n * doomed work forever (amplifying the loop and cost — see #1825). It is a\n * poison-pill signal: callers must bound retries tightly and then SEAL.\n *\n * Accordingly the schedule executor (`_executeScheduleCallback`) and the\n * alarm-boundary circuit breaker (`Agent.alarm`) treat it as its OWN class: a\n * memory-limit reset is re-thrown (row preserved) so it reaches the breaker,\n * which tolerates a few strikes (`maxAlarmMemoryLimitStrikes`) and then seals +\n * purges the looping row — i.e. *bounded* deferral, never the unbounded deferral\n * the transient classes get.\n */\nconst MEMORY_LIMIT_RESET_PATTERN = /exceeded its memory limit/i;\n\nfunction errorMessageOf(error: unknown): string {\n return error instanceof Error\n ? error.message\n : typeof error === \"string\"\n ? error\n : \"\";\n}\n\n/**\n * Iterate an error and its `cause` chain (depth-limited so a cyclic chain\n * can't spin). Wrappers like `SqlError` carry the original platform error in\n * `cause` and may not propagate signal properties (e.g. the CF `retryable`\n * flag), so classification must look through them.\n */\nfunction* selfAndCauses(error: unknown): Generator<unknown> {\n let current = error;\n for (let depth = 0; depth < 8 && current != null; depth++) {\n yield current;\n current =\n typeof current === \"object\"\n ? (current as { cause?: unknown }).cause\n : undefined;\n }\n}\n\n/**\n * Whether an error (or anything in its `cause` chain) is a transient\n * \"superseded isolate\" failure — see `SUPERSEDED_ISOLATE_PATTERN`. In-process\n * retries are futile for this class; the work must be deferred to a fresh\n * invocation, which runs the new code and succeeds.\n */\nexport function isDurableObjectCodeUpdateReset(error: unknown): boolean {\n for (const e of selfAndCauses(error)) {\n if (SUPERSEDED_ISOLATE_PATTERN.test(errorMessageOf(e))) return true;\n }\n return false;\n}\n\n/**\n * Whether an error (or anything in its `cause` chain) carries the exact\n * Durable Object storage-reset platform fragment. Generic SQL/internal errors\n * deliberately do not qualify.\n */\nexport function isDurableObjectStorageReset(error: unknown): boolean {\n for (const e of selfAndCauses(error)) {\n if (STORAGE_RESET_PATTERN.test(errorMessageOf(e))) return true;\n }\n return false;\n}\n\n/**\n * Whether an error (or anything in its `cause` chain, or a raw error-message\n * string) is a Durable Object memory-limit reset — see\n * {@link MEMORY_LIMIT_RESET_PATTERN}. Unlike {@link isPlatformTransientError},\n * re-running the same work re-OOMs deterministically, so callers must NOT defer\n * it like a transient; they should bound retries tightly and then seal (#1825).\n */\nexport function isDurableObjectMemoryLimitReset(error: unknown): boolean {\n for (const e of selfAndCauses(error)) {\n if (MEMORY_LIMIT_RESET_PATTERN.test(errorMessageOf(e))) return true;\n }\n return false;\n}\n\n/**\n * Whether an error (or anything in its `cause` chain) is a transient failure\n * of the PLATFORM rather than of the code that threw it:\n *\n * - a superseded-isolate reset (\"reset because its code was updated\" /\n * \"this script has been upgraded\") — a deploy replaced the isolate;\n * - an error the platform itself flags `retryable: true` (excluding\n * overloaded errors, where retrying the same object won't help) — see\n * `isErrorRetryable`;\n * - \"Network connection lost.\" — the storage/stub connection dropped. The\n * CF `retryable` flag does not survive error wrappers (e.g. `SqlError`\n * copies only the message + `cause`) and is absent in some local-dev\n * shapes, so the verbatim message is matched as well;\n * - the exact \"Internal error in Durable Object storage caused object to be\n * reset\" platform fragment. Generic internal and SQL errors remain fatal.\n *\n * Used to decide whether failed work should be RE-RUN LATER (platform\n * transient — the same work succeeds once the platform recovers, typically\n * seconds after a deploy) versus ABANDONED as genuinely failing (application\n * error — re-running yields the same failure). A genuine application error\n * carries none of these signals, so it is never misclassified by this check.\n */\n/**\n * Whether a failure is the PLATFORM's rather than the application's — any\n * platform transient (see {@link isPlatformTransientError}, which includes\n * superseded-isolate resets) or a memory-limit reset. Failed work in this\n * class must be PRESERVED and deferred, never completed as an application\n * failure. The two sub-classes defer differently: transients re-run\n * indefinitely (the platform recovers), while memory-limit deferral is\n * bounded by the alarm circuit breaker (#1825).\n */\nexport function isPlatformFailure(error: unknown): boolean {\n return (\n isPlatformTransientError(error) || isDurableObjectMemoryLimitReset(error)\n );\n}\n\nexport function isPlatformTransientError(error: unknown): boolean {\n for (const e of selfAndCauses(error)) {\n const message = errorMessageOf(e);\n if (SUPERSEDED_ISOLATE_PATTERN.test(message)) return true;\n if (CONNECTION_LOST_PATTERN.test(message)) return true;\n if (STORAGE_RESET_PATTERN.test(message)) return true;\n if (isErrorRetryable(e)) return true;\n }\n return false;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import {
|
|
2
|
+
r as CapabilityWebSocketUpgradeContext,
|
|
3
|
+
s as LifecycleCapability,
|
|
4
|
+
t as CapabilityRequestContext
|
|
5
|
+
} from "../capability-runner-Be_-PLR1.js";
|
|
6
|
+
import {
|
|
7
|
+
a as routeAgentRequest,
|
|
8
|
+
c as Agent,
|
|
9
|
+
i as getAgentByName,
|
|
10
|
+
n as AgentOptions,
|
|
11
|
+
r as RoutingRetryOptions,
|
|
12
|
+
t as AgentGetOptions
|
|
13
|
+
} from "../agent-routing-DE5zmCQ8.js";
|
|
14
|
+
|
|
15
|
+
//#region src/routing/routed-agents.d.ts
|
|
16
|
+
/** A public entry in an {@link RoutedAgents}. */
|
|
17
|
+
type RoutedAgentEntry<Metadata = unknown> = {
|
|
18
|
+
/** Stable application-facing identifier used in routes. */ readonly id: string /** Application-owned metadata stored with the entry. */;
|
|
19
|
+
readonly metadata: Metadata | null /** Creation time, as Unix milliseconds. */;
|
|
20
|
+
readonly createdAt: number /** Time the entry or its metadata last changed, as Unix milliseconds. */;
|
|
21
|
+
readonly updatedAt: number;
|
|
22
|
+
};
|
|
23
|
+
/** Options for creating an entry in an {@link RoutedAgents}. */
|
|
24
|
+
type RoutedAgentCreateOptions<Metadata = unknown> = {
|
|
25
|
+
/** Initial application-owned metadata. */ readonly metadata?: Metadata;
|
|
26
|
+
};
|
|
27
|
+
/** Configuration for an {@link RoutedAgents}. */
|
|
28
|
+
type RoutedAgentsOptions<TAgent extends Agent> = {
|
|
29
|
+
/** Top-level Durable Object namespace the entries are created in. */ readonly namespace: DurableObjectNamespace<TAgent> /** One URL-safe path segment under the owning Durable Object. */;
|
|
30
|
+
readonly route: string;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* A durable, routed collection of independent top-level Agents.
|
|
34
|
+
*
|
|
35
|
+
* Install this on the owning Durable Object, typically a per-user hub. It
|
|
36
|
+
* maps public entry IDs to opaque physical Agent names, handles catalog
|
|
37
|
+
* CRUD without waking any target, and forwards matching HTTP requests and
|
|
38
|
+
* WebSocket upgrades to the selected Agent. After an upgrade the target
|
|
39
|
+
* owns the socket, so ordinary frames never wake the owner. The target
|
|
40
|
+
* Agent needs no matching capability. Destroying the owner condemns every
|
|
41
|
+
* remaining entry with a few retries so targets don't casually outlive
|
|
42
|
+
* their catalog — this is best-effort, not a durability guarantee; see
|
|
43
|
+
* {@link RoutedAgents.dispose}.
|
|
44
|
+
*
|
|
45
|
+
* Pick a `route` that cannot appear as a literal path segment elsewhere
|
|
46
|
+
* under the owner (its own name, another route, or a path the owner's own
|
|
47
|
+
* `onRequest` handles) — forwarding matches every occurrence of the route
|
|
48
|
+
* segment in the path, so a coincidental match with no active entry
|
|
49
|
+
* behind it is answered `404` instead of reaching the owner.
|
|
50
|
+
*
|
|
51
|
+
* A forwarded suffix is not searched for a `/sub/{class}/{name}` dynamic
|
|
52
|
+
* agents marker: `Agent.fetch()` resolves that marker against the OWNER's
|
|
53
|
+
* exported classes before this capability's `onRequest` ever runs, so a
|
|
54
|
+
* matching marker is served as a facet of the owner, not forwarded to the
|
|
55
|
+
* target. Address a target's own dynamic agents through a direct
|
|
56
|
+
* connection to that target, not through the owner's route.
|
|
57
|
+
*
|
|
58
|
+
* @experimental The API surface may change before stabilizing.
|
|
59
|
+
*/
|
|
60
|
+
declare class RoutedAgents<
|
|
61
|
+
TAgent extends Agent = Agent,
|
|
62
|
+
Metadata = unknown
|
|
63
|
+
> extends LifecycleCapability {
|
|
64
|
+
#private;
|
|
65
|
+
/**
|
|
66
|
+
* @param options - Target binding and the route segment this capability
|
|
67
|
+
* claims. Install with `this.lifecycle.use()` before startup.
|
|
68
|
+
*/
|
|
69
|
+
constructor(options: RoutedAgentsOptions<TAgent>);
|
|
70
|
+
/** Create an entry without waking the target Agent. */
|
|
71
|
+
create(
|
|
72
|
+
options?: RoutedAgentCreateOptions<Metadata>
|
|
73
|
+
): Promise<RoutedAgentEntry<Metadata>>;
|
|
74
|
+
/** Resolve an active entry to an initialized, typed Agent stub. */
|
|
75
|
+
get(id: string): Promise<DurableObjectStub<TAgent> | null>;
|
|
76
|
+
/**
|
|
77
|
+
* List active entries, most recently updated first. Entries whose
|
|
78
|
+
* `updatedAt` ties are ordered by actual write order, not by the
|
|
79
|
+
* random entry `id`.
|
|
80
|
+
*/
|
|
81
|
+
list(): Promise<ReadonlyArray<RoutedAgentEntry<Metadata>>>;
|
|
82
|
+
/** Replace an active entry's metadata. Returns false for unknown IDs. */
|
|
83
|
+
setMetadata(id: string, metadata: Metadata | null): Promise<boolean>;
|
|
84
|
+
/**
|
|
85
|
+
* Make an entry unreachable, condemn its Agent, then remove the row.
|
|
86
|
+
* Returns false for unknown IDs.
|
|
87
|
+
*
|
|
88
|
+
* The target is condemned through Agent's deferred teardown, which
|
|
89
|
+
* durably marks it and returns without aborting the isolate; its storage
|
|
90
|
+
* is wiped on its own next wake, moments later, and the marker survives
|
|
91
|
+
* interruption. A failed RPC leaves a hidden `deleting` row so a
|
|
92
|
+
* repeated call retries.
|
|
93
|
+
*/
|
|
94
|
+
delete(id: string): Promise<boolean>;
|
|
95
|
+
onStart(): void;
|
|
96
|
+
/**
|
|
97
|
+
* Condemn every remaining entry (including one already `deleting`, in
|
|
98
|
+
* case its own condemnation RPC never landed) when the owner itself is
|
|
99
|
+
* destroyed.
|
|
100
|
+
*
|
|
101
|
+
* `Agent.destroy()` disposes capabilities before it wipes its own
|
|
102
|
+
* storage, so the catalog is still readable here — without this, the
|
|
103
|
+
* catalog would vanish with the owner while every target it named kept
|
|
104
|
+
* running and billing storage, unreachable forever.
|
|
105
|
+
*
|
|
106
|
+
* This is best-effort, not a durability guarantee: `Agent.destroy()`
|
|
107
|
+
* wipes the owner's storage immediately after disposal regardless of
|
|
108
|
+
* whether any capability's `dispose()` reports failure, so a target
|
|
109
|
+
* that is still unreachable after retries here is orphaned for good —
|
|
110
|
+
* there is no later "repeated call retries" for a catalog row that no
|
|
111
|
+
* longer exists. Retrying briefly here converts the common transient
|
|
112
|
+
* failure into a condemned target instead of an orphan; it cannot
|
|
113
|
+
* convert a target that is durably unreachable.
|
|
114
|
+
*/
|
|
115
|
+
dispose(): Promise<void>;
|
|
116
|
+
/** Forward a matching HTTP request to the selected Agent. */
|
|
117
|
+
onRequest({
|
|
118
|
+
request
|
|
119
|
+
}: CapabilityRequestContext): Promise<Response | undefined>;
|
|
120
|
+
/** Forward a matching upgrade so the selected Agent owns the WebSocket. */
|
|
121
|
+
onWebSocketUpgrade({
|
|
122
|
+
request
|
|
123
|
+
}: CapabilityWebSocketUpgradeContext): Promise<Response | undefined>;
|
|
124
|
+
}
|
|
125
|
+
//#endregion
|
|
126
|
+
export {
|
|
127
|
+
type AgentGetOptions,
|
|
128
|
+
type AgentOptions,
|
|
129
|
+
type RoutedAgentCreateOptions,
|
|
130
|
+
type RoutedAgentEntry,
|
|
131
|
+
RoutedAgents,
|
|
132
|
+
type RoutedAgentsOptions,
|
|
133
|
+
type RoutingRetryOptions,
|
|
134
|
+
getAgentByName,
|
|
135
|
+
routeAgentRequest
|
|
136
|
+
};
|
|
137
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { getAgentByName, routeAgentRequest } from "../agent-routing.js";
|
|
2
|
+
import { i as _classPrivateFieldInitSpec, n as _classPrivateFieldSet2, r as _assertClassBrand, t as _classPrivateFieldGet2 } from "../classPrivateFieldGet2-DZBYAB34.js";
|
|
3
|
+
import { t as LifecycleCapability } from "../capability-BjSKYpzg.js";
|
|
4
|
+
import { t as _classPrivateMethodInitSpec } from "../classPrivateMethodInitSpec-qMjJ6sHQ.js";
|
|
5
|
+
//#region src/routing/routed-agents.ts
|
|
6
|
+
/**
|
|
7
|
+
* Catalog of the entries an owning Durable Object has created, keyed by
|
|
8
|
+
* route so several namespaces can share one owner. `WITHOUT ROWID` keeps
|
|
9
|
+
* an insert at one billed row; `agent_name` is a random UUID, so it needs
|
|
10
|
+
* no unique index of its own.
|
|
11
|
+
*/
|
|
12
|
+
const TABLE = "cf_agents_routed_agents";
|
|
13
|
+
/**
|
|
14
|
+
* Derives the next per-route sequence number from a `MAX(seq)` read
|
|
15
|
+
* instead of a maintained counter row. Breaks ties between equal
|
|
16
|
+
* `Date.now()` values deterministically by write order, which a random
|
|
17
|
+
* entry `id` cannot: DO SQLite millisecond timestamps collide easily
|
|
18
|
+
* under rapid same-route writes.
|
|
19
|
+
*
|
|
20
|
+
* This scans every row for the route on each create()/setMetadata() —
|
|
21
|
+
* intentionally, not a missed index. DO SQLite bills roughly 1000
|
|
22
|
+
* writes for the cost of 1000 reads, so a maintained counter row (an
|
|
23
|
+
* extra write on every call) only wins at deep four-figure entries per
|
|
24
|
+
* route; a `(route, seq)` index would cost an extra write on every call
|
|
25
|
+
* too, since `seq` changes on every write it would index. `RoutedAgents`
|
|
26
|
+
* targets one owner's own catalog (chats, documents, sessions) — for a
|
|
27
|
+
* route expected to hold thousands of entries, benchmark before relying
|
|
28
|
+
* on this ordering; it is not built for that scale.
|
|
29
|
+
*/
|
|
30
|
+
const NEXT_SEQ = `(SELECT COALESCE(MAX(seq), 0) + 1 FROM ${TABLE} WHERE route = ?)`;
|
|
31
|
+
function encodeMetadata(value) {
|
|
32
|
+
const encoded = JSON.stringify(value ?? null);
|
|
33
|
+
if (encoded === void 0) throw new TypeError("RoutedAgents metadata must be JSON-serializable");
|
|
34
|
+
return encoded;
|
|
35
|
+
}
|
|
36
|
+
var _namespace = /* @__PURE__ */ new WeakMap();
|
|
37
|
+
var _route = /* @__PURE__ */ new WeakMap();
|
|
38
|
+
var _RoutedAgents_brand = /* @__PURE__ */ new WeakSet();
|
|
39
|
+
/**
|
|
40
|
+
* A durable, routed collection of independent top-level Agents.
|
|
41
|
+
*
|
|
42
|
+
* Install this on the owning Durable Object, typically a per-user hub. It
|
|
43
|
+
* maps public entry IDs to opaque physical Agent names, handles catalog
|
|
44
|
+
* CRUD without waking any target, and forwards matching HTTP requests and
|
|
45
|
+
* WebSocket upgrades to the selected Agent. After an upgrade the target
|
|
46
|
+
* owns the socket, so ordinary frames never wake the owner. The target
|
|
47
|
+
* Agent needs no matching capability. Destroying the owner condemns every
|
|
48
|
+
* remaining entry with a few retries so targets don't casually outlive
|
|
49
|
+
* their catalog — this is best-effort, not a durability guarantee; see
|
|
50
|
+
* {@link RoutedAgents.dispose}.
|
|
51
|
+
*
|
|
52
|
+
* Pick a `route` that cannot appear as a literal path segment elsewhere
|
|
53
|
+
* under the owner (its own name, another route, or a path the owner's own
|
|
54
|
+
* `onRequest` handles) — forwarding matches every occurrence of the route
|
|
55
|
+
* segment in the path, so a coincidental match with no active entry
|
|
56
|
+
* behind it is answered `404` instead of reaching the owner.
|
|
57
|
+
*
|
|
58
|
+
* A forwarded suffix is not searched for a `/sub/{class}/{name}` dynamic
|
|
59
|
+
* agents marker: `Agent.fetch()` resolves that marker against the OWNER's
|
|
60
|
+
* exported classes before this capability's `onRequest` ever runs, so a
|
|
61
|
+
* matching marker is served as a facet of the owner, not forwarded to the
|
|
62
|
+
* target. Address a target's own dynamic agents through a direct
|
|
63
|
+
* connection to that target, not through the owner's route.
|
|
64
|
+
*
|
|
65
|
+
* @experimental The API surface may change before stabilizing.
|
|
66
|
+
*/
|
|
67
|
+
var RoutedAgents = class extends LifecycleCapability {
|
|
68
|
+
/**
|
|
69
|
+
* @param options - Target binding and the route segment this capability
|
|
70
|
+
* claims. Install with `this.lifecycle.use()` before startup.
|
|
71
|
+
*/
|
|
72
|
+
constructor(options) {
|
|
73
|
+
const route = options.route.replace(/^\/+|\/+$/g, "");
|
|
74
|
+
if (!/^[A-Za-z0-9_-]+$/.test(route)) throw new Error("RoutedAgents route must be one non-empty URL-safe path segment");
|
|
75
|
+
super(`routed-agents:${route}`);
|
|
76
|
+
_classPrivateMethodInitSpec(this, _RoutedAgents_brand);
|
|
77
|
+
_classPrivateFieldInitSpec(this, _namespace, void 0);
|
|
78
|
+
_classPrivateFieldInitSpec(this, _route, void 0);
|
|
79
|
+
_classPrivateFieldSet2(_namespace, this, options.namespace);
|
|
80
|
+
_classPrivateFieldSet2(_route, this, route);
|
|
81
|
+
}
|
|
82
|
+
/** Create an entry without waking the target Agent. */
|
|
83
|
+
async create(options) {
|
|
84
|
+
await this.lifecycle.ready();
|
|
85
|
+
const id = crypto.randomUUID();
|
|
86
|
+
const encoded = encodeMetadata(options?.metadata ?? null);
|
|
87
|
+
const now = Date.now();
|
|
88
|
+
_assertClassBrand(_RoutedAgents_brand, this, _sql).call(this, `INSERT INTO ${TABLE} (route, id, agent_name, status, metadata, created_at, updated_at, seq)
|
|
89
|
+
VALUES (?, ?, ?, 'active', ?, ?, ?, ${NEXT_SEQ})`, _classPrivateFieldGet2(_route, this), id, crypto.randomUUID(), encoded, now, now, _classPrivateFieldGet2(_route, this));
|
|
90
|
+
return {
|
|
91
|
+
id,
|
|
92
|
+
metadata: JSON.parse(encoded),
|
|
93
|
+
createdAt: now,
|
|
94
|
+
updatedAt: now
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/** Resolve an active entry to an initialized, typed Agent stub. */
|
|
98
|
+
async get(id) {
|
|
99
|
+
await this.lifecycle.ready();
|
|
100
|
+
const agentName = _assertClassBrand(_RoutedAgents_brand, this, _agentName).call(this, id, "active");
|
|
101
|
+
return agentName ? _assertClassBrand(_RoutedAgents_brand, this, _stub).call(this, agentName) : null;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* List active entries, most recently updated first. Entries whose
|
|
105
|
+
* `updatedAt` ties are ordered by actual write order, not by the
|
|
106
|
+
* random entry `id`.
|
|
107
|
+
*/
|
|
108
|
+
async list() {
|
|
109
|
+
await this.lifecycle.ready();
|
|
110
|
+
return _assertClassBrand(_RoutedAgents_brand, this, _sql).call(this, `SELECT id, metadata, created_at AS createdAt, updated_at AS updatedAt
|
|
111
|
+
FROM ${TABLE} WHERE route = ? AND status = 'active'
|
|
112
|
+
ORDER BY updated_at DESC, seq DESC, id ASC`, _classPrivateFieldGet2(_route, this)).map((row) => ({
|
|
113
|
+
...row,
|
|
114
|
+
metadata: JSON.parse(row.metadata)
|
|
115
|
+
}));
|
|
116
|
+
}
|
|
117
|
+
/** Replace an active entry's metadata. Returns false for unknown IDs. */
|
|
118
|
+
async setMetadata(id, metadata) {
|
|
119
|
+
await this.lifecycle.ready();
|
|
120
|
+
return _assertClassBrand(_RoutedAgents_brand, this, _sql).call(this, `UPDATE ${TABLE} SET metadata = ?, updated_at = ?, seq = ${NEXT_SEQ}
|
|
121
|
+
WHERE route = ? AND id = ? AND status = 'active' RETURNING id`, encodeMetadata(metadata), Date.now(), _classPrivateFieldGet2(_route, this), _classPrivateFieldGet2(_route, this), id).length > 0;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Make an entry unreachable, condemn its Agent, then remove the row.
|
|
125
|
+
* Returns false for unknown IDs.
|
|
126
|
+
*
|
|
127
|
+
* The target is condemned through Agent's deferred teardown, which
|
|
128
|
+
* durably marks it and returns without aborting the isolate; its storage
|
|
129
|
+
* is wiped on its own next wake, moments later, and the marker survives
|
|
130
|
+
* interruption. A failed RPC leaves a hidden `deleting` row so a
|
|
131
|
+
* repeated call retries.
|
|
132
|
+
*/
|
|
133
|
+
async delete(id) {
|
|
134
|
+
await this.lifecycle.ready();
|
|
135
|
+
const agentName = _assertClassBrand(_RoutedAgents_brand, this, _agentName).call(this, id);
|
|
136
|
+
if (!agentName) return false;
|
|
137
|
+
_assertClassBrand(_RoutedAgents_brand, this, _sql).call(this, `UPDATE ${TABLE} SET status = 'deleting', updated_at = ?
|
|
138
|
+
WHERE route = ? AND id = ?`, Date.now(), _classPrivateFieldGet2(_route, this), id);
|
|
139
|
+
await _classPrivateFieldGet2(_namespace, this).get(_classPrivateFieldGet2(_namespace, this).idFromName(agentName))._cf_scheduleDestroy();
|
|
140
|
+
_assertClassBrand(_RoutedAgents_brand, this, _sql).call(this, `DELETE FROM ${TABLE} WHERE route = ? AND id = ?`, _classPrivateFieldGet2(_route, this), id);
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
onStart() {
|
|
144
|
+
_assertClassBrand(_RoutedAgents_brand, this, _sql).call(this, `CREATE TABLE IF NOT EXISTS ${TABLE} (
|
|
145
|
+
route TEXT NOT NULL,
|
|
146
|
+
id TEXT NOT NULL,
|
|
147
|
+
agent_name TEXT NOT NULL,
|
|
148
|
+
status TEXT NOT NULL CHECK (status IN ('active', 'deleting')),
|
|
149
|
+
metadata TEXT NOT NULL,
|
|
150
|
+
created_at INTEGER NOT NULL,
|
|
151
|
+
updated_at INTEGER NOT NULL,
|
|
152
|
+
seq INTEGER NOT NULL DEFAULT 0,
|
|
153
|
+
PRIMARY KEY (route, id)
|
|
154
|
+
) WITHOUT ROWID`);
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Condemn every remaining entry (including one already `deleting`, in
|
|
158
|
+
* case its own condemnation RPC never landed) when the owner itself is
|
|
159
|
+
* destroyed.
|
|
160
|
+
*
|
|
161
|
+
* `Agent.destroy()` disposes capabilities before it wipes its own
|
|
162
|
+
* storage, so the catalog is still readable here — without this, the
|
|
163
|
+
* catalog would vanish with the owner while every target it named kept
|
|
164
|
+
* running and billing storage, unreachable forever.
|
|
165
|
+
*
|
|
166
|
+
* This is best-effort, not a durability guarantee: `Agent.destroy()`
|
|
167
|
+
* wipes the owner's storage immediately after disposal regardless of
|
|
168
|
+
* whether any capability's `dispose()` reports failure, so a target
|
|
169
|
+
* that is still unreachable after retries here is orphaned for good —
|
|
170
|
+
* there is no later "repeated call retries" for a catalog row that no
|
|
171
|
+
* longer exists. Retrying briefly here converts the common transient
|
|
172
|
+
* failure into a condemned target instead of an orphan; it cannot
|
|
173
|
+
* convert a target that is durably unreachable.
|
|
174
|
+
*/
|
|
175
|
+
async dispose() {
|
|
176
|
+
const entries = _assertClassBrand(_RoutedAgents_brand, this, _sql).call(this, `SELECT agent_name AS agentName FROM ${TABLE}
|
|
177
|
+
WHERE route = ? AND status IN ('active', 'deleting')`, _classPrivateFieldGet2(_route, this));
|
|
178
|
+
await Promise.all(entries.map(({ agentName }) => _assertClassBrand(_RoutedAgents_brand, this, _condemnWithRetry).call(this, agentName)));
|
|
179
|
+
}
|
|
180
|
+
/** Forward a matching HTTP request to the selected Agent. */
|
|
181
|
+
onRequest({ request }) {
|
|
182
|
+
return _assertClassBrand(_RoutedAgents_brand, this, _forward).call(this, request);
|
|
183
|
+
}
|
|
184
|
+
/** Forward a matching upgrade so the selected Agent owns the WebSocket. */
|
|
185
|
+
onWebSocketUpgrade({ request }) {
|
|
186
|
+
return _assertClassBrand(_RoutedAgents_brand, this, _forward).call(this, request);
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
async function _condemnWithRetry(agentName, attempts = 3) {
|
|
190
|
+
for (let attempt = 1; attempt <= attempts; attempt++) try {
|
|
191
|
+
await _classPrivateFieldGet2(_namespace, this).get(_classPrivateFieldGet2(_namespace, this).idFromName(agentName))._cf_scheduleDestroy();
|
|
192
|
+
return;
|
|
193
|
+
} catch (error) {
|
|
194
|
+
if (attempt === attempts) {
|
|
195
|
+
console.error(`RoutedAgents "${_classPrivateFieldGet2(_route, this)}" could not condemn ${agentName} on owner disposal after ${attempts} attempts; its storage will leak, since the owner's catalog — the only record of it — is wiped immediately after disposal`, error);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
await new Promise((resolve) => setTimeout(resolve, attempt * 50));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* The route segment may also appear as the owner's own name or inside
|
|
203
|
+
* the forwarded suffix, so every `/{route}/{id}` occurrence is tried
|
|
204
|
+
* against the catalog and the first active entry wins. A route match
|
|
205
|
+
* with no active entry is a 404; no match at all lets the request
|
|
206
|
+
* continue to the owner's other capabilities.
|
|
207
|
+
*/
|
|
208
|
+
async function _forward(request) {
|
|
209
|
+
const url = new URL(request.url);
|
|
210
|
+
const segments = url.pathname.split("/");
|
|
211
|
+
let matched = false;
|
|
212
|
+
for (let i = 1; i < segments.length - 1; i++) {
|
|
213
|
+
if (segments[i] !== _classPrivateFieldGet2(_route, this) || segments[i + 1] === "") continue;
|
|
214
|
+
matched = true;
|
|
215
|
+
const agentName = _assertClassBrand(_RoutedAgents_brand, this, _agentName).call(this, decode(segments[i + 1]), "active");
|
|
216
|
+
if (!agentName) continue;
|
|
217
|
+
url.pathname = `/${segments.slice(i + 2).join("/")}`;
|
|
218
|
+
return _classPrivateFieldGet2(_namespace, this).get(_classPrivateFieldGet2(_namespace, this).idFromName(agentName)).fetch(new Request(url, request));
|
|
219
|
+
}
|
|
220
|
+
return matched ? new Response("Agent not found", { status: 404 }) : void 0;
|
|
221
|
+
}
|
|
222
|
+
/** Initialized stub; the explicit generics keep inference shallow. */
|
|
223
|
+
function _stub(agentName) {
|
|
224
|
+
return getAgentByName(_classPrivateFieldGet2(_namespace, this), agentName);
|
|
225
|
+
}
|
|
226
|
+
function _agentName(id, status) {
|
|
227
|
+
const [row] = _assertClassBrand(_RoutedAgents_brand, this, _sql).call(this, `SELECT agent_name AS agentName FROM ${TABLE}
|
|
228
|
+
WHERE route = ? AND id = ? AND status = COALESCE(?, status)`, _classPrivateFieldGet2(_route, this), id, status ?? null);
|
|
229
|
+
return row?.agentName;
|
|
230
|
+
}
|
|
231
|
+
function _sql(query, ...values) {
|
|
232
|
+
return this.lifecycle.storage.sql.exec(query, ...values).toArray();
|
|
233
|
+
}
|
|
234
|
+
function decode(segment) {
|
|
235
|
+
try {
|
|
236
|
+
return decodeURIComponent(segment);
|
|
237
|
+
} catch {
|
|
238
|
+
return segment;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
//#endregion
|
|
242
|
+
export { RoutedAgents, getAgentByName, routeAgentRequest };
|
|
243
|
+
|
|
244
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/routing/routed-agents.ts"],"sourcesContent":["import { getAgentByName } from \"../agent-routing\";\nimport type { Agent } from \"../index\";\nimport { LifecycleCapability } from \"../lifecycle/capability\";\nimport type {\n CapabilityRequestContext,\n CapabilityWebSocketUpgradeContext\n} from \"../lifecycle/capability-runner\";\n\n/**\n * Catalog of the entries an owning Durable Object has created, keyed by\n * route so several namespaces can share one owner. `WITHOUT ROWID` keeps\n * an insert at one billed row; `agent_name` is a random UUID, so it needs\n * no unique index of its own.\n */\nconst TABLE = \"cf_agents_routed_agents\";\n\n/**\n * Derives the next per-route sequence number from a `MAX(seq)` read\n * instead of a maintained counter row. Breaks ties between equal\n * `Date.now()` values deterministically by write order, which a random\n * entry `id` cannot: DO SQLite millisecond timestamps collide easily\n * under rapid same-route writes.\n *\n * This scans every row for the route on each create()/setMetadata() —\n * intentionally, not a missed index. DO SQLite bills roughly 1000\n * writes for the cost of 1000 reads, so a maintained counter row (an\n * extra write on every call) only wins at deep four-figure entries per\n * route; a `(route, seq)` index would cost an extra write on every call\n * too, since `seq` changes on every write it would index. `RoutedAgents`\n * targets one owner's own catalog (chats, documents, sessions) — for a\n * route expected to hold thousands of entries, benchmark before relying\n * on this ordering; it is not built for that scale.\n */\nconst NEXT_SEQ = `(SELECT COALESCE(MAX(seq), 0) + 1 FROM ${TABLE} WHERE route = ?)`;\n\n/** A public entry in an {@link RoutedAgents}. */\nexport type RoutedAgentEntry<Metadata = unknown> = {\n /** Stable application-facing identifier used in routes. */\n readonly id: string;\n /** Application-owned metadata stored with the entry. */\n readonly metadata: Metadata | null;\n /** Creation time, as Unix milliseconds. */\n readonly createdAt: number;\n /** Time the entry or its metadata last changed, as Unix milliseconds. */\n readonly updatedAt: number;\n};\n\n/** Options for creating an entry in an {@link RoutedAgents}. */\nexport type RoutedAgentCreateOptions<Metadata = unknown> = {\n /** Initial application-owned metadata. */\n readonly metadata?: Metadata;\n};\n\n/** Configuration for an {@link RoutedAgents}. */\nexport type RoutedAgentsOptions<TAgent extends Agent> = {\n /** Top-level Durable Object namespace the entries are created in. */\n readonly namespace: DurableObjectNamespace<TAgent>;\n /** One URL-safe path segment under the owning Durable Object. */\n readonly route: string;\n};\n\ntype EntryRow = {\n id: string;\n metadata: string;\n createdAt: number;\n updatedAt: number;\n};\n\nfunction encodeMetadata(value: unknown): string {\n const encoded = JSON.stringify(value ?? null);\n if (encoded === undefined) {\n throw new TypeError(\"RoutedAgents metadata must be JSON-serializable\");\n }\n return encoded;\n}\n\n/**\n * A durable, routed collection of independent top-level Agents.\n *\n * Install this on the owning Durable Object, typically a per-user hub. It\n * maps public entry IDs to opaque physical Agent names, handles catalog\n * CRUD without waking any target, and forwards matching HTTP requests and\n * WebSocket upgrades to the selected Agent. After an upgrade the target\n * owns the socket, so ordinary frames never wake the owner. The target\n * Agent needs no matching capability. Destroying the owner condemns every\n * remaining entry with a few retries so targets don't casually outlive\n * their catalog — this is best-effort, not a durability guarantee; see\n * {@link RoutedAgents.dispose}.\n *\n * Pick a `route` that cannot appear as a literal path segment elsewhere\n * under the owner (its own name, another route, or a path the owner's own\n * `onRequest` handles) — forwarding matches every occurrence of the route\n * segment in the path, so a coincidental match with no active entry\n * behind it is answered `404` instead of reaching the owner.\n *\n * A forwarded suffix is not searched for a `/sub/{class}/{name}` dynamic\n * agents marker: `Agent.fetch()` resolves that marker against the OWNER's\n * exported classes before this capability's `onRequest` ever runs, so a\n * matching marker is served as a facet of the owner, not forwarded to the\n * target. Address a target's own dynamic agents through a direct\n * connection to that target, not through the owner's route.\n *\n * @experimental The API surface may change before stabilizing.\n */\nexport class RoutedAgents<\n TAgent extends Agent = Agent,\n Metadata = unknown\n> extends LifecycleCapability {\n readonly #namespace: DurableObjectNamespace<TAgent>;\n readonly #route: string;\n\n /**\n * @param options - Target binding and the route segment this capability\n * claims. Install with `this.lifecycle.use()` before startup.\n */\n constructor(options: RoutedAgentsOptions<TAgent>) {\n const route = options.route.replace(/^\\/+|\\/+$/g, \"\");\n if (!/^[A-Za-z0-9_-]+$/.test(route)) {\n throw new Error(\n \"RoutedAgents route must be one non-empty URL-safe path segment\"\n );\n }\n super(`routed-agents:${route}`);\n this.#namespace = options.namespace;\n this.#route = route;\n }\n\n /** Create an entry without waking the target Agent. */\n async create(\n options?: RoutedAgentCreateOptions<Metadata>\n ): Promise<RoutedAgentEntry<Metadata>> {\n await this.lifecycle.ready();\n const id = crypto.randomUUID();\n const encoded = encodeMetadata(options?.metadata ?? null);\n const now = Date.now();\n this.#sql(\n `INSERT INTO ${TABLE} (route, id, agent_name, status, metadata, created_at, updated_at, seq)\n VALUES (?, ?, ?, 'active', ?, ?, ?, ${NEXT_SEQ})`,\n this.#route,\n id,\n crypto.randomUUID(),\n encoded,\n now,\n now,\n this.#route\n );\n // Round-trip through JSON so this agrees with list()'s decoded copy —\n // returning the caller's object verbatim would diverge for values JSON\n // can't represent exactly (undefined fields, NaN, non-plain objects).\n return {\n id,\n metadata: JSON.parse(encoded) as Metadata | null,\n createdAt: now,\n updatedAt: now\n };\n }\n\n /** Resolve an active entry to an initialized, typed Agent stub. */\n async get(id: string): Promise<DurableObjectStub<TAgent> | null> {\n await this.lifecycle.ready();\n const agentName = this.#agentName(id, \"active\");\n return agentName ? this.#stub(agentName) : null;\n }\n\n /**\n * List active entries, most recently updated first. Entries whose\n * `updatedAt` ties are ordered by actual write order, not by the\n * random entry `id`.\n */\n async list(): Promise<ReadonlyArray<RoutedAgentEntry<Metadata>>> {\n await this.lifecycle.ready();\n return this.#sql<EntryRow>(\n `SELECT id, metadata, created_at AS createdAt, updated_at AS updatedAt\n FROM ${TABLE} WHERE route = ? AND status = 'active'\n ORDER BY updated_at DESC, seq DESC, id ASC`,\n this.#route\n ).map((row) => ({\n ...row,\n // SAFETY: written by create() or setMetadata() from a Metadata value.\n // Changing Metadata for an existing namespace needs an application\n // migration; JSON carries no type to recover.\n metadata: JSON.parse(row.metadata) as Metadata | null\n }));\n }\n\n /** Replace an active entry's metadata. Returns false for unknown IDs. */\n async setMetadata(id: string, metadata: Metadata | null): Promise<boolean> {\n await this.lifecycle.ready();\n return (\n this.#sql(\n `UPDATE ${TABLE} SET metadata = ?, updated_at = ?, seq = ${NEXT_SEQ}\n WHERE route = ? AND id = ? AND status = 'active' RETURNING id`,\n encodeMetadata(metadata),\n Date.now(),\n this.#route,\n this.#route,\n id\n ).length > 0\n );\n }\n\n /**\n * Make an entry unreachable, condemn its Agent, then remove the row.\n * Returns false for unknown IDs.\n *\n * The target is condemned through Agent's deferred teardown, which\n * durably marks it and returns without aborting the isolate; its storage\n * is wiped on its own next wake, moments later, and the marker survives\n * interruption. A failed RPC leaves a hidden `deleting` row so a\n * repeated call retries.\n */\n async delete(id: string): Promise<boolean> {\n await this.lifecycle.ready();\n const agentName = this.#agentName(id);\n if (!agentName) return false;\n this.#sql(\n `UPDATE ${TABLE} SET status = 'deleting', updated_at = ?\n WHERE route = ? AND id = ?`,\n Date.now(),\n this.#route,\n id\n );\n await this.#namespace\n .get(this.#namespace.idFromName(agentName))\n ._cf_scheduleDestroy();\n this.#sql(\n `DELETE FROM ${TABLE} WHERE route = ? AND id = ?`,\n this.#route,\n id\n );\n return true;\n }\n\n override onStart(): void {\n this.#sql(`CREATE TABLE IF NOT EXISTS ${TABLE} (\n route TEXT NOT NULL,\n id TEXT NOT NULL,\n agent_name TEXT NOT NULL,\n status TEXT NOT NULL CHECK (status IN ('active', 'deleting')),\n metadata TEXT NOT NULL,\n created_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL,\n seq INTEGER NOT NULL DEFAULT 0,\n PRIMARY KEY (route, id)\n ) WITHOUT ROWID`);\n }\n\n /**\n * Condemn every remaining entry (including one already `deleting`, in\n * case its own condemnation RPC never landed) when the owner itself is\n * destroyed.\n *\n * `Agent.destroy()` disposes capabilities before it wipes its own\n * storage, so the catalog is still readable here — without this, the\n * catalog would vanish with the owner while every target it named kept\n * running and billing storage, unreachable forever.\n *\n * This is best-effort, not a durability guarantee: `Agent.destroy()`\n * wipes the owner's storage immediately after disposal regardless of\n * whether any capability's `dispose()` reports failure, so a target\n * that is still unreachable after retries here is orphaned for good —\n * there is no later \"repeated call retries\" for a catalog row that no\n * longer exists. Retrying briefly here converts the common transient\n * failure into a condemned target instead of an orphan; it cannot\n * convert a target that is durably unreachable.\n */\n async dispose(): Promise<void> {\n const entries = this.#sql<{ agentName: string }>(\n `SELECT agent_name AS agentName FROM ${TABLE}\n WHERE route = ? AND status IN ('active', 'deleting')`,\n this.#route\n );\n await Promise.all(\n entries.map(({ agentName }) => this.#condemnWithRetry(agentName))\n );\n }\n\n async #condemnWithRetry(agentName: string, attempts = 3): Promise<void> {\n for (let attempt = 1; attempt <= attempts; attempt++) {\n try {\n await this.#namespace\n .get(this.#namespace.idFromName(agentName))\n ._cf_scheduleDestroy();\n return;\n } catch (error) {\n if (attempt === attempts) {\n console.error(\n `RoutedAgents \"${this.#route}\" could not condemn ${agentName} on owner disposal after ${attempts} attempts; its storage will leak, since the owner's catalog — the only record of it — is wiped immediately after disposal`,\n error\n );\n return;\n }\n await new Promise((resolve) => setTimeout(resolve, attempt * 50));\n }\n }\n }\n\n /** Forward a matching HTTP request to the selected Agent. */\n onRequest({\n request\n }: CapabilityRequestContext): Promise<Response | undefined> {\n return this.#forward(request);\n }\n\n /** Forward a matching upgrade so the selected Agent owns the WebSocket. */\n onWebSocketUpgrade({\n request\n }: CapabilityWebSocketUpgradeContext): Promise<Response | undefined> {\n return this.#forward(request);\n }\n\n /**\n * The route segment may also appear as the owner's own name or inside\n * the forwarded suffix, so every `/{route}/{id}` occurrence is tried\n * against the catalog and the first active entry wins. A route match\n * with no active entry is a 404; no match at all lets the request\n * continue to the owner's other capabilities.\n */\n async #forward(request: Request): Promise<Response | undefined> {\n const url = new URL(request.url);\n const segments = url.pathname.split(\"/\");\n let matched = false;\n for (let i = 1; i < segments.length - 1; i++) {\n if (segments[i] !== this.#route || segments[i + 1] === \"\") continue;\n matched = true;\n const agentName = this.#agentName(decode(segments[i + 1]), \"active\");\n if (!agentName) continue;\n url.pathname = `/${segments.slice(i + 2).join(\"/\")}`;\n return this.#namespace\n .get(this.#namespace.idFromName(agentName))\n .fetch(new Request(url, request));\n }\n return matched\n ? new Response(\"Agent not found\", { status: 404 })\n : undefined;\n }\n\n /** Initialized stub; the explicit generics keep inference shallow. */\n #stub(agentName: string): Promise<DurableObjectStub<TAgent>> {\n return getAgentByName<Cloudflare.Env, TAgent>(this.#namespace, agentName);\n }\n\n #agentName(id: string, status?: \"active\"): string | undefined {\n const [row] = this.#sql<{ agentName: string }>(\n `SELECT agent_name AS agentName FROM ${TABLE}\n WHERE route = ? AND id = ? AND status = COALESCE(?, status)`,\n this.#route,\n id,\n status ?? null\n );\n return row?.agentName;\n }\n\n #sql<Row extends Record<string, SqlStorageValue>>(\n query: string,\n ...values: SqlStorageValue[]\n ): Row[] {\n return this.lifecycle.storage.sql.exec<Row>(query, ...values).toArray();\n }\n}\n\nfunction decode(segment: string): string {\n try {\n return decodeURIComponent(segment);\n } catch {\n return segment;\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
//#region src/sessions/sanitize.ts
|
|
2
|
+
/**
|
|
3
|
+
* Message hygiene shared by every Sessions write: UTF-8 measurement without a
|
|
4
|
+
* full encoded copy, and removal of ephemeral provider metadata that must not
|
|
5
|
+
* be stored or replayed.
|
|
6
|
+
*/
|
|
7
|
+
const textEncoder = new TextEncoder();
|
|
8
|
+
const BYTE_LENGTH_BUFFER_BYTES = 16 * 1024;
|
|
9
|
+
const BYTE_LENGTH_WINDOW_CHARS = 16 * 1024;
|
|
10
|
+
/**
|
|
11
|
+
* Measure UTF-8 byte length without allocating a complete encoded copy.
|
|
12
|
+
* Memory stays bounded by a 16 KiB buffer even for near-row-limit strings.
|
|
13
|
+
*/
|
|
14
|
+
function byteLength(s) {
|
|
15
|
+
const buffer = new Uint8Array(BYTE_LENGTH_BUFFER_BYTES);
|
|
16
|
+
let offset = 0;
|
|
17
|
+
let bytes = 0;
|
|
18
|
+
while (offset < s.length) {
|
|
19
|
+
let end = Math.min(s.length, offset + BYTE_LENGTH_WINDOW_CHARS);
|
|
20
|
+
if (end < s.length && end > offset && isHighSurrogate(s.charCodeAt(end - 1))) end--;
|
|
21
|
+
const { read, written } = textEncoder.encodeInto(s.slice(offset, end), buffer);
|
|
22
|
+
if (read === 0) break;
|
|
23
|
+
offset += read;
|
|
24
|
+
bytes += written;
|
|
25
|
+
}
|
|
26
|
+
return bytes;
|
|
27
|
+
}
|
|
28
|
+
function isHighSurrogate(code) {
|
|
29
|
+
return code >= 55296 && code <= 56319;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Sanitize a message for persistence by removing ephemeral provider-specific
|
|
33
|
+
* data that should not be stored or sent back in subsequent requests.
|
|
34
|
+
*
|
|
35
|
+
* 1. Strips OpenAI ephemeral fields (itemId, reasoningEncryptedContent)
|
|
36
|
+
* 2. Filters truly empty reasoning parts (no text, no remaining providerMetadata)
|
|
37
|
+
*/
|
|
38
|
+
function sanitizeMessage(message) {
|
|
39
|
+
const parts = message.parts.flatMap((part) => {
|
|
40
|
+
let sanitized = part;
|
|
41
|
+
for (const key of ["providerMetadata", "callProviderMetadata"]) {
|
|
42
|
+
const metadata = sanitized[key];
|
|
43
|
+
if (metadata && typeof metadata === "object" && "openai" in metadata) sanitized = stripOpenAIMetadata(sanitized, key);
|
|
44
|
+
}
|
|
45
|
+
if (sanitized.type === "reasoning") {
|
|
46
|
+
const text = sanitized.text;
|
|
47
|
+
if (typeof text !== "string" || text.trim() === "") {
|
|
48
|
+
const metadata = sanitized.providerMetadata;
|
|
49
|
+
if (!(metadata && typeof metadata === "object" && Object.keys(metadata).length > 0)) return [];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return [sanitized];
|
|
53
|
+
});
|
|
54
|
+
return {
|
|
55
|
+
...message,
|
|
56
|
+
parts
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function stripOpenAIMetadata(part, metadataKey) {
|
|
60
|
+
const metadata = part[metadataKey];
|
|
61
|
+
if (!metadata?.openai) return part;
|
|
62
|
+
const { itemId: _itemId, reasoningEncryptedContent: _rec, ...restOpenai } = metadata.openai;
|
|
63
|
+
const { openai: _openai, ...restMetadata } = metadata;
|
|
64
|
+
let next;
|
|
65
|
+
if (Object.keys(restOpenai).length > 0) next = {
|
|
66
|
+
...restMetadata,
|
|
67
|
+
openai: restOpenai
|
|
68
|
+
};
|
|
69
|
+
else if (Object.keys(restMetadata).length > 0) next = restMetadata;
|
|
70
|
+
const { [metadataKey]: _old, ...rest } = part;
|
|
71
|
+
return next ? {
|
|
72
|
+
...rest,
|
|
73
|
+
[metadataKey]: next
|
|
74
|
+
} : rest;
|
|
75
|
+
}
|
|
76
|
+
//#endregion
|
|
77
|
+
export { sanitizeMessage as n, byteLength as t };
|
|
78
|
+
|
|
79
|
+
//# sourceMappingURL=sanitize-D9TujEK8.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sanitize-D9TujEK8.js","names":[],"sources":["../src/sessions/sanitize.ts"],"sourcesContent":["/**\n * Message hygiene shared by every Sessions write: UTF-8 measurement without a\n * full encoded copy, and removal of ephemeral provider metadata that must not\n * be stored or replayed.\n */\n\nconst textEncoder = new TextEncoder();\nconst BYTE_LENGTH_BUFFER_BYTES = 16 * 1024;\nconst BYTE_LENGTH_WINDOW_CHARS = 16 * 1024;\n\n/**\n * Measure UTF-8 byte length without allocating a complete encoded copy.\n * Memory stays bounded by a 16 KiB buffer even for near-row-limit strings.\n */\nexport function byteLength(s: string): number {\n const buffer = new Uint8Array(BYTE_LENGTH_BUFFER_BYTES);\n let offset = 0;\n let bytes = 0;\n while (offset < s.length) {\n let end = Math.min(s.length, offset + BYTE_LENGTH_WINDOW_CHARS);\n if (\n end < s.length &&\n end > offset &&\n isHighSurrogate(s.charCodeAt(end - 1))\n ) {\n end--;\n }\n const { read, written } = textEncoder.encodeInto(\n s.slice(offset, end),\n buffer\n );\n if (read === 0) break;\n offset += read;\n bytes += written;\n }\n return bytes;\n}\n\nfunction isHighSurrogate(code: number): boolean {\n return code >= 0xd800 && code <= 0xdbff;\n}\n\ntype PartRecord = Record<string, unknown> & { type: string };\n\n/**\n * Sanitize a message for persistence by removing ephemeral provider-specific\n * data that should not be stored or sent back in subsequent requests.\n *\n * 1. Strips OpenAI ephemeral fields (itemId, reasoningEncryptedContent)\n * 2. Filters truly empty reasoning parts (no text, no remaining providerMetadata)\n */\nexport function sanitizeMessage<M extends { parts: readonly object[] }>(\n message: M\n): M {\n const parts = (message.parts as readonly PartRecord[]).flatMap((part) => {\n let sanitized = part;\n for (const key of [\"providerMetadata\", \"callProviderMetadata\"] as const) {\n const metadata = sanitized[key];\n if (\n metadata &&\n typeof metadata === \"object\" &&\n \"openai\" in (metadata as Record<string, unknown>)\n ) {\n sanitized = stripOpenAIMetadata(sanitized, key);\n }\n }\n if (sanitized.type === \"reasoning\") {\n const text = sanitized.text;\n if (typeof text !== \"string\" || text.trim() === \"\") {\n const metadata = sanitized.providerMetadata;\n const keep =\n metadata &&\n typeof metadata === \"object\" &&\n Object.keys(metadata as object).length > 0;\n if (!keep) return [];\n }\n }\n return [sanitized];\n });\n return { ...message, parts };\n}\n\nfunction stripOpenAIMetadata(\n part: PartRecord,\n metadataKey: \"providerMetadata\" | \"callProviderMetadata\"\n): PartRecord {\n const metadata = part[metadataKey] as {\n openai?: Record<string, unknown>;\n [key: string]: unknown;\n };\n if (!metadata?.openai) return part;\n\n const {\n itemId: _itemId,\n reasoningEncryptedContent: _rec,\n ...restOpenai\n } = metadata.openai;\n const { openai: _openai, ...restMetadata } = metadata;\n\n let next: Record<string, unknown> | undefined;\n if (Object.keys(restOpenai).length > 0) {\n next = { ...restMetadata, openai: restOpenai };\n } else if (Object.keys(restMetadata).length > 0) {\n next = restMetadata;\n }\n\n const { [metadataKey]: _old, ...rest } = part;\n return next\n ? ({ ...rest, [metadataKey]: next } as PartRecord)\n : (rest as PartRecord);\n}\n"],"mappings":";;;;;;AAMA,MAAM,cAAc,IAAI,YAAY;AACpC,MAAM,2BAA2B,KAAK;AACtC,MAAM,2BAA2B,KAAK;;;;;AAMtC,SAAgB,WAAW,GAAmB;CAC5C,MAAM,SAAS,IAAI,WAAW,wBAAwB;CACtD,IAAI,SAAS;CACb,IAAI,QAAQ;CACZ,OAAO,SAAS,EAAE,QAAQ;EACxB,IAAI,MAAM,KAAK,IAAI,EAAE,QAAQ,SAAS,wBAAwB;EAC9D,IACE,MAAM,EAAE,UACR,MAAM,UACN,gBAAgB,EAAE,WAAW,MAAM,CAAC,CAAC,GAErC;EAEF,MAAM,EAAE,MAAM,YAAY,YAAY,WACpC,EAAE,MAAM,QAAQ,GAAG,GACnB,MACF;EACA,IAAI,SAAS,GAAG;EAChB,UAAU;EACV,SAAS;CACX;CACA,OAAO;AACT;AAEA,SAAS,gBAAgB,MAAuB;CAC9C,OAAO,QAAQ,SAAU,QAAQ;AACnC;;;;;;;;AAWA,SAAgB,gBACd,SACG;CACH,MAAM,QAAS,QAAQ,MAAgC,SAAS,SAAS;EACvE,IAAI,YAAY;EAChB,KAAK,MAAM,OAAO,CAAC,oBAAoB,sBAAsB,GAAY;GACvE,MAAM,WAAW,UAAU;GAC3B,IACE,YACA,OAAO,aAAa,YACpB,YAAa,UAEb,YAAY,oBAAoB,WAAW,GAAG;EAElD;EACA,IAAI,UAAU,SAAS,aAAa;GAClC,MAAM,OAAO,UAAU;GACvB,IAAI,OAAO,SAAS,YAAY,KAAK,KAAK,MAAM,IAAI;IAClD,MAAM,WAAW,UAAU;IAK3B,IAAI,EAHF,YACA,OAAO,aAAa,YACpB,OAAO,KAAK,QAAkB,CAAC,CAAC,SAAS,IAChC,OAAO,CAAC;GACrB;EACF;EACA,OAAO,CAAC,SAAS;CACnB,CAAC;CACD,OAAO;EAAE,GAAG;EAAS;CAAM;AAC7B;AAEA,SAAS,oBACP,MACA,aACY;CACZ,MAAM,WAAW,KAAK;CAItB,IAAI,CAAC,UAAU,QAAQ,OAAO;CAE9B,MAAM,EACJ,QAAQ,SACR,2BAA2B,MAC3B,GAAG,eACD,SAAS;CACb,MAAM,EAAE,QAAQ,SAAS,GAAG,iBAAiB;CAE7C,IAAI;CACJ,IAAI,OAAO,KAAK,UAAU,CAAC,CAAC,SAAS,GACnC,OAAO;EAAE,GAAG;EAAc,QAAQ;CAAW;MACxC,IAAI,OAAO,KAAK,YAAY,CAAC,CAAC,SAAS,GAC5C,OAAO;CAGT,MAAM,GAAG,cAAc,MAAM,GAAG,SAAS;CACzC,OAAO,OACF;EAAE,GAAG;GAAO,cAAc;CAAK,IAC/B;AACP"}
|