agents 0.16.2 → 0.17.1
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 +11 -8
- package/dist/{agent-tool-types-CTw3UJUP.d.ts → agent-tool-types-CNyE1iz_.d.ts} +671 -138
- package/dist/agent-tool-types.d.ts +34 -18
- package/dist/agent-tool-types.js +20 -1
- package/dist/agent-tool-types.js.map +1 -0
- package/dist/agent-tools-BXlsuX0d.js +304 -0
- package/dist/agent-tools-BXlsuX0d.js.map +1 -0
- package/dist/agent-tools-CSnyGvJ2.d.ts +119 -0
- package/dist/agent-tools.d.ts +24 -18
- package/dist/agent-tools.js.map +1 -1
- package/dist/ai-chat-agent.d.ts +1 -1
- package/dist/ai-chat-agent.js +1 -2
- package/dist/ai-chat-agent.js.map +1 -1
- package/dist/ai-chat-v5-migration.d.ts +1 -1
- package/dist/ai-chat-v5-migration.js +1 -2
- package/dist/ai-chat-v5-migration.js.map +1 -1
- package/dist/ai-react.d.ts +1 -1
- package/dist/ai-react.js +1 -2
- package/dist/ai-react.js.map +1 -1
- package/dist/ai-types.d.ts +1 -7
- package/dist/ai-types.js +2 -8
- package/dist/ai-types.js.map +1 -1
- package/dist/browser/ai.js +1 -1
- package/dist/browser/index.js +1 -1
- package/dist/chat/index.d.ts +2033 -77
- package/dist/chat/index.js +1828 -245
- package/dist/chat/index.js.map +1 -1
- package/dist/chat/react.d.ts +602 -0
- package/dist/chat/react.js +1506 -0
- package/dist/chat/react.js.map +1 -0
- package/dist/chat-sdk/index.d.ts +4 -4
- package/dist/{classPrivateFieldGet2-CZ7QjTXN.js → classPrivateFieldGet2-DZBYAB34.js} +5 -5
- package/dist/{classPrivateMethodInitSpec-D-0__zd9.js → classPrivateMethodInitSpec-qMjJ6sHQ.js} +2 -2
- package/dist/{client-BXJ9n2f7.js → client-BZ-B3NhC.js} +2 -2
- package/dist/client-BZ-B3NhC.js.map +1 -0
- package/dist/client-tools-aIBO0Fk7.d.ts +53 -0
- package/dist/client.d.ts +76 -57
- package/dist/client.js +33 -5
- package/dist/client.js.map +1 -1
- package/dist/{connector-CrKhowfD.js → connector-CdldGF3h.js} +5 -5
- package/dist/{connector-CrKhowfD.js.map → connector-CdldGF3h.js.map} +1 -1
- package/dist/email.js +1 -1
- package/dist/email.js.map +1 -1
- package/dist/{index-B7IbEeze.d.ts → index-BRnybD6X.d.ts} +197 -14
- package/dist/index.d.ts +95 -73
- package/dist/index.js +694 -25
- package/dist/index.js.map +1 -1
- package/dist/mcp/client.d.ts +18 -14
- package/dist/mcp/client.js +1 -1
- package/dist/mcp/index.d.ts +30 -30
- package/dist/mcp/index.js +7 -7
- package/dist/mcp/index.js.map +1 -1
- package/dist/{agent-tools-3zLG7MgA.js → message-builder-BymO4N_D.js} +45 -126
- package/dist/message-builder-BymO4N_D.js.map +1 -0
- package/dist/observability/index.d.ts +1 -1
- package/dist/observability/index.js +4 -2
- package/dist/observability/index.js.map +1 -1
- package/dist/react.d.ts +123 -110
- package/dist/react.js +44 -11
- package/dist/react.js.map +1 -1
- package/dist/{retries-CwlpAGet.d.ts → retries-CvHJwSuh.d.ts} +15 -6
- package/dist/retries.d.ts +8 -6
- package/dist/retries.js +44 -1
- package/dist/retries.js.map +1 -1
- package/dist/serializable.d.ts +1 -1
- package/dist/skills/compile.js +1 -1
- package/dist/skills/compile.js.map +1 -1
- package/dist/skills/index.js +5 -5
- package/dist/skills/index.js.map +1 -1
- package/dist/sub-routing.d.ts +6 -6
- package/dist/utils.js +1 -1
- package/dist/utils.js.map +1 -1
- package/dist/vite.js +5 -5
- package/dist/vite.js.map +1 -1
- package/dist/wire-types-nflOzNuU.js +240 -0
- package/dist/wire-types-nflOzNuU.js.map +1 -0
- package/dist/{workflow-types-SrZK_o9p.d.ts → workflow-types-Baz_PO5v.d.ts} +42 -22
- package/dist/workflow-types.d.ts +25 -21
- package/dist/workflow-types.js.map +1 -1
- package/dist/workflows.d.ts +22 -21
- package/dist/workflows.js +31 -7
- package/dist/workflows.js.map +1 -1
- package/docs/adding-to-existing-project.md +450 -0
- package/docs/agent-class.md +503 -0
- package/docs/agent-tools.md +552 -0
- package/docs/browse-the-web.md +430 -0
- package/docs/callable-methods.md +627 -0
- package/docs/chat-agents.md +1696 -0
- package/docs/chat-sdk.md +181 -0
- package/docs/client-sdk.md +520 -0
- package/docs/client-tools-continuation.md +177 -0
- package/docs/codemode.md +440 -0
- package/docs/configuration.md +775 -0
- package/docs/cross-domain-authentication.md +171 -0
- package/docs/durable-execution.md +537 -0
- package/docs/email.md +663 -0
- package/docs/get-current-agent.md +204 -0
- package/docs/getting-started.md +305 -0
- package/docs/http-websockets.md +668 -0
- package/docs/human-in-the-loop.md +661 -0
- package/docs/index.md +151 -0
- package/docs/long-running-agents.md +730 -0
- package/docs/mcp-client.md +620 -0
- package/docs/mcp-servers.md +526 -0
- package/docs/mcp-transports.md +308 -0
- package/docs/migration-to-ai-sdk-v5.md +96 -0
- package/docs/migration-to-ai-sdk-v6.md +163 -0
- package/docs/observability.md +261 -0
- package/docs/push-notifications.md +367 -0
- package/docs/queue.md +329 -0
- package/docs/readonly-connections.md +278 -0
- package/docs/resumable-streaming.md +127 -0
- package/docs/retries.md +444 -0
- package/docs/routing.md +749 -0
- package/docs/scheduling.md +898 -0
- package/docs/securing-mcp-servers.md +359 -0
- package/docs/server-driven-messages.md +477 -0
- package/docs/sessions.md +1024 -0
- package/docs/state.md +512 -0
- package/docs/sub-agents.md +389 -0
- package/docs/webhooks.md +604 -0
- package/docs/workflows.md +877 -0
- package/package.json +41 -14
- package/dist/agent-tools-3zLG7MgA.js.map +0 -1
- package/dist/agent-tools-DZhI5F6Q.d.ts +0 -14
- package/dist/client-BXJ9n2f7.js.map +0 -1
package/dist/react.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import "./types.js";
|
|
2
2
|
import { camelCaseToKebabCase } from "./utils.js";
|
|
3
|
-
import { createStubProxy } from "./client.js";
|
|
4
|
-
import { n as
|
|
3
|
+
import { AgentConnectionError, createStubProxy, isTerminalCloseEvent } from "./client.js";
|
|
4
|
+
import { n as applyAgentToolEvent, r as createAgentToolEventState } from "./agent-tools-BXlsuX0d.js";
|
|
5
5
|
import { usePartySocket } from "partysocket/react";
|
|
6
6
|
import { use, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
7
7
|
//#region src/react.tsx
|
|
@@ -67,7 +67,7 @@ const _testUtils = {
|
|
|
67
67
|
};
|
|
68
68
|
function useAgent(options) {
|
|
69
69
|
const agentNamespace = camelCaseToKebabCase(options.agent);
|
|
70
|
-
const { query, queryDeps, cacheTtl, sub: subOption, path: userPath, defaultCallTimeout, ...restOptions } = options;
|
|
70
|
+
const { query, queryDeps, cacheTtl, sub: subOption, path: userPath, defaultCallTimeout, onConnectionError, shouldReconnectOnClose, ...restOptions } = options;
|
|
71
71
|
const subChain = useMemo(() => (subOption ?? []).map((s) => ({
|
|
72
72
|
agent: s.agent,
|
|
73
73
|
name: s.name
|
|
@@ -172,6 +172,12 @@ function useAgent(options) {
|
|
|
172
172
|
if (awaitingQueryRefresh && resolvedQuery !== void 0) setAwaitingQueryRefresh(false);
|
|
173
173
|
}, [awaitingQueryRefresh, resolvedQuery]);
|
|
174
174
|
const [agentState, setAgentState] = useState(void 0);
|
|
175
|
+
const [connectionError, setConnectionError] = useState(null);
|
|
176
|
+
const connectionErrorRef = useRef(null);
|
|
177
|
+
const connectionErrorAddressKeyRef = useRef(null);
|
|
178
|
+
const shouldReconnectOnCloseRef = useRef(shouldReconnectOnClose);
|
|
179
|
+
shouldReconnectOnCloseRef.current = shouldReconnectOnClose;
|
|
180
|
+
const classifyReconnect = useCallback((event) => (shouldReconnectOnCloseRef.current?.(event) ?? true) && !isTerminalCloseEvent(event), []);
|
|
175
181
|
const [identity, setIdentity] = useState({
|
|
176
182
|
name: leafName,
|
|
177
183
|
agent: camelCaseToKebabCase(leafAgent),
|
|
@@ -198,14 +204,16 @@ function useAgent(options) {
|
|
|
198
204
|
basePath: options.basePath,
|
|
199
205
|
path: combinedPath || void 0,
|
|
200
206
|
query: resolvedQuery,
|
|
201
|
-
...restOptions
|
|
207
|
+
...restOptions,
|
|
208
|
+
shouldReconnectOnClose: classifyReconnect
|
|
202
209
|
} : {
|
|
203
210
|
party: agentNamespace,
|
|
204
211
|
prefix: "agents",
|
|
205
212
|
room: options.name || "default",
|
|
206
213
|
path: combinedPath || void 0,
|
|
207
214
|
query: resolvedQuery,
|
|
208
|
-
...restOptions
|
|
215
|
+
...restOptions,
|
|
216
|
+
shouldReconnectOnClose: classifyReconnect
|
|
209
217
|
};
|
|
210
218
|
const socketEnabled = !awaitingQueryRefresh && (restOptions.enabled ?? true);
|
|
211
219
|
const addressKey = JSON.stringify([
|
|
@@ -215,10 +223,14 @@ function useAgent(options) {
|
|
|
215
223
|
options.name || "default",
|
|
216
224
|
combinedPath || null
|
|
217
225
|
]);
|
|
226
|
+
const visibleConnectionError = connectionErrorAddressKeyRef.current === addressKey ? connectionError : null;
|
|
227
|
+
connectionErrorRef.current = visibleConnectionError;
|
|
218
228
|
const agent = usePartySocket({
|
|
219
229
|
...socketOptions,
|
|
220
230
|
enabled: socketEnabled,
|
|
221
231
|
onOpen: (event) => {
|
|
232
|
+
connectionErrorAddressKeyRef.current = null;
|
|
233
|
+
setConnectionError(null);
|
|
222
234
|
flushQueuedCalls();
|
|
223
235
|
options.onOpen?.(event);
|
|
224
236
|
},
|
|
@@ -310,7 +322,11 @@ function useAgent(options) {
|
|
|
310
322
|
onClose: (event) => {
|
|
311
323
|
const closedSocket = event.target ?? socketRef.current;
|
|
312
324
|
const isCurrentSocket = closedSocket === socketRef.current;
|
|
313
|
-
|
|
325
|
+
const terminalClose = isTerminalCloseEvent(event);
|
|
326
|
+
if (closedSocket) {
|
|
327
|
+
rejectCallsSentOn(closedSocket, "Connection closed");
|
|
328
|
+
if (isCurrentSocket && !closedSocket.shouldReconnect) rejectQueuedCalls("Connection closed");
|
|
329
|
+
}
|
|
314
330
|
if (isCurrentSocket) {
|
|
315
331
|
resetReady();
|
|
316
332
|
if (mutableAgentRef.current) mutableAgentRef.current.identified = false;
|
|
@@ -318,9 +334,17 @@ function useAgent(options) {
|
|
|
318
334
|
...prev,
|
|
319
335
|
identified: false
|
|
320
336
|
}));
|
|
321
|
-
if (
|
|
322
|
-
|
|
323
|
-
|
|
337
|
+
if (closedSocket?.shouldReconnect) {
|
|
338
|
+
if (isAsyncQuery) setAwaitingQueryRefresh(true);
|
|
339
|
+
deleteCacheEntry(cacheKeyRef.current);
|
|
340
|
+
setCacheInvalidatedAt(Date.now());
|
|
341
|
+
}
|
|
342
|
+
if (!closedSocket?.shouldReconnect && terminalClose) {
|
|
343
|
+
const error = new AgentConnectionError(event);
|
|
344
|
+
connectionErrorAddressKeyRef.current = addressKey;
|
|
345
|
+
setConnectionError(error);
|
|
346
|
+
onConnectionError?.(error);
|
|
347
|
+
}
|
|
324
348
|
}
|
|
325
349
|
options.onClose?.(event);
|
|
326
350
|
}
|
|
@@ -333,7 +357,11 @@ function useAgent(options) {
|
|
|
333
357
|
prevSocketRef.current = agent;
|
|
334
358
|
const prevAddress = prevAddressKeyRef.current;
|
|
335
359
|
prevAddressKeyRef.current = addressKey;
|
|
336
|
-
if (prevAddress !== addressKey)
|
|
360
|
+
if (prevAddress !== addressKey) {
|
|
361
|
+
connectionErrorAddressKeyRef.current = null;
|
|
362
|
+
setConnectionError(null);
|
|
363
|
+
rejectQueuedCalls("Call discarded: the agent address changed before the request could be sent");
|
|
364
|
+
}
|
|
337
365
|
if (prev && prev !== agent) {
|
|
338
366
|
rejectCallsSentOn(prev, "Connection closed");
|
|
339
367
|
resetReady();
|
|
@@ -346,6 +374,11 @@ function useAgent(options) {
|
|
|
346
374
|
}, [agent, addressKey]);
|
|
347
375
|
const call = useCallback((method, args = [], options) => {
|
|
348
376
|
return new Promise((resolve, reject) => {
|
|
377
|
+
const socket = socketRef.current;
|
|
378
|
+
if (socket && connectionErrorRef.current && socket.readyState === socket.CLOSED) {
|
|
379
|
+
reject(/* @__PURE__ */ new Error("Connection closed"));
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
349
382
|
const id = crypto.randomUUID();
|
|
350
383
|
let timeoutId;
|
|
351
384
|
const isLegacyFormat = options && ("onChunk" in options || "onDone" in options || "onError" in options);
|
|
@@ -373,7 +406,6 @@ function useAgent(options) {
|
|
|
373
406
|
request,
|
|
374
407
|
sentOn: null
|
|
375
408
|
});
|
|
376
|
-
const socket = socketRef.current;
|
|
377
409
|
if (socket && socket.readyState === socket.OPEN) {
|
|
378
410
|
socket.send(request);
|
|
379
411
|
const pending = pendingCallsRef.current.get(id);
|
|
@@ -396,6 +428,7 @@ function useAgent(options) {
|
|
|
396
428
|
agent.identified = identity.identified;
|
|
397
429
|
agent.ready = readyRef.current.promise;
|
|
398
430
|
agent.state = agentState;
|
|
431
|
+
agent.connectionError = visibleConnectionError;
|
|
399
432
|
mutableAgentRef.current = agent;
|
|
400
433
|
agent.stub = useMemo(() => createStubProxy(call), [call]);
|
|
401
434
|
agent.getHttpUrl = () => {
|
package/dist/react.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"react.js","names":[],"sources":["../src/react.tsx"],"sourcesContent":["import type { PartySocket } from \"partysocket\";\nimport { usePartySocket } from \"partysocket/react\";\nimport { useCallback, useRef, use, useMemo, useState, useEffect } from \"react\";\nimport type { MCPServersState, RPCRequest, RPCResponse } from \"./\";\nimport type {\n AgentPromiseReturnType,\n AgentStub,\n CallOptions,\n OptionalAgentMethods,\n RequiredAgentMethods,\n StreamOptions,\n UntypedAgentStub\n} from \"./client\";\nimport type { ClientParameters } from \"./serializable\";\nimport { createStubProxy, DEFAULT_CALL_TIMEOUT_MS } from \"./client\";\nimport { camelCaseToKebabCase } from \"./utils\";\nimport { MessageType } from \"./types\";\nimport {\n applyAgentToolEvent,\n createAgentToolEventState,\n type AgentToolEventMessage,\n type AgentToolEventState,\n type AgentToolRunState\n} from \"./chat/agent-tools\";\n\ntype QueryObject = Record<string, string | null>;\n\ninterface CacheEntry {\n promise: Promise<QueryObject>;\n expiresAt: number;\n}\n\nconst queryCache = new Map<string, CacheEntry>();\n\nfunction createCacheKey(\n agentNamespace: string,\n name: string | undefined,\n subChainOrDeps: ReadonlyArray<{ agent: string; name: string }> | unknown[],\n deps?: unknown[]\n): string {\n // Backwards-compatible overload: if called with 3 args, the third\n // argument is `deps` and `subChain` defaults to empty. With 4 args,\n // the third is the sub-chain. This keeps existing callers (and\n // the `_testUtils` surface) working while letting new callers\n // include the nested chain in the cache key.\n //\n // Empty sub-chain must produce the same key as the old 3-arg\n // form, so nested-addressing code can opt-in without invalidating\n // existing caches.\n if (deps === undefined) {\n return JSON.stringify([\n agentNamespace,\n name || \"default\",\n ...(subChainOrDeps as unknown[])\n ]);\n }\n const subChain = subChainOrDeps as ReadonlyArray<{\n agent: string;\n name: string;\n }>;\n if (subChain.length === 0) {\n return JSON.stringify([agentNamespace, name || \"default\", ...deps]);\n }\n return JSON.stringify([\n agentNamespace,\n name || \"default\",\n subChain.map((s) => [s.agent, s.name]),\n ...deps\n ]);\n}\n\n/** Build a URL path tail `/sub/{agent-kebab}/{name}/...` from a sub chain. */\nfunction buildSubPath(\n subChain: ReadonlyArray<{ agent: string; name: string }>,\n extraPath?: string\n): string {\n if (subChain.length === 0) return extraPath ?? \"\";\n const parts = subChain.flatMap((step) => [\n \"sub\",\n camelCaseToKebabCase(step.agent),\n encodeURIComponent(step.name)\n ]);\n const combined = parts.join(\"/\");\n if (extraPath) {\n const trimmed = extraPath.startsWith(\"/\") ? extraPath.slice(1) : extraPath;\n return `${combined}/${trimmed}`;\n }\n return combined;\n}\n\nfunction getCacheEntry(key: string): CacheEntry | undefined {\n const entry = queryCache.get(key);\n if (!entry) return undefined;\n\n if (Date.now() >= entry.expiresAt) {\n queryCache.delete(key);\n return undefined;\n }\n\n return entry;\n}\n\nfunction setCacheEntry(\n key: string,\n promise: Promise<QueryObject>,\n cacheTtl: number\n): CacheEntry {\n const entry: CacheEntry = {\n promise,\n expiresAt: Date.now() + cacheTtl\n };\n queryCache.set(key, entry);\n return entry;\n}\n\nfunction deleteCacheEntry(key: string): void {\n queryCache.delete(key);\n}\n\n// Export for testing purposes\nexport const _testUtils = {\n queryCache,\n setCacheEntry,\n getCacheEntry,\n deleteCacheEntry,\n clearCache: () => queryCache.clear(),\n createStubProxy,\n createCacheKey\n};\n\n/**\n * Options for the useAgent hook\n * @template State Type of the Agent's state\n */\nexport type UseAgentOptions<State = unknown> = Omit<\n Parameters<typeof usePartySocket>[0],\n \"party\" | \"room\" | \"query\"\n> & {\n /** Name of the agent to connect to (ignored if basePath is set) */\n agent: string;\n /** Name of the specific Agent instance (ignored if basePath is set) */\n name?: string;\n /**\n * Full URL path - bypasses agent/name URL construction.\n * When set, the client connects to this path directly.\n * Server must handle routing manually (e.g., with getAgentByName + fetch).\n * @example\n * // Client connects to /user, server routes based on session\n * useAgent({ agent: \"UserAgent\", basePath: \"user\" })\n */\n basePath?: string;\n /** Query parameters - can be static object or async function */\n query?: QueryObject | (() => Promise<QueryObject>);\n /** Dependencies for async query caching */\n queryDeps?: unknown[];\n /** Cache TTL in milliseconds for auth tokens/time-sensitive data */\n cacheTtl?: number;\n /** Called when the Agent's state is updated */\n onStateUpdate?: (state: State, source: \"server\" | \"client\") => void;\n /** Called when a state update fails (e.g., connection is readonly) */\n onStateUpdateError?: (error: string) => void;\n /** Called when MCP server state is updated */\n onMcpUpdate?: (mcpServers: MCPServersState) => void;\n /**\n * Called when the server sends the agent's identity on connect.\n * Useful when using basePath, as the actual instance name is determined server-side.\n * @param name The actual agent instance name\n * @param agent The agent class name (kebab-case)\n */\n onIdentity?: (name: string, agent: string) => void;\n /**\n * Called when identity changes on reconnect (different instance than before).\n * If not provided and identity changes, a warning will be logged.\n * @param oldName Previous instance name\n * @param newName New instance name\n * @param oldAgent Previous agent class name\n * @param newAgent New agent class name\n */\n onIdentityChange?: (\n oldName: string,\n newName: string,\n oldAgent: string,\n newAgent: string\n ) => void;\n /**\n * Additional path to append to the URL.\n * Works with both standard routing and basePath.\n * @example\n * // With basePath: /user/settings\n * { basePath: \"user\", path: \"settings\" }\n * // Standard: /agents/my-agent/room/settings\n * { agent: \"MyAgent\", name: \"room\", path: \"settings\" }\n */\n path?: string;\n /**\n * Connect to a sub-agent (facet) via its parent. Flat array,\n * root-first. Each step addresses one parent↔child hop.\n *\n * The hook's returned `.agent` / `.name` report the **leaf**\n * identity (the deepest entry in `sub`), so downstream hooks\n * like `useAgentChat` see the child they actually talk to.\n * `.path` exposes the full chain for observability, deep links,\n * and reconnect keying.\n *\n * @example\n * ```ts\n * // Two-level nesting: Inbox (Alice) → Chat (abc)\n * useAgent({\n * agent: \"inbox\", name: userId,\n * sub: [{ agent: \"chat\", name: chatId }]\n * });\n *\n * // Three-level: tenant → inbox → chat\n * useAgent({\n * agent: \"tenant\", name: tenantId,\n * sub: [\n * { agent: \"inbox\", name: userId },\n * { agent: \"chat\", name: chatId }\n * ]\n * });\n * ```\n *\n * @experimental The API surface may change before stabilizing.\n */\n sub?: ReadonlyArray<{ agent: string; name: string }>;\n /**\n * Default timeout (in milliseconds) applied to non-streaming `call()`s\n * that don't pass an explicit `timeout`. Acts as a backstop so calls\n * whose response is lost (e.g. the connection is replaced mid-flight)\n * reject instead of hanging forever.\n *\n * Defaults to 30 000 ms. Set to `0` to disable the default timeout.\n * Streaming calls never get a default timeout (long-lived streams are\n * legitimate); pass an explicit `timeout` to bound them.\n */\n defaultCallTimeout?: number;\n};\n\ntype OptionalArgsAgentMethodCall<AgentT> = <\n K extends keyof OptionalAgentMethods<AgentT>\n>(\n method: K,\n args?: ClientParameters<OptionalAgentMethods<AgentT>[K]>,\n options?: CallOptions | StreamOptions\n) => AgentPromiseReturnType<AgentT, K>;\n\ntype RequiredArgsAgentMethodCall<AgentT> = <\n K extends keyof RequiredAgentMethods<AgentT>\n>(\n method: K,\n args: ClientParameters<RequiredAgentMethods<AgentT>[K]>,\n options?: CallOptions | StreamOptions\n) => AgentPromiseReturnType<AgentT, K>;\n\ntype AgentMethodCall<AgentT> = OptionalArgsAgentMethodCall<AgentT> &\n RequiredArgsAgentMethodCall<AgentT>;\n\ntype UntypedAgentMethodCall = <T = unknown>(\n method: string,\n args?: unknown[],\n options?: CallOptions | StreamOptions\n) => Promise<T>;\n\n/**\n * React hook for connecting to an Agent\n */\nexport function useAgent<State = unknown>(\n options: UseAgentOptions<State>\n): Omit<PartySocket, \"path\"> & {\n agent: string;\n name: string;\n /** Full root-first address chain, including leaf. Single entry when `sub` isn't set. */\n path: ReadonlyArray<{ agent: string; name: string }>;\n identified: boolean;\n ready: Promise<void>;\n state: State | undefined;\n setState: (state: State) => void;\n call: UntypedAgentMethodCall;\n stub: UntypedAgentStub;\n getHttpUrl: () => string;\n};\nexport function useAgent<\n AgentT extends {\n get state(): State;\n },\n State\n>(\n options: UseAgentOptions<State>\n): Omit<PartySocket, \"path\"> & {\n agent: string;\n name: string;\n path: ReadonlyArray<{ agent: string; name: string }>;\n identified: boolean;\n ready: Promise<void>;\n state: State | undefined;\n setState: (state: State) => void;\n call: AgentMethodCall<AgentT>;\n stub: AgentStub<AgentT>;\n getHttpUrl: () => string;\n};\nexport function useAgent<State>(options: UseAgentOptions<unknown>): Omit<\n PartySocket,\n \"path\"\n> & {\n agent: string;\n name: string;\n path: ReadonlyArray<{ agent: string; name: string }>;\n identified: boolean;\n ready: Promise<void>;\n state: State | undefined;\n setState: (state: State) => void;\n call: UntypedAgentMethodCall | AgentMethodCall<unknown>;\n stub: UntypedAgentStub;\n getHttpUrl: () => string;\n} {\n const agentNamespace = camelCaseToKebabCase(options.agent);\n // NOTE: `path` is destructured out (as `userPath`) so it does NOT\n // end up in `restOptions`. Spreading `restOptions` after the\n // computed `path: combinedPath` would otherwise let the user's raw\n // `path` overwrite the combined sub-agent URL, dropping every\n // `/sub/{child}/{name}` segment on the way to the socket.\n const {\n query,\n queryDeps,\n cacheTtl,\n sub: subOption,\n path: userPath,\n defaultCallTimeout,\n ...restOptions\n } = options;\n\n const subChain = useMemo(\n () => (subOption ?? []).map((s) => ({ agent: s.agent, name: s.name })),\n // Stable serialization — deep changes re-memoize.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [JSON.stringify(subOption ?? [])]\n );\n\n // The \"leaf\" is the deepest entry in the chain; it's what\n // downstream code (useAgentChat etc.) should see as the\n // authoritative identity.\n const leafAgent =\n subChain.length > 0 ? subChain[subChain.length - 1].agent : options.agent;\n const leafName =\n subChain.length > 0\n ? subChain[subChain.length - 1].name\n : options.name || \"default\";\n\n // Full root-first chain, including the leaf. Exposed as `.path`\n // and used for cache keying so nested sessions with the same leaf\n // name don't collide.\n const fullPath = useMemo(\n () => [\n { agent: options.agent, name: options.name || \"default\" },\n ...subChain\n ],\n [options.agent, options.name, subChain]\n );\n\n // Keep track of pending RPC calls.\n //\n // Each entry is tagged with the socket the request was transmitted on\n // (`sentOn`). Requests are only handed to a socket once it's OPEN —\n // until then they stay queued here (`sentOn: null`). This matters\n // because `usePartySocket` *replaces* the socket object whenever\n // connection options change (async query refresh, path change, etc.):\n // anything buffered inside a replaced socket is lost forever, and a\n // call transmitted on a replaced socket can never receive its\n // response. Tagging lets us:\n // - flush still-queued requests on whichever socket connects next\n // (safe: they were never transmitted, so no double-execution risk)\n // - reject calls transmitted on a socket that closed or was replaced\n // (their response can never arrive)\n // - avoid rejecting calls in flight on the *new* socket when a stale\n // close event from an old socket trickles in\n const pendingCallsRef = useRef(\n new Map<\n string,\n {\n resolve: (value: unknown) => void;\n reject: (error: Error) => void;\n stream?: StreamOptions;\n timeoutId?: ReturnType<typeof setTimeout>;\n /** Serialized RPC request, kept so it can be (re)transmitted */\n request: string;\n /** Socket the request was transmitted on; null while queued */\n sentOn: PartySocket | null;\n }\n >()\n );\n\n // Always points at the socket from the latest render. `call`,\n // `setState`, and the queue-flushing logic go through this ref so\n // that stale `agent` references held by old effect closures still\n // route their traffic to the live socket instead of a dead one.\n const socketRef = useRef<PartySocket | null>(null);\n\n const defaultCallTimeoutRef = useRef(\n defaultCallTimeout ?? DEFAULT_CALL_TIMEOUT_MS\n );\n defaultCallTimeoutRef.current = defaultCallTimeout ?? DEFAULT_CALL_TIMEOUT_MS;\n\n /** Reject (and remove) every pending call transmitted on `socket`. */\n const rejectCallsSentOn = (socket: PartySocket, reason: string) => {\n const error = new Error(reason);\n for (const [id, pending] of pendingCallsRef.current) {\n if (pending.sentOn === socket) {\n if (pending.timeoutId) clearTimeout(pending.timeoutId);\n pendingCallsRef.current.delete(id);\n pending.reject(error);\n pending.stream?.onError?.(reason);\n }\n }\n };\n\n /** Transmit queued (never-sent) calls if the live socket is open. */\n const flushQueuedCalls = () => {\n const socket = socketRef.current;\n if (!socket || socket.readyState !== socket.OPEN) return;\n for (const pending of pendingCallsRef.current.values()) {\n if (pending.sentOn === null) {\n socket.send(pending.request);\n pending.sentOn = socket;\n }\n }\n };\n\n /** Reject (and remove) every still-queued (never transmitted) call. */\n const rejectQueuedCalls = (reason: string) => {\n const error = new Error(reason);\n for (const [id, pending] of pendingCallsRef.current) {\n if (pending.sentOn === null) {\n if (pending.timeoutId) clearTimeout(pending.timeoutId);\n pendingCallsRef.current.delete(id);\n pending.reject(error);\n pending.stream?.onError?.(reason);\n }\n }\n };\n\n const cacheKey = useMemo(\n () =>\n createCacheKey(agentNamespace, options.name, subChain, queryDeps || []),\n [agentNamespace, options.name, subChain, queryDeps]\n );\n\n // Track current cache key in a ref for use in onClose handler.\n // This ensures we invalidate the correct cache entry when the connection closes,\n // even if the component re-renders with different props before onClose fires.\n // We update synchronously during render (not in useEffect) to avoid race\n // conditions where onClose could fire before the effect runs.\n const cacheKeyRef = useRef(cacheKey);\n cacheKeyRef.current = cacheKey;\n\n const ttl = cacheTtl ?? 5 * 60 * 1000;\n\n // Track cache invalidation to force re-render when TTL expires\n const [cacheInvalidatedAt, setCacheInvalidatedAt] = useState<number>(0);\n\n // Disable socket while waiting for async query to refresh after disconnect\n const isAsyncQuery = query && typeof query === \"function\";\n const [awaitingQueryRefresh, setAwaitingQueryRefresh] = useState(false);\n\n // Get or create the query promise\n const queryPromise = useMemo(() => {\n // Re-run when cache is invalidated after TTL expiry\n void cacheInvalidatedAt;\n\n if (!query || typeof query !== \"function\") {\n return null;\n }\n\n // Always check cache first to deduplicate concurrent requests\n const cached = getCacheEntry(cacheKey);\n if (cached) {\n return cached.promise;\n }\n\n // Create new promise\n const promise = query().catch((error) => {\n console.error(\n `[useAgent] Query failed for agent \"${options.agent}\":`,\n error\n );\n deleteCacheEntry(cacheKey);\n throw error;\n });\n\n // Always cache to deduplicate concurrent requests\n setCacheEntry(cacheKey, promise, ttl);\n\n return promise;\n }, [cacheKey, query, options.agent, ttl, cacheInvalidatedAt]);\n\n // Schedule cache invalidation when TTL expires\n useEffect(() => {\n if (!queryPromise || ttl <= 0) return;\n\n const entry = getCacheEntry(cacheKey);\n if (!entry) return;\n\n const timeUntilExpiry = entry.expiresAt - Date.now();\n\n // Always set a timer (with min 0ms) to ensure cleanup function is returned\n const timer = setTimeout(\n () => {\n deleteCacheEntry(cacheKey);\n setCacheInvalidatedAt(Date.now());\n },\n Math.max(0, timeUntilExpiry)\n );\n\n return () => clearTimeout(timer);\n }, [cacheKey, queryPromise, ttl]);\n\n let resolvedQuery: QueryObject | undefined;\n\n if (query) {\n if (typeof query === \"function\") {\n // Use React's use() to resolve the promise\n const queryResult = use(queryPromise!);\n\n // Check for non-primitive values and warn\n if (queryResult) {\n for (const [key, value] of Object.entries(queryResult)) {\n if (\n value !== null &&\n value !== undefined &&\n typeof value !== \"string\" &&\n typeof value !== \"number\" &&\n typeof value !== \"boolean\"\n ) {\n console.warn(\n `[useAgent] Query parameter \"${key}\" is an object and will be converted to \"[object Object]\". ` +\n \"Query parameters should be string, number, boolean, or null.\"\n );\n }\n }\n resolvedQuery = queryResult;\n }\n } else {\n // Sync query - use directly\n resolvedQuery = query;\n }\n }\n\n // Re-enable socket after async query resolves\n useEffect(() => {\n if (awaitingQueryRefresh && resolvedQuery !== undefined) {\n setAwaitingQueryRefresh(false);\n }\n }, [awaitingQueryRefresh, resolvedQuery]);\n\n // Track agent state for reactivity — updated on server broadcasts and client setState\n const [agentState, setAgentState] = useState<State | undefined>(undefined);\n\n // Store identity in React state for reactivity. Seed with the\n // leaf's address — what the server will echo back in\n // `cf_agent_identity`.\n const [identity, setIdentity] = useState({\n name: leafName,\n agent: camelCaseToKebabCase(leafAgent),\n identified: false\n });\n\n // Track previous identity for change detection\n const previousIdentityRef = useRef<{\n name: string | null;\n agent: string | null;\n }>({ name: null, agent: null });\n\n // Ready promise - resolves when identity is received, resets on close\n const readyRef = useRef<\n { promise: Promise<void>; resolve: () => void } | undefined\n >(undefined);\n\n const resetReady = () => {\n let resolve: () => void;\n const promise = new Promise<void>((r) => {\n resolve = r;\n });\n readyRef.current = { promise, resolve: resolve! };\n };\n\n if (!readyRef.current) {\n resetReady();\n }\n\n const mutableAgentRef = useRef<{\n agent: string;\n name: string;\n identified: boolean;\n } | null>(null);\n\n // Combine the sub-agent chain with the user-provided `path`.\n // Order matters: `/sub/{child}/{name}/...` comes before `path` so\n // the server sees the hierarchy it expects.\n const combinedPath = useMemo(\n () => buildSubPath(subChain, userPath),\n [subChain, userPath]\n );\n\n // If basePath is provided, use it directly; otherwise construct from agent/name\n const socketOptions = options.basePath\n ? {\n basePath: options.basePath,\n path: combinedPath || undefined,\n query: resolvedQuery,\n ...restOptions\n }\n : {\n party: agentNamespace,\n prefix: \"agents\",\n room: options.name || \"default\",\n path: combinedPath || undefined,\n query: resolvedQuery,\n ...restOptions\n };\n\n const socketEnabled = !awaitingQueryRefresh && (restOptions.enabled ?? true);\n\n // Identifies *which agent instance* this hook is addressing. Queued\n // (never-transmitted) RPC calls are only safe to flush onto a later\n // socket if it still points at the same instance — a call composed for\n // agent \"alpha\" must not execute on agent \"beta\" just because the\n // `name` prop changed while the call was waiting for a connection.\n // Credentials (query params) are deliberately excluded: a token\n // refresh doesn't change where calls go.\n const addressKey = JSON.stringify([\n options.host ?? null,\n options.basePath ?? null,\n agentNamespace,\n options.name || \"default\",\n combinedPath || null\n ]);\n\n const agent = usePartySocket({\n ...socketOptions,\n enabled: socketEnabled,\n onOpen: (event: Event) => {\n // The socket is open: transmit any RPC requests that were issued\n // while disconnected (or while a previous socket was being\n // replaced). They were never handed to a socket before, so this\n // cannot double-execute anything server-side.\n flushQueuedCalls();\n options.onOpen?.(event);\n },\n onMessage: (message) => {\n if (typeof message.data === \"string\") {\n let parsedMessage: Record<string, unknown>;\n try {\n parsedMessage = JSON.parse(message.data);\n } catch (_error) {\n // silently ignore invalid messages for now\n // TODO: log errors with log levels\n return options.onMessage?.(message);\n }\n if (parsedMessage.type === MessageType.CF_AGENT_IDENTITY) {\n const oldName = previousIdentityRef.current.name;\n const oldAgent = previousIdentityRef.current.agent;\n const newName = parsedMessage.name as string;\n const newAgent = parsedMessage.agent as string;\n\n const currentAgent = mutableAgentRef.current;\n if (currentAgent) {\n currentAgent.name = newName;\n currentAgent.agent = newAgent;\n currentAgent.identified = true;\n }\n\n // Update reactive state (triggers re-render)\n setIdentity({ name: newName, agent: newAgent, identified: true });\n\n // Resolve ready promise\n readyRef.current?.resolve();\n\n // Detect identity change on reconnect\n if (\n oldName !== null &&\n oldAgent !== null &&\n (oldName !== newName || oldAgent !== newAgent)\n ) {\n if (options.onIdentityChange) {\n options.onIdentityChange(oldName, newName, oldAgent, newAgent);\n } else {\n const agentChanged = oldAgent !== newAgent;\n const nameChanged = oldName !== newName;\n let changeDescription = \"\";\n if (agentChanged && nameChanged) {\n changeDescription = `agent \"${oldAgent}\" → \"${newAgent}\", instance \"${oldName}\" → \"${newName}\"`;\n } else if (agentChanged) {\n changeDescription = `agent \"${oldAgent}\" → \"${newAgent}\"`;\n } else {\n changeDescription = `instance \"${oldName}\" → \"${newName}\"`;\n }\n console.warn(\n `[agents] Identity changed on reconnect: ${changeDescription}. ` +\n \"This can happen with server-side routing (e.g., basePath with getAgentByName) \" +\n \"where the instance is determined by auth/session. \" +\n \"Provide onIdentityChange callback to handle this explicitly, \" +\n \"or ignore if this is expected for your routing pattern.\"\n );\n }\n }\n\n // Track for next change detection\n previousIdentityRef.current = { name: newName, agent: newAgent };\n\n // Call onIdentity callback\n options.onIdentity?.(newName, newAgent);\n return;\n }\n if (parsedMessage.type === MessageType.CF_AGENT_STATE) {\n setAgentState(parsedMessage.state as State);\n options.onStateUpdate?.(parsedMessage.state as State, \"server\");\n return;\n }\n if (parsedMessage.type === MessageType.CF_AGENT_STATE_ERROR) {\n options.onStateUpdateError?.(parsedMessage.error as string);\n return;\n }\n if (parsedMessage.type === MessageType.CF_AGENT_MCP_SERVERS) {\n options.onMcpUpdate?.(parsedMessage.mcp as MCPServersState);\n return;\n }\n if (parsedMessage.type === MessageType.RPC) {\n const response = parsedMessage as RPCResponse;\n const pending = pendingCallsRef.current.get(response.id);\n if (!pending) {\n console.warn(\n `[useAgent] Discarded an RPC response with no matching pending call (id \"${response.id}\"). ` +\n \"The call likely timed out or was rejected when its connection closed before the response arrived.\"\n );\n return;\n }\n\n if (!response.success) {\n if (pending.timeoutId) clearTimeout(pending.timeoutId);\n pending.reject(new Error(response.error));\n pendingCallsRef.current.delete(response.id);\n pending.stream?.onError?.(response.error);\n return;\n }\n\n // Handle streaming responses\n if (\"done\" in response) {\n if (response.done) {\n if (pending.timeoutId) clearTimeout(pending.timeoutId);\n pending.resolve(response.result);\n pendingCallsRef.current.delete(response.id);\n pending.stream?.onDone?.(response.result);\n } else {\n pending.stream?.onChunk?.(response.result);\n }\n } else {\n // Non-streaming response\n if (pending.timeoutId) clearTimeout(pending.timeoutId);\n pending.resolve(response.result);\n pendingCallsRef.current.delete(response.id);\n }\n return;\n }\n }\n options.onMessage?.(message);\n },\n onClose: (event: CloseEvent) => {\n // Identify which socket actually closed. Close events are\n // dispatched asynchronously, so a close from an old socket that\n // was just replaced can arrive while a new socket is already\n // connecting (or connected). `event.target` is the PartySocket\n // that dispatched the event; fall back to the live socket if the\n // environment doesn't populate it.\n const closedSocket =\n (event.target as PartySocket | null) ?? socketRef.current;\n const isCurrentSocket = closedSocket === socketRef.current;\n\n // Calls transmitted on the closed socket can never receive their\n // response — reject them. Calls still queued (never transmitted)\n // stay pending and are flushed when a socket next opens; calls\n // in flight on a *different* (newer) socket are untouched.\n if (closedSocket) {\n rejectCallsSentOn(closedSocket, \"Connection closed\");\n }\n\n if (isCurrentSocket) {\n // Reset ready state for next connection\n resetReady();\n if (mutableAgentRef.current) {\n mutableAgentRef.current.identified = false;\n }\n setIdentity((prev) => ({ ...prev, identified: false }));\n\n // Pause reconnection for async queries until fresh query params are ready\n if (isAsyncQuery) {\n setAwaitingQueryRefresh(true);\n }\n\n // Invalidate cache and trigger re-render to fetch fresh query params\n deleteCacheEntry(cacheKeyRef.current);\n setCacheInvalidatedAt(Date.now());\n }\n\n // Call user's onClose if provided\n options.onClose?.(event);\n }\n }) as PartySocket & {\n agent: string;\n name: string;\n identified: boolean;\n ready: Promise<void>;\n state: State | undefined;\n setState: (state: State) => void;\n call: UntypedAgentMethodCall;\n stub: UntypedAgentStub;\n getHttpUrl: () => string;\n };\n // Update the live-socket ref before anything below can use it.\n socketRef.current = agent;\n\n // When `usePartySocket` replaces the socket object (connection options\n // changed — async query refresh, path change, enabled toggle, ...) the\n // old socket's event listeners are detached at the same commit, so its\n // final close event may never be observed by our onClose handler.\n // Sweep here instead: anything transmitted on the old socket can never\n // get a response, and the identity it established no longer applies.\n // Queued (never-transmitted) calls survive and flush when the new\n // socket opens.\n const prevSocketRef = useRef<PartySocket | null>(null);\n const prevAddressKeyRef = useRef(addressKey);\n useEffect(() => {\n const prev = prevSocketRef.current;\n prevSocketRef.current = agent;\n const prevAddress = prevAddressKeyRef.current;\n prevAddressKeyRef.current = addressKey;\n\n // Destination guard: if the agent address changed (different agent,\n // name, or path — not just refreshed credentials), calls that are\n // still queued were composed for the *old* instance. Reject them\n // before anything can flush them onto the new instance.\n if (prevAddress !== addressKey) {\n rejectQueuedCalls(\n \"Call discarded: the agent address changed before the request could be sent\"\n );\n }\n\n if (prev && prev !== agent) {\n rejectCallsSentOn(prev, \"Connection closed\");\n resetReady();\n if (mutableAgentRef.current) {\n mutableAgentRef.current.identified = false;\n }\n setIdentity((current) =>\n current.identified ? { ...current, identified: false } : current\n );\n }\n // The helpers only touch refs; re-running on socket/address change is all we need.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [agent, addressKey]);\n\n // Create the call method. Deliberately dependency-free: it routes\n // through refs, so even a stale `agent` reference captured by an old\n // effect closure issues calls against the live socket.\n const call = useCallback(\n <T = unknown,>(\n method: string,\n args: unknown[] = [],\n options?: CallOptions | StreamOptions\n ): Promise<T> => {\n return new Promise((resolve, reject) => {\n const id = crypto.randomUUID();\n let timeoutId: ReturnType<typeof setTimeout> | undefined;\n\n // Detect legacy format: { onChunk?, onDone?, onError? } vs new format: { timeout?, stream? }\n const isLegacyFormat =\n options &&\n (\"onChunk\" in options || \"onDone\" in options || \"onError\" in options);\n const streamOptions = isLegacyFormat\n ? (options as StreamOptions)\n : (options as CallOptions | undefined)?.stream;\n const timeout = isLegacyFormat\n ? undefined\n : (options as CallOptions | undefined)?.timeout;\n\n // Apply the default timeout as a backstop for non-streaming\n // calls so a lost response rejects instead of hanging forever.\n // An explicit `timeout` (including 0 = disabled) always wins.\n const effectiveTimeout =\n timeout !== undefined\n ? timeout\n : streamOptions\n ? undefined\n : defaultCallTimeoutRef.current;\n\n if (effectiveTimeout) {\n timeoutId = setTimeout(() => {\n const pending = pendingCallsRef.current.get(id);\n pendingCallsRef.current.delete(id);\n const errorMessage = `RPC call to ${method} timed out after ${effectiveTimeout}ms`;\n pending?.stream?.onError?.(errorMessage);\n reject(new Error(errorMessage));\n }, effectiveTimeout);\n }\n\n const rpcRequest: RPCRequest = {\n args,\n id,\n method,\n type: MessageType.RPC\n };\n const request = JSON.stringify(rpcRequest);\n\n pendingCallsRef.current.set(id, {\n reject,\n resolve: resolve as (value: unknown) => void,\n stream: streamOptions,\n timeoutId,\n request,\n sentOn: null\n });\n\n // Transmit immediately if the live socket is open; otherwise the\n // request stays queued and is flushed on the next open event.\n // We never hand requests to a non-open socket: its internal\n // buffer is lost forever if the socket gets replaced.\n const socket = socketRef.current;\n if (socket && socket.readyState === socket.OPEN) {\n socket.send(request);\n const pending = pendingCallsRef.current.get(id);\n if (pending) pending.sentOn = socket;\n }\n });\n },\n // eslint-disable-next-line react-hooks/exhaustive-deps\n []\n );\n\n agent.setState = (newState: State) => {\n // Route through the live socket so stale `agent` references don't\n // write into a replaced socket's dead buffer.\n (socketRef.current ?? agent).send(\n JSON.stringify({ state: newState, type: MessageType.CF_AGENT_STATE })\n );\n setAgentState(newState);\n options.onStateUpdate?.(newState, \"client\");\n };\n\n agent.call = call;\n // Use reactive identity state (updates on identity message)\n agent.agent = identity.agent;\n agent.name = identity.name;\n // Full root-first chain including the leaf. Computed from the\n // user-provided options — the server doesn't need to echo it\n // back because the client already knows. Write past the\n // PartySocket `.path: string` shape via an unknown cast — the\n // overload signatures expose this as `ReadonlyArray<...>`.\n (\n agent as unknown as { path: ReadonlyArray<{ agent: string; name: string }> }\n ).path = fullPath;\n agent.identified = identity.identified;\n agent.ready = readyRef.current!.promise;\n agent.state = agentState;\n mutableAgentRef.current = agent;\n // Memoize stub so it's referentially stable across renders\n // (call is already stable via useCallback)\n const stub = useMemo(() => createStubProxy(call), [call]);\n agent.stub = stub;\n agent.getHttpUrl = () => {\n // TODO: upstream to partysocket — expose an HTTP URL property\n // @ts-expect-error accessing protected PartySocket internals\n const wsUrl: string = (agent._url as string | null) || agent._pkurl || \"\";\n return wsUrl.replace(\"ws://\", \"http://\").replace(\"wss://\", \"https://\");\n };\n\n // warn if agent isn't in lowercase\n if (identity.agent !== identity.agent.toLowerCase()) {\n console.warn(\n \"Agent name: \" +\n identity.agent +\n \" should probably be in lowercase. Received: \" +\n identity.agent\n );\n }\n\n // The overload signatures return `Omit<PartySocket, \"path\"> & { path: ... }`,\n // but `agent` is inferred as the raw PartySocket. Cast to satisfy\n // the overload contract — the runtime override of `agent.path`\n // above ensures the shape matches.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return agent as any;\n}\n\ntype AgentToolEventAgent = Pick<\n PartySocket,\n \"addEventListener\" | \"removeEventListener\"\n>;\n\nfunction agentToolDedupeKey(message: AgentToolEventMessage): string {\n return [\n message.parentToolCallId ?? \"\",\n message.event.runId,\n String(message.sequence)\n ].join(\"\\0\");\n}\n\nexport function useAgentToolEvents(options: { agent: AgentToolEventAgent }): {\n runsById: Record<string, AgentToolRunState>;\n runsByToolCallId: Record<string, AgentToolRunState[]>;\n unboundRuns: AgentToolRunState[];\n getRunsForToolCall(toolCallId: string): AgentToolRunState[];\n resetLocalState(): void;\n} {\n const { agent } = options;\n const [state, setState] = useState<AgentToolEventState>(() =>\n createAgentToolEventState()\n );\n const seenRef = useRef(new Set<string>());\n\n useEffect(() => {\n const onMessage = (event: MessageEvent) => {\n if (typeof event.data !== \"string\") return;\n let message: AgentToolEventMessage;\n try {\n message = JSON.parse(event.data) as AgentToolEventMessage;\n } catch {\n return;\n }\n if (message.type !== \"agent-tool-event\") return;\n const key = agentToolDedupeKey(message);\n if (seenRef.current.has(key)) return;\n seenRef.current.add(key);\n setState((prev) => applyAgentToolEvent(prev, message));\n };\n\n agent.addEventListener(\"message\", onMessage);\n return () => agent.removeEventListener(\"message\", onMessage);\n }, [agent]);\n\n const resetLocalState = useCallback(() => {\n seenRef.current.clear();\n setState(createAgentToolEventState());\n }, []);\n\n const getRunsForToolCall = useCallback(\n (toolCallId: string) => state.runsByToolCallId[toolCallId] ?? [],\n [state.runsByToolCallId]\n );\n\n return {\n ...state,\n getRunsForToolCall,\n resetLocalState\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"react.js","names":["AgentConnectionErrorCtor"],"sources":["../src/react.tsx"],"sourcesContent":["import type { PartySocket } from \"partysocket\";\nimport { usePartySocket } from \"partysocket/react\";\nimport { useCallback, useRef, use, useMemo, useState, useEffect } from \"react\";\nimport type { MCPServersState, RPCRequest, RPCResponse } from \"./\";\nimport type {\n AgentConnectionError,\n AgentPromiseReturnType,\n AgentStub,\n CallOptions,\n OptionalAgentMethods,\n RequiredAgentMethods,\n StreamOptions,\n UntypedAgentStub\n} from \"./client\";\nimport type { ClientParameters } from \"./serializable\";\nimport {\n createStubProxy,\n DEFAULT_CALL_TIMEOUT_MS,\n AgentConnectionError as AgentConnectionErrorCtor,\n isTerminalCloseEvent\n} from \"./client\";\nimport { camelCaseToKebabCase } from \"./utils\";\nimport { MessageType } from \"./types\";\nimport {\n applyAgentToolEvent,\n createAgentToolEventState,\n type AgentToolEventMessage,\n type AgentToolEventState,\n type AgentToolRunPart,\n type AgentToolRunState\n} from \"./chat/agent-tools\";\n\ntype QueryObject = Record<string, string | null>;\ntype TerminalReconnectOptions = {\n shouldReconnectOnClose?: (event: CloseEvent) => boolean;\n};\n\ninterface CacheEntry {\n promise: Promise<QueryObject>;\n expiresAt: number;\n}\n\nconst queryCache = new Map<string, CacheEntry>();\n\nfunction createCacheKey(\n agentNamespace: string,\n name: string | undefined,\n subChainOrDeps: ReadonlyArray<{ agent: string; name: string }> | unknown[],\n deps?: unknown[]\n): string {\n // Backwards-compatible overload: if called with 3 args, the third\n // argument is `deps` and `subChain` defaults to empty. With 4 args,\n // the third is the sub-chain. This keeps existing callers (and\n // the `_testUtils` surface) working while letting new callers\n // include the nested chain in the cache key.\n //\n // Empty sub-chain must produce the same key as the old 3-arg\n // form, so nested-addressing code can opt-in without invalidating\n // existing caches.\n if (deps === undefined) {\n return JSON.stringify([\n agentNamespace,\n name || \"default\",\n ...(subChainOrDeps as unknown[])\n ]);\n }\n const subChain = subChainOrDeps as ReadonlyArray<{\n agent: string;\n name: string;\n }>;\n if (subChain.length === 0) {\n return JSON.stringify([agentNamespace, name || \"default\", ...deps]);\n }\n return JSON.stringify([\n agentNamespace,\n name || \"default\",\n subChain.map((s) => [s.agent, s.name]),\n ...deps\n ]);\n}\n\n/** Build a URL path tail `/sub/{agent-kebab}/{name}/...` from a sub chain. */\nfunction buildSubPath(\n subChain: ReadonlyArray<{ agent: string; name: string }>,\n extraPath?: string\n): string {\n if (subChain.length === 0) return extraPath ?? \"\";\n const parts = subChain.flatMap((step) => [\n \"sub\",\n camelCaseToKebabCase(step.agent),\n encodeURIComponent(step.name)\n ]);\n const combined = parts.join(\"/\");\n if (extraPath) {\n const trimmed = extraPath.startsWith(\"/\") ? extraPath.slice(1) : extraPath;\n return `${combined}/${trimmed}`;\n }\n return combined;\n}\n\nfunction getCacheEntry(key: string): CacheEntry | undefined {\n const entry = queryCache.get(key);\n if (!entry) return undefined;\n\n if (Date.now() >= entry.expiresAt) {\n queryCache.delete(key);\n return undefined;\n }\n\n return entry;\n}\n\nfunction setCacheEntry(\n key: string,\n promise: Promise<QueryObject>,\n cacheTtl: number\n): CacheEntry {\n const entry: CacheEntry = {\n promise,\n expiresAt: Date.now() + cacheTtl\n };\n queryCache.set(key, entry);\n return entry;\n}\n\nfunction deleteCacheEntry(key: string): void {\n queryCache.delete(key);\n}\n\n// Export for testing purposes\nexport const _testUtils = {\n queryCache,\n setCacheEntry,\n getCacheEntry,\n deleteCacheEntry,\n clearCache: () => queryCache.clear(),\n createStubProxy,\n createCacheKey\n};\n\n/**\n * Options for the useAgent hook\n * @template State Type of the Agent's state\n */\nexport type UseAgentOptions<State = unknown> = Omit<\n Parameters<typeof usePartySocket>[0],\n \"party\" | \"room\" | \"query\"\n> &\n TerminalReconnectOptions & {\n /** Name of the agent to connect to (ignored if basePath is set) */\n agent: string;\n /** Name of the specific Agent instance (ignored if basePath is set) */\n name?: string;\n /**\n * Full URL path - bypasses agent/name URL construction.\n * When set, the client connects to this path directly.\n * Server must handle routing manually (e.g., with getAgentByName + fetch).\n * @example\n * // Client connects to /user, server routes based on session\n * useAgent({ agent: \"UserAgent\", basePath: \"user\" })\n */\n basePath?: string;\n /** Query parameters - can be static object or async function */\n query?: QueryObject | (() => Promise<QueryObject>);\n /** Dependencies for async query caching */\n queryDeps?: unknown[];\n /** Cache TTL in milliseconds for auth tokens/time-sensitive data */\n cacheTtl?: number;\n /** Called when the Agent's state is updated */\n onStateUpdate?: (state: State, source: \"server\" | \"client\") => void;\n /** Called when a state update fails (e.g., connection is readonly) */\n onStateUpdateError?: (error: string) => void;\n /** Called when MCP server state is updated */\n onMcpUpdate?: (mcpServers: MCPServersState) => void;\n /**\n * Called when the server sends the agent's identity on connect.\n * Useful when using basePath, as the actual instance name is determined server-side.\n * @param name The actual agent instance name\n * @param agent The agent class name (kebab-case)\n */\n onIdentity?: (name: string, agent: string) => void;\n /**\n * Called when identity changes on reconnect (different instance than before).\n * If not provided and identity changes, a warning will be logged.\n * @param oldName Previous instance name\n * @param newName New instance name\n * @param oldAgent Previous agent class name\n * @param newAgent New agent class name\n */\n onIdentityChange?: (\n oldName: string,\n newName: string,\n oldAgent: string,\n newAgent: string\n ) => void;\n /**\n * Additional path to append to the URL.\n * Works with both standard routing and basePath.\n * @example\n * // With basePath: /user/settings\n * { basePath: \"user\", path: \"settings\" }\n * // Standard: /agents/my-agent/room/settings\n * { agent: \"MyAgent\", name: \"room\", path: \"settings\" }\n */\n path?: string;\n /**\n * Connect to a sub-agent (facet) via its parent. Flat array,\n * root-first. Each step addresses one parent↔child hop.\n *\n * The hook's returned `.agent` / `.name` report the **leaf**\n * identity (the deepest entry in `sub`), so downstream hooks\n * like `useAgentChat` see the child they actually talk to.\n * `.path` exposes the full chain for observability, deep links,\n * and reconnect keying.\n *\n * @example\n * ```ts\n * // Two-level nesting: Inbox (Alice) → Chat (abc)\n * useAgent({\n * agent: \"inbox\", name: userId,\n * sub: [{ agent: \"chat\", name: chatId }]\n * });\n *\n * // Three-level: tenant → inbox → chat\n * useAgent({\n * agent: \"tenant\", name: tenantId,\n * sub: [\n * { agent: \"inbox\", name: userId },\n * { agent: \"chat\", name: chatId }\n * ]\n * });\n * ```\n *\n * @experimental The API surface may change before stabilizing.\n */\n sub?: ReadonlyArray<{ agent: string; name: string }>;\n /**\n * Default timeout (in milliseconds) applied to non-streaming `call()`s\n * that don't pass an explicit `timeout`. Acts as a backstop so calls\n * whose response is lost (e.g. the connection is replaced mid-flight)\n * reject instead of hanging forever.\n *\n * Defaults to 30 000 ms. Set to `0` to disable the default timeout.\n * Streaming calls never get a default timeout (long-lived streams are\n * legitimate); pass an explicit `timeout` to bound them.\n */\n defaultCallTimeout?: number;\n /** Called when the connection closes with a terminal code and will not reconnect. */\n onConnectionError?: (error: AgentConnectionError) => void;\n };\n\ntype OptionalArgsAgentMethodCall<AgentT> = <\n K extends keyof OptionalAgentMethods<AgentT>\n>(\n method: K,\n args?: ClientParameters<OptionalAgentMethods<AgentT>[K]>,\n options?: CallOptions | StreamOptions\n) => AgentPromiseReturnType<AgentT, K>;\n\ntype RequiredArgsAgentMethodCall<AgentT> = <\n K extends keyof RequiredAgentMethods<AgentT>\n>(\n method: K,\n args: ClientParameters<RequiredAgentMethods<AgentT>[K]>,\n options?: CallOptions | StreamOptions\n) => AgentPromiseReturnType<AgentT, K>;\n\ntype AgentMethodCall<AgentT> = OptionalArgsAgentMethodCall<AgentT> &\n RequiredArgsAgentMethodCall<AgentT>;\n\ntype UntypedAgentMethodCall = <T = unknown>(\n method: string,\n args?: unknown[],\n options?: CallOptions | StreamOptions\n) => Promise<T>;\n\n/**\n * React hook for connecting to an Agent\n */\nexport function useAgent<State = unknown>(\n options: UseAgentOptions<State>\n): Omit<PartySocket, \"path\"> & {\n agent: string;\n name: string;\n /** Full root-first address chain, including leaf. Single entry when `sub` isn't set. */\n path: ReadonlyArray<{ agent: string; name: string }>;\n identified: boolean;\n ready: Promise<void>;\n state: State | undefined;\n connectionError: AgentConnectionError | null;\n setState: (state: State) => void;\n call: UntypedAgentMethodCall;\n stub: UntypedAgentStub;\n getHttpUrl: () => string;\n};\nexport function useAgent<\n AgentT extends {\n get state(): State;\n },\n State\n>(\n options: UseAgentOptions<State>\n): Omit<PartySocket, \"path\"> & {\n agent: string;\n name: string;\n path: ReadonlyArray<{ agent: string; name: string }>;\n identified: boolean;\n ready: Promise<void>;\n state: State | undefined;\n connectionError: AgentConnectionError | null;\n setState: (state: State) => void;\n call: AgentMethodCall<AgentT>;\n stub: AgentStub<AgentT>;\n getHttpUrl: () => string;\n};\nexport function useAgent<State>(options: UseAgentOptions<unknown>): Omit<\n PartySocket,\n \"path\"\n> & {\n agent: string;\n name: string;\n path: ReadonlyArray<{ agent: string; name: string }>;\n identified: boolean;\n ready: Promise<void>;\n state: State | undefined;\n connectionError: AgentConnectionError | null;\n setState: (state: State) => void;\n call: UntypedAgentMethodCall | AgentMethodCall<unknown>;\n stub: UntypedAgentStub;\n getHttpUrl: () => string;\n} {\n const agentNamespace = camelCaseToKebabCase(options.agent);\n // NOTE: `path` is destructured out (as `userPath`) so it does NOT\n // end up in `restOptions`. Spreading `restOptions` after the\n // computed `path: combinedPath` would otherwise let the user's raw\n // `path` overwrite the combined sub-agent URL, dropping every\n // `/sub/{child}/{name}` segment on the way to the socket.\n const {\n query,\n queryDeps,\n cacheTtl,\n sub: subOption,\n path: userPath,\n defaultCallTimeout,\n onConnectionError,\n shouldReconnectOnClose,\n ...restOptions\n } = options;\n\n const subChain = useMemo(\n () => (subOption ?? []).map((s) => ({ agent: s.agent, name: s.name })),\n // Stable serialization — deep changes re-memoize.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [JSON.stringify(subOption ?? [])]\n );\n\n // The \"leaf\" is the deepest entry in the chain; it's what\n // downstream code (useAgentChat etc.) should see as the\n // authoritative identity.\n const leafAgent =\n subChain.length > 0 ? subChain[subChain.length - 1].agent : options.agent;\n const leafName =\n subChain.length > 0\n ? subChain[subChain.length - 1].name\n : options.name || \"default\";\n\n // Full root-first chain, including the leaf. Exposed as `.path`\n // and used for cache keying so nested sessions with the same leaf\n // name don't collide.\n const fullPath = useMemo(\n () => [\n { agent: options.agent, name: options.name || \"default\" },\n ...subChain\n ],\n [options.agent, options.name, subChain]\n );\n\n // Keep track of pending RPC calls.\n //\n // Each entry is tagged with the socket the request was transmitted on\n // (`sentOn`). Requests are only handed to a socket once it's OPEN —\n // until then they stay queued here (`sentOn: null`). This matters\n // because `usePartySocket` *replaces* the socket object whenever\n // connection options change (async query refresh, path change, etc.):\n // anything buffered inside a replaced socket is lost forever, and a\n // call transmitted on a replaced socket can never receive its\n // response. Tagging lets us:\n // - flush still-queued requests on whichever socket connects next\n // (safe: they were never transmitted, so no double-execution risk)\n // - reject calls transmitted on a socket that closed or was replaced\n // (their response can never arrive)\n // - avoid rejecting calls in flight on the *new* socket when a stale\n // close event from an old socket trickles in\n const pendingCallsRef = useRef(\n new Map<\n string,\n {\n resolve: (value: unknown) => void;\n reject: (error: Error) => void;\n stream?: StreamOptions;\n timeoutId?: ReturnType<typeof setTimeout>;\n /** Serialized RPC request, kept so it can be (re)transmitted */\n request: string;\n /** Socket the request was transmitted on; null while queued */\n sentOn: PartySocket | null;\n }\n >()\n );\n\n // Always points at the socket from the latest render. `call`,\n // `setState`, and the queue-flushing logic go through this ref so\n // that stale `agent` references held by old effect closures still\n // route their traffic to the live socket instead of a dead one.\n const socketRef = useRef<PartySocket | null>(null);\n\n const defaultCallTimeoutRef = useRef(\n defaultCallTimeout ?? DEFAULT_CALL_TIMEOUT_MS\n );\n defaultCallTimeoutRef.current = defaultCallTimeout ?? DEFAULT_CALL_TIMEOUT_MS;\n\n /** Reject (and remove) every pending call transmitted on `socket`. */\n const rejectCallsSentOn = (socket: PartySocket, reason: string) => {\n const error = new Error(reason);\n for (const [id, pending] of pendingCallsRef.current) {\n if (pending.sentOn === socket) {\n if (pending.timeoutId) clearTimeout(pending.timeoutId);\n pendingCallsRef.current.delete(id);\n pending.reject(error);\n pending.stream?.onError?.(reason);\n }\n }\n };\n\n /** Transmit queued (never-sent) calls if the live socket is open. */\n const flushQueuedCalls = () => {\n const socket = socketRef.current;\n if (!socket || socket.readyState !== socket.OPEN) return;\n for (const pending of pendingCallsRef.current.values()) {\n if (pending.sentOn === null) {\n socket.send(pending.request);\n pending.sentOn = socket;\n }\n }\n };\n\n /** Reject (and remove) every still-queued (never transmitted) call. */\n const rejectQueuedCalls = (reason: string) => {\n const error = new Error(reason);\n for (const [id, pending] of pendingCallsRef.current) {\n if (pending.sentOn === null) {\n if (pending.timeoutId) clearTimeout(pending.timeoutId);\n pendingCallsRef.current.delete(id);\n pending.reject(error);\n pending.stream?.onError?.(reason);\n }\n }\n };\n\n const cacheKey = useMemo(\n () =>\n createCacheKey(agentNamespace, options.name, subChain, queryDeps || []),\n [agentNamespace, options.name, subChain, queryDeps]\n );\n\n // Track current cache key in a ref for use in onClose handler.\n // This ensures we invalidate the correct cache entry when the connection closes,\n // even if the component re-renders with different props before onClose fires.\n // We update synchronously during render (not in useEffect) to avoid race\n // conditions where onClose could fire before the effect runs.\n const cacheKeyRef = useRef(cacheKey);\n cacheKeyRef.current = cacheKey;\n\n const ttl = cacheTtl ?? 5 * 60 * 1000;\n\n // Track cache invalidation to force re-render when TTL expires\n const [cacheInvalidatedAt, setCacheInvalidatedAt] = useState<number>(0);\n\n // Disable socket while waiting for async query to refresh after disconnect\n const isAsyncQuery = query && typeof query === \"function\";\n const [awaitingQueryRefresh, setAwaitingQueryRefresh] = useState(false);\n\n // Get or create the query promise\n const queryPromise = useMemo(() => {\n // Re-run when cache is invalidated after TTL expiry\n void cacheInvalidatedAt;\n\n if (!query || typeof query !== \"function\") {\n return null;\n }\n\n // Always check cache first to deduplicate concurrent requests\n const cached = getCacheEntry(cacheKey);\n if (cached) {\n return cached.promise;\n }\n\n // Create new promise\n const promise = query().catch((error) => {\n console.error(\n `[useAgent] Query failed for agent \"${options.agent}\":`,\n error\n );\n deleteCacheEntry(cacheKey);\n throw error;\n });\n\n // Always cache to deduplicate concurrent requests\n setCacheEntry(cacheKey, promise, ttl);\n\n return promise;\n }, [cacheKey, query, options.agent, ttl, cacheInvalidatedAt]);\n\n // Schedule cache invalidation when TTL expires\n useEffect(() => {\n if (!queryPromise || ttl <= 0) return;\n\n const entry = getCacheEntry(cacheKey);\n if (!entry) return;\n\n const timeUntilExpiry = entry.expiresAt - Date.now();\n\n // Always set a timer (with min 0ms) to ensure cleanup function is returned\n const timer = setTimeout(\n () => {\n deleteCacheEntry(cacheKey);\n setCacheInvalidatedAt(Date.now());\n },\n Math.max(0, timeUntilExpiry)\n );\n\n return () => clearTimeout(timer);\n }, [cacheKey, queryPromise, ttl]);\n\n let resolvedQuery: QueryObject | undefined;\n\n if (query) {\n if (typeof query === \"function\") {\n // Use React's use() to resolve the promise\n const queryResult = use(queryPromise!);\n\n // Check for non-primitive values and warn\n if (queryResult) {\n for (const [key, value] of Object.entries(queryResult)) {\n if (\n value !== null &&\n value !== undefined &&\n typeof value !== \"string\" &&\n typeof value !== \"number\" &&\n typeof value !== \"boolean\"\n ) {\n console.warn(\n `[useAgent] Query parameter \"${key}\" is an object and will be converted to \"[object Object]\". ` +\n \"Query parameters should be string, number, boolean, or null.\"\n );\n }\n }\n resolvedQuery = queryResult;\n }\n } else {\n // Sync query - use directly\n resolvedQuery = query;\n }\n }\n\n // Re-enable socket after async query resolves\n useEffect(() => {\n if (awaitingQueryRefresh && resolvedQuery !== undefined) {\n setAwaitingQueryRefresh(false);\n }\n }, [awaitingQueryRefresh, resolvedQuery]);\n\n // Track agent state for reactivity — updated on server broadcasts and client setState\n const [agentState, setAgentState] = useState<State | undefined>(undefined);\n const [connectionError, setConnectionError] =\n useState<AgentConnectionError | null>(null);\n const connectionErrorRef = useRef<AgentConnectionError | null>(null);\n const connectionErrorAddressKeyRef = useRef<string | null>(null);\n const shouldReconnectOnCloseRef = useRef(shouldReconnectOnClose);\n shouldReconnectOnCloseRef.current = shouldReconnectOnClose;\n const classifyReconnect = useCallback(\n (event: CloseEvent) =>\n (shouldReconnectOnCloseRef.current?.(event) ?? true) &&\n !isTerminalCloseEvent(event),\n []\n );\n\n // Store identity in React state for reactivity. Seed with the\n // leaf's address — what the server will echo back in\n // `cf_agent_identity`.\n const [identity, setIdentity] = useState({\n name: leafName,\n agent: camelCaseToKebabCase(leafAgent),\n identified: false\n });\n\n // Track previous identity for change detection\n const previousIdentityRef = useRef<{\n name: string | null;\n agent: string | null;\n }>({ name: null, agent: null });\n\n // Ready promise - resolves when identity is received, resets on close\n const readyRef = useRef<\n { promise: Promise<void>; resolve: () => void } | undefined\n >(undefined);\n\n const resetReady = () => {\n let resolve: () => void;\n const promise = new Promise<void>((r) => {\n resolve = r;\n });\n readyRef.current = { promise, resolve: resolve! };\n };\n\n if (!readyRef.current) {\n resetReady();\n }\n\n const mutableAgentRef = useRef<{\n agent: string;\n name: string;\n identified: boolean;\n } | null>(null);\n\n // Combine the sub-agent chain with the user-provided `path`.\n // Order matters: `/sub/{child}/{name}/...` comes before `path` so\n // the server sees the hierarchy it expects.\n const combinedPath = useMemo(\n () => buildSubPath(subChain, userPath),\n [subChain, userPath]\n );\n\n // If basePath is provided, use it directly; otherwise construct from agent/name\n const socketOptions = options.basePath\n ? {\n basePath: options.basePath,\n path: combinedPath || undefined,\n query: resolvedQuery,\n ...restOptions,\n shouldReconnectOnClose: classifyReconnect\n }\n : {\n party: agentNamespace,\n prefix: \"agents\",\n room: options.name || \"default\",\n path: combinedPath || undefined,\n query: resolvedQuery,\n ...restOptions,\n shouldReconnectOnClose: classifyReconnect\n };\n\n const socketEnabled = !awaitingQueryRefresh && (restOptions.enabled ?? true);\n\n // Identifies *which agent instance* this hook is addressing. Queued\n // (never-transmitted) RPC calls are only safe to flush onto a later\n // socket if it still points at the same instance — a call composed for\n // agent \"alpha\" must not execute on agent \"beta\" just because the\n // `name` prop changed while the call was waiting for a connection.\n // Credentials (query params) are deliberately excluded: a token\n // refresh doesn't change where calls go.\n const addressKey = JSON.stringify([\n options.host ?? null,\n options.basePath ?? null,\n agentNamespace,\n options.name || \"default\",\n combinedPath || null\n ]);\n const visibleConnectionError =\n connectionErrorAddressKeyRef.current === addressKey\n ? connectionError\n : null;\n connectionErrorRef.current = visibleConnectionError;\n\n const agent = usePartySocket({\n ...socketOptions,\n enabled: socketEnabled,\n onOpen: (event: Event) => {\n connectionErrorAddressKeyRef.current = null;\n setConnectionError(null);\n // The socket is open: transmit any RPC requests that were issued\n // while disconnected (or while a previous socket was being\n // replaced). They were never handed to a socket before, so this\n // cannot double-execute anything server-side.\n flushQueuedCalls();\n options.onOpen?.(event);\n },\n onMessage: (message) => {\n if (typeof message.data === \"string\") {\n let parsedMessage: Record<string, unknown>;\n try {\n parsedMessage = JSON.parse(message.data);\n } catch (_error) {\n // silently ignore invalid messages for now\n // TODO: log errors with log levels\n return options.onMessage?.(message);\n }\n if (parsedMessage.type === MessageType.CF_AGENT_IDENTITY) {\n const oldName = previousIdentityRef.current.name;\n const oldAgent = previousIdentityRef.current.agent;\n const newName = parsedMessage.name as string;\n const newAgent = parsedMessage.agent as string;\n\n const currentAgent = mutableAgentRef.current;\n if (currentAgent) {\n currentAgent.name = newName;\n currentAgent.agent = newAgent;\n currentAgent.identified = true;\n }\n\n // Update reactive state (triggers re-render)\n setIdentity({ name: newName, agent: newAgent, identified: true });\n\n // Resolve ready promise\n readyRef.current?.resolve();\n\n // Detect identity change on reconnect\n if (\n oldName !== null &&\n oldAgent !== null &&\n (oldName !== newName || oldAgent !== newAgent)\n ) {\n if (options.onIdentityChange) {\n options.onIdentityChange(oldName, newName, oldAgent, newAgent);\n } else {\n const agentChanged = oldAgent !== newAgent;\n const nameChanged = oldName !== newName;\n let changeDescription = \"\";\n if (agentChanged && nameChanged) {\n changeDescription = `agent \"${oldAgent}\" → \"${newAgent}\", instance \"${oldName}\" → \"${newName}\"`;\n } else if (agentChanged) {\n changeDescription = `agent \"${oldAgent}\" → \"${newAgent}\"`;\n } else {\n changeDescription = `instance \"${oldName}\" → \"${newName}\"`;\n }\n console.warn(\n `[agents] Identity changed on reconnect: ${changeDescription}. ` +\n \"This can happen with server-side routing (e.g., basePath with getAgentByName) \" +\n \"where the instance is determined by auth/session. \" +\n \"Provide onIdentityChange callback to handle this explicitly, \" +\n \"or ignore if this is expected for your routing pattern.\"\n );\n }\n }\n\n // Track for next change detection\n previousIdentityRef.current = { name: newName, agent: newAgent };\n\n // Call onIdentity callback\n options.onIdentity?.(newName, newAgent);\n return;\n }\n if (parsedMessage.type === MessageType.CF_AGENT_STATE) {\n setAgentState(parsedMessage.state as State);\n options.onStateUpdate?.(parsedMessage.state as State, \"server\");\n return;\n }\n if (parsedMessage.type === MessageType.CF_AGENT_STATE_ERROR) {\n options.onStateUpdateError?.(parsedMessage.error as string);\n return;\n }\n if (parsedMessage.type === MessageType.CF_AGENT_MCP_SERVERS) {\n options.onMcpUpdate?.(parsedMessage.mcp as MCPServersState);\n return;\n }\n if (parsedMessage.type === MessageType.RPC) {\n const response = parsedMessage as RPCResponse;\n const pending = pendingCallsRef.current.get(response.id);\n if (!pending) {\n console.warn(\n `[useAgent] Discarded an RPC response with no matching pending call (id \"${response.id}\"). ` +\n \"The call likely timed out or was rejected when its connection closed before the response arrived.\"\n );\n return;\n }\n\n if (!response.success) {\n if (pending.timeoutId) clearTimeout(pending.timeoutId);\n pending.reject(new Error(response.error));\n pendingCallsRef.current.delete(response.id);\n pending.stream?.onError?.(response.error);\n return;\n }\n\n // Handle streaming responses\n if (\"done\" in response) {\n if (response.done) {\n if (pending.timeoutId) clearTimeout(pending.timeoutId);\n pending.resolve(response.result);\n pendingCallsRef.current.delete(response.id);\n pending.stream?.onDone?.(response.result);\n } else {\n pending.stream?.onChunk?.(response.result);\n }\n } else {\n // Non-streaming response\n if (pending.timeoutId) clearTimeout(pending.timeoutId);\n pending.resolve(response.result);\n pendingCallsRef.current.delete(response.id);\n }\n return;\n }\n }\n options.onMessage?.(message);\n },\n onClose: (event: CloseEvent) => {\n // Identify which socket actually closed. Close events are\n // dispatched asynchronously, so a close from an old socket that\n // was just replaced can arrive while a new socket is already\n // connecting (or connected). `event.target` is the PartySocket\n // that dispatched the event; fall back to the live socket if the\n // environment doesn't populate it.\n const closedSocket =\n (event.target as PartySocket | null) ?? socketRef.current;\n const isCurrentSocket = closedSocket === socketRef.current;\n const terminalClose = isTerminalCloseEvent(event);\n\n // Calls transmitted on the closed socket can never receive their\n // response — reject them. Calls still queued (never transmitted)\n // stay pending and are flushed when a socket next opens; calls\n // in flight on a *different* (newer) socket are untouched.\n if (closedSocket) {\n rejectCallsSentOn(closedSocket, \"Connection closed\");\n if (isCurrentSocket && !closedSocket.shouldReconnect) {\n rejectQueuedCalls(\"Connection closed\");\n }\n }\n\n if (isCurrentSocket) {\n // Reset ready state for next connection\n resetReady();\n if (mutableAgentRef.current) {\n mutableAgentRef.current.identified = false;\n }\n setIdentity((prev) => ({ ...prev, identified: false }));\n\n if (closedSocket?.shouldReconnect) {\n // Pause reconnection for async queries until fresh query params are ready.\n if (isAsyncQuery) {\n setAwaitingQueryRefresh(true);\n }\n\n // Invalidate cache and trigger re-render to fetch fresh query params.\n deleteCacheEntry(cacheKeyRef.current);\n setCacheInvalidatedAt(Date.now());\n }\n\n if (!closedSocket?.shouldReconnect && terminalClose) {\n const error = new AgentConnectionErrorCtor(event);\n connectionErrorAddressKeyRef.current = addressKey;\n setConnectionError(error);\n onConnectionError?.(error);\n }\n }\n\n // Call user's onClose if provided\n options.onClose?.(event);\n }\n }) as PartySocket & {\n agent: string;\n name: string;\n identified: boolean;\n ready: Promise<void>;\n state: State | undefined;\n connectionError: AgentConnectionError | null;\n setState: (state: State) => void;\n call: UntypedAgentMethodCall;\n stub: UntypedAgentStub;\n getHttpUrl: () => string;\n };\n // Update the live-socket ref before anything below can use it.\n socketRef.current = agent;\n\n // When `usePartySocket` replaces the socket object (connection options\n // changed — async query refresh, path change, enabled toggle, ...) the\n // old socket's event listeners are detached at the same commit, so its\n // final close event may never be observed by our onClose handler.\n // Sweep here instead: anything transmitted on the old socket can never\n // get a response, and the identity it established no longer applies.\n // Queued (never-transmitted) calls survive and flush when the new\n // socket opens.\n const prevSocketRef = useRef<PartySocket | null>(null);\n const prevAddressKeyRef = useRef(addressKey);\n useEffect(() => {\n const prev = prevSocketRef.current;\n prevSocketRef.current = agent;\n const prevAddress = prevAddressKeyRef.current;\n prevAddressKeyRef.current = addressKey;\n\n // Destination guard: if the agent address changed (different agent,\n // name, or path — not just refreshed credentials), calls that are\n // still queued were composed for the *old* instance. Reject them\n // before anything can flush them onto the new instance.\n if (prevAddress !== addressKey) {\n connectionErrorAddressKeyRef.current = null;\n setConnectionError(null);\n rejectQueuedCalls(\n \"Call discarded: the agent address changed before the request could be sent\"\n );\n }\n\n if (prev && prev !== agent) {\n rejectCallsSentOn(prev, \"Connection closed\");\n resetReady();\n if (mutableAgentRef.current) {\n mutableAgentRef.current.identified = false;\n }\n setIdentity((current) =>\n current.identified ? { ...current, identified: false } : current\n );\n }\n // The helpers only touch refs; re-running on socket/address change is all we need.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [agent, addressKey]);\n\n // Create the call method. Deliberately dependency-free: it routes\n // through refs, so even a stale `agent` reference captured by an old\n // effect closure issues calls against the live socket.\n const call = useCallback(\n <T = unknown,>(\n method: string,\n args: unknown[] = [],\n options?: CallOptions | StreamOptions\n ): Promise<T> => {\n return new Promise((resolve, reject) => {\n const socket = socketRef.current;\n if (\n socket &&\n connectionErrorRef.current &&\n socket.readyState === socket.CLOSED\n ) {\n reject(new Error(\"Connection closed\"));\n return;\n }\n\n const id = crypto.randomUUID();\n let timeoutId: ReturnType<typeof setTimeout> | undefined;\n\n // Detect legacy format: { onChunk?, onDone?, onError? } vs new format: { timeout?, stream? }\n const isLegacyFormat =\n options &&\n (\"onChunk\" in options || \"onDone\" in options || \"onError\" in options);\n const streamOptions = isLegacyFormat\n ? (options as StreamOptions)\n : (options as CallOptions | undefined)?.stream;\n const timeout = isLegacyFormat\n ? undefined\n : (options as CallOptions | undefined)?.timeout;\n\n // Apply the default timeout as a backstop for non-streaming\n // calls so a lost response rejects instead of hanging forever.\n // An explicit `timeout` (including 0 = disabled) always wins.\n const effectiveTimeout =\n timeout !== undefined\n ? timeout\n : streamOptions\n ? undefined\n : defaultCallTimeoutRef.current;\n\n if (effectiveTimeout) {\n timeoutId = setTimeout(() => {\n const pending = pendingCallsRef.current.get(id);\n pendingCallsRef.current.delete(id);\n const errorMessage = `RPC call to ${method} timed out after ${effectiveTimeout}ms`;\n pending?.stream?.onError?.(errorMessage);\n reject(new Error(errorMessage));\n }, effectiveTimeout);\n }\n\n const rpcRequest: RPCRequest = {\n args,\n id,\n method,\n type: MessageType.RPC\n };\n const request = JSON.stringify(rpcRequest);\n\n pendingCallsRef.current.set(id, {\n reject,\n resolve: resolve as (value: unknown) => void,\n stream: streamOptions,\n timeoutId,\n request,\n sentOn: null\n });\n\n // Transmit immediately if the live socket is open; otherwise the\n // request stays queued and is flushed on the next open event.\n // We never hand requests to a non-open socket: its internal\n // buffer is lost forever if the socket gets replaced.\n if (socket && socket.readyState === socket.OPEN) {\n socket.send(request);\n const pending = pendingCallsRef.current.get(id);\n if (pending) pending.sentOn = socket;\n }\n });\n },\n // eslint-disable-next-line react-hooks/exhaustive-deps\n []\n );\n\n agent.setState = (newState: State) => {\n // Route through the live socket so stale `agent` references don't\n // write into a replaced socket's dead buffer.\n (socketRef.current ?? agent).send(\n JSON.stringify({ state: newState, type: MessageType.CF_AGENT_STATE })\n );\n setAgentState(newState);\n options.onStateUpdate?.(newState, \"client\");\n };\n\n agent.call = call;\n // Use reactive identity state (updates on identity message)\n agent.agent = identity.agent;\n agent.name = identity.name;\n // Full root-first chain including the leaf. Computed from the\n // user-provided options — the server doesn't need to echo it\n // back because the client already knows. Write past the\n // PartySocket `.path: string` shape via an unknown cast — the\n // overload signatures expose this as `ReadonlyArray<...>`.\n (\n agent as unknown as { path: ReadonlyArray<{ agent: string; name: string }> }\n ).path = fullPath;\n agent.identified = identity.identified;\n agent.ready = readyRef.current!.promise;\n agent.state = agentState;\n agent.connectionError = visibleConnectionError;\n mutableAgentRef.current = agent;\n // Memoize stub so it's referentially stable across renders\n // (call is already stable via useCallback)\n const stub = useMemo(() => createStubProxy(call), [call]);\n agent.stub = stub;\n agent.getHttpUrl = () => {\n // TODO: upstream to partysocket — expose an HTTP URL property\n // @ts-expect-error accessing protected PartySocket internals\n const wsUrl: string = (agent._url as string | null) || agent._pkurl || \"\";\n return wsUrl.replace(\"ws://\", \"http://\").replace(\"wss://\", \"https://\");\n };\n\n // warn if agent isn't in lowercase\n if (identity.agent !== identity.agent.toLowerCase()) {\n console.warn(\n \"Agent name: \" +\n identity.agent +\n \" should probably be in lowercase. Received: \" +\n identity.agent\n );\n }\n\n // The overload signatures return `Omit<PartySocket, \"path\"> & { path: ... }`,\n // but `agent` is inferred as the raw PartySocket. Cast to satisfy\n // the overload contract — the runtime override of `agent.path`\n // above ensures the shape matches.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return agent as any;\n}\n\ntype AgentToolEventAgent = Pick<\n PartySocket,\n \"addEventListener\" | \"removeEventListener\"\n>;\n\nfunction agentToolDedupeKey(message: AgentToolEventMessage): string {\n return [\n message.parentToolCallId ?? \"\",\n message.event.runId,\n String(message.sequence)\n ].join(\"\\0\");\n}\n\nexport function useAgentToolEvents<\n Part extends AgentToolRunPart = AgentToolRunPart\n>(options: {\n agent: AgentToolEventAgent;\n}): {\n runsById: Record<string, AgentToolRunState<Part>>;\n runsByToolCallId: Record<string, AgentToolRunState<Part>[]>;\n unboundRuns: AgentToolRunState<Part>[];\n getRunsForToolCall(toolCallId: string): AgentToolRunState<Part>[];\n resetLocalState(): void;\n} {\n const { agent } = options;\n const [state, setState] = useState<AgentToolEventState<Part>>(() =>\n createAgentToolEventState<Part>()\n );\n const seenRef = useRef(new Set<string>());\n\n useEffect(() => {\n const onMessage = (event: MessageEvent) => {\n if (typeof event.data !== \"string\") return;\n let message: AgentToolEventMessage;\n try {\n message = JSON.parse(event.data) as AgentToolEventMessage;\n } catch {\n return;\n }\n if (message.type !== \"agent-tool-event\") return;\n const key = agentToolDedupeKey(message);\n if (seenRef.current.has(key)) return;\n seenRef.current.add(key);\n setState((prev) => applyAgentToolEvent<Part>(prev, message));\n };\n\n agent.addEventListener(\"message\", onMessage);\n return () => agent.removeEventListener(\"message\", onMessage);\n }, [agent]);\n\n const resetLocalState = useCallback(() => {\n seenRef.current.clear();\n setState(createAgentToolEventState<Part>());\n }, []);\n\n const getRunsForToolCall = useCallback(\n (toolCallId: string) => state.runsByToolCallId[toolCallId] ?? [],\n [state.runsByToolCallId]\n );\n\n return {\n ...state,\n getRunsForToolCall,\n resetLocalState\n };\n}\n"],"mappings":";;;;;;;AA0CA,MAAM,6BAAa,IAAI,IAAwB;AAE/C,SAAS,eACP,gBACA,MACA,gBACA,MACQ;CAUR,IAAI,SAAS,KAAA,GACX,OAAO,KAAK,UAAU;EACpB;EACA,QAAQ;EACR,GAAI;CACN,CAAC;CAEH,MAAM,WAAW;CAIjB,IAAI,SAAS,WAAW,GACtB,OAAO,KAAK,UAAU;EAAC;EAAgB,QAAQ;EAAW,GAAG;CAAI,CAAC;CAEpE,OAAO,KAAK,UAAU;EACpB;EACA,QAAQ;EACR,SAAS,KAAK,MAAM,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC;EACrC,GAAG;CACL,CAAC;AACH;;AAGA,SAAS,aACP,UACA,WACQ;CACR,IAAI,SAAS,WAAW,GAAG,OAAO,aAAa;CAM/C,MAAM,WALQ,SAAS,SAAS,SAAS;EACvC;EACA,qBAAqB,KAAK,KAAK;EAC/B,mBAAmB,KAAK,IAAI;CAC9B,CACqB,CAAC,CAAC,KAAK,GAAG;CAC/B,IAAI,WAEF,OAAO,GAAG,SAAS,GADH,UAAU,WAAW,GAAG,IAAI,UAAU,MAAM,CAAC,IAAI;CAGnE,OAAO;AACT;AAEA,SAAS,cAAc,KAAqC;CAC1D,MAAM,QAAQ,WAAW,IAAI,GAAG;CAChC,IAAI,CAAC,OAAO,OAAO,KAAA;CAEnB,IAAI,KAAK,IAAI,KAAK,MAAM,WAAW;EACjC,WAAW,OAAO,GAAG;EACrB;CACF;CAEA,OAAO;AACT;AAEA,SAAS,cACP,KACA,SACA,UACY;CACZ,MAAM,QAAoB;EACxB;EACA,WAAW,KAAK,IAAI,IAAI;CAC1B;CACA,WAAW,IAAI,KAAK,KAAK;CACzB,OAAO;AACT;AAEA,SAAS,iBAAiB,KAAmB;CAC3C,WAAW,OAAO,GAAG;AACvB;AAGA,MAAa,aAAa;CACxB;CACA;CACA;CACA;CACA,kBAAkB,WAAW,MAAM;CACnC;CACA;AACF;AAiLA,SAAgB,SAAgB,SAe9B;CACA,MAAM,iBAAiB,qBAAqB,QAAQ,KAAK;CAMzD,MAAM,EACJ,OACA,WACA,UACA,KAAK,WACL,MAAM,UACN,oBACA,mBACA,wBACA,GAAG,gBACD;CAEJ,MAAM,WAAW,eACR,aAAa,CAAC,EAAA,CAAG,KAAK,OAAO;EAAE,OAAO,EAAE;EAAO,MAAM,EAAE;CAAK,EAAE,GAGrE,CAAC,KAAK,UAAU,aAAa,CAAC,CAAC,CAAC,CAClC;CAKA,MAAM,YACJ,SAAS,SAAS,IAAI,SAAS,SAAS,SAAS,EAAE,CAAC,QAAQ,QAAQ;CACtE,MAAM,WACJ,SAAS,SAAS,IACd,SAAS,SAAS,SAAS,EAAE,CAAC,OAC9B,QAAQ,QAAQ;CAKtB,MAAM,WAAW,cACT,CACJ;EAAE,OAAO,QAAQ;EAAO,MAAM,QAAQ,QAAQ;CAAU,GACxD,GAAG,QACL,GACA;EAAC,QAAQ;EAAO,QAAQ;EAAM;CAAQ,CACxC;CAkBA,MAAM,kBAAkB,uBACtB,IAAI,IAYF,CACJ;CAMA,MAAM,YAAY,OAA2B,IAAI;CAEjD,MAAM,wBAAwB,OAC5B,sBAAA,GACF;CACA,sBAAsB,UAAU,sBAAA;;CAGhC,MAAM,qBAAqB,QAAqB,WAAmB;EACjE,MAAM,QAAQ,IAAI,MAAM,MAAM;EAC9B,KAAK,MAAM,CAAC,IAAI,YAAY,gBAAgB,SAC1C,IAAI,QAAQ,WAAW,QAAQ;GAC7B,IAAI,QAAQ,WAAW,aAAa,QAAQ,SAAS;GACrD,gBAAgB,QAAQ,OAAO,EAAE;GACjC,QAAQ,OAAO,KAAK;GACpB,QAAQ,QAAQ,UAAU,MAAM;EAClC;CAEJ;;CAGA,MAAM,yBAAyB;EAC7B,MAAM,SAAS,UAAU;EACzB,IAAI,CAAC,UAAU,OAAO,eAAe,OAAO,MAAM;EAClD,KAAK,MAAM,WAAW,gBAAgB,QAAQ,OAAO,GACnD,IAAI,QAAQ,WAAW,MAAM;GAC3B,OAAO,KAAK,QAAQ,OAAO;GAC3B,QAAQ,SAAS;EACnB;CAEJ;;CAGA,MAAM,qBAAqB,WAAmB;EAC5C,MAAM,QAAQ,IAAI,MAAM,MAAM;EAC9B,KAAK,MAAM,CAAC,IAAI,YAAY,gBAAgB,SAC1C,IAAI,QAAQ,WAAW,MAAM;GAC3B,IAAI,QAAQ,WAAW,aAAa,QAAQ,SAAS;GACrD,gBAAgB,QAAQ,OAAO,EAAE;GACjC,QAAQ,OAAO,KAAK;GACpB,QAAQ,QAAQ,UAAU,MAAM;EAClC;CAEJ;CAEA,MAAM,WAAW,cAEb,eAAe,gBAAgB,QAAQ,MAAM,UAAU,aAAa,CAAC,CAAC,GACxE;EAAC;EAAgB,QAAQ;EAAM;EAAU;CAAS,CACpD;CAOA,MAAM,cAAc,OAAO,QAAQ;CACnC,YAAY,UAAU;CAEtB,MAAM,MAAM,YAAY,MAAS;CAGjC,MAAM,CAAC,oBAAoB,yBAAyB,SAAiB,CAAC;CAGtE,MAAM,eAAe,SAAS,OAAO,UAAU;CAC/C,MAAM,CAAC,sBAAsB,2BAA2B,SAAS,KAAK;CAGtE,MAAM,eAAe,cAAc;EAIjC,IAAI,CAAC,SAAS,OAAO,UAAU,YAC7B,OAAO;EAIT,MAAM,SAAS,cAAc,QAAQ;EACrC,IAAI,QACF,OAAO,OAAO;EAIhB,MAAM,UAAU,MAAM,CAAC,CAAC,OAAO,UAAU;GACvC,QAAQ,MACN,sCAAsC,QAAQ,MAAM,KACpD,KACF;GACA,iBAAiB,QAAQ;GACzB,MAAM;EACR,CAAC;EAGD,cAAc,UAAU,SAAS,GAAG;EAEpC,OAAO;CACT,GAAG;EAAC;EAAU;EAAO,QAAQ;EAAO;EAAK;CAAkB,CAAC;CAG5D,gBAAgB;EACd,IAAI,CAAC,gBAAgB,OAAO,GAAG;EAE/B,MAAM,QAAQ,cAAc,QAAQ;EACpC,IAAI,CAAC,OAAO;EAEZ,MAAM,kBAAkB,MAAM,YAAY,KAAK,IAAI;EAGnD,MAAM,QAAQ,iBACN;GACJ,iBAAiB,QAAQ;GACzB,sBAAsB,KAAK,IAAI,CAAC;EAClC,GACA,KAAK,IAAI,GAAG,eAAe,CAC7B;EAEA,aAAa,aAAa,KAAK;CACjC,GAAG;EAAC;EAAU;EAAc;CAAG,CAAC;CAEhC,IAAI;CAEJ,IAAI,OACF,IAAI,OAAO,UAAU,YAAY;EAE/B,MAAM,cAAc,IAAI,YAAa;EAGrC,IAAI,aAAa;GACf,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,WAAW,GACnD,IACE,UAAU,QACV,UAAU,KAAA,KACV,OAAO,UAAU,YACjB,OAAO,UAAU,YACjB,OAAO,UAAU,WAEjB,QAAQ,KACN,+BAA+B,IAAI,wHAErC;GAGJ,gBAAgB;EAClB;CACF,OAEE,gBAAgB;CAKpB,gBAAgB;EACd,IAAI,wBAAwB,kBAAkB,KAAA,GAC5C,wBAAwB,KAAK;CAEjC,GAAG,CAAC,sBAAsB,aAAa,CAAC;CAGxC,MAAM,CAAC,YAAY,iBAAiB,SAA4B,KAAA,CAAS;CACzE,MAAM,CAAC,iBAAiB,sBACtB,SAAsC,IAAI;CAC5C,MAAM,qBAAqB,OAAoC,IAAI;CACnE,MAAM,+BAA+B,OAAsB,IAAI;CAC/D,MAAM,4BAA4B,OAAO,sBAAsB;CAC/D,0BAA0B,UAAU;CACpC,MAAM,oBAAoB,aACvB,WACE,0BAA0B,UAAU,KAAK,KAAK,SAC/C,CAAC,qBAAqB,KAAK,GAC7B,CAAC,CACH;CAKA,MAAM,CAAC,UAAU,eAAe,SAAS;EACvC,MAAM;EACN,OAAO,qBAAqB,SAAS;EACrC,YAAY;CACd,CAAC;CAGD,MAAM,sBAAsB,OAGzB;EAAE,MAAM;EAAM,OAAO;CAAK,CAAC;CAG9B,MAAM,WAAW,OAEf,KAAA,CAAS;CAEX,MAAM,mBAAmB;EACvB,IAAI;EAIJ,SAAS,UAAU;GAAE,SAAA,IAHD,SAAe,MAAM;IACvC,UAAU;GACZ,CAC2B;GAAY;EAAS;CAClD;CAEA,IAAI,CAAC,SAAS,SACZ,WAAW;CAGb,MAAM,kBAAkB,OAId,IAAI;CAKd,MAAM,eAAe,cACb,aAAa,UAAU,QAAQ,GACrC,CAAC,UAAU,QAAQ,CACrB;CAGA,MAAM,gBAAgB,QAAQ,WAC1B;EACE,UAAU,QAAQ;EAClB,MAAM,gBAAgB,KAAA;EACtB,OAAO;EACP,GAAG;EACH,wBAAwB;CAC1B,IACA;EACE,OAAO;EACP,QAAQ;EACR,MAAM,QAAQ,QAAQ;EACtB,MAAM,gBAAgB,KAAA;EACtB,OAAO;EACP,GAAG;EACH,wBAAwB;CAC1B;CAEJ,MAAM,gBAAgB,CAAC,yBAAyB,YAAY,WAAW;CASvE,MAAM,aAAa,KAAK,UAAU;EAChC,QAAQ,QAAQ;EAChB,QAAQ,YAAY;EACpB;EACA,QAAQ,QAAQ;EAChB,gBAAgB;CAClB,CAAC;CACD,MAAM,yBACJ,6BAA6B,YAAY,aACrC,kBACA;CACN,mBAAmB,UAAU;CAE7B,MAAM,QAAQ,eAAe;EAC3B,GAAG;EACH,SAAS;EACT,SAAS,UAAiB;GACxB,6BAA6B,UAAU;GACvC,mBAAmB,IAAI;GAKvB,iBAAiB;GACjB,QAAQ,SAAS,KAAK;EACxB;EACA,YAAY,YAAY;GACtB,IAAI,OAAO,QAAQ,SAAS,UAAU;IACpC,IAAI;IACJ,IAAI;KACF,gBAAgB,KAAK,MAAM,QAAQ,IAAI;IACzC,SAAS,QAAQ;KAGf,OAAO,QAAQ,YAAY,OAAO;IACpC;IACA,IAAI,cAAc,SAAA,qBAAwC;KACxD,MAAM,UAAU,oBAAoB,QAAQ;KAC5C,MAAM,WAAW,oBAAoB,QAAQ;KAC7C,MAAM,UAAU,cAAc;KAC9B,MAAM,WAAW,cAAc;KAE/B,MAAM,eAAe,gBAAgB;KACrC,IAAI,cAAc;MAChB,aAAa,OAAO;MACpB,aAAa,QAAQ;MACrB,aAAa,aAAa;KAC5B;KAGA,YAAY;MAAE,MAAM;MAAS,OAAO;MAAU,YAAY;KAAK,CAAC;KAGhE,SAAS,SAAS,QAAQ;KAG1B,IACE,YAAY,QACZ,aAAa,SACZ,YAAY,WAAW,aAAa,WAErC,IAAI,QAAQ,kBACV,QAAQ,iBAAiB,SAAS,SAAS,UAAU,QAAQ;UACxD;MACL,MAAM,eAAe,aAAa;MAClC,MAAM,cAAc,YAAY;MAChC,IAAI,oBAAoB;MACxB,IAAI,gBAAgB,aAClB,oBAAoB,UAAU,SAAS,OAAO,SAAS,eAAe,QAAQ,OAAO,QAAQ;WACxF,IAAI,cACT,oBAAoB,UAAU,SAAS,OAAO,SAAS;WAEvD,oBAAoB,aAAa,QAAQ,OAAO,QAAQ;MAE1D,QAAQ,KACN,2CAA2C,kBAAkB,uPAK/D;KACF;KAIF,oBAAoB,UAAU;MAAE,MAAM;MAAS,OAAO;KAAS;KAG/D,QAAQ,aAAa,SAAS,QAAQ;KACtC;IACF;IACA,IAAI,cAAc,SAAA,kBAAqC;KACrD,cAAc,cAAc,KAAc;KAC1C,QAAQ,gBAAgB,cAAc,OAAgB,QAAQ;KAC9D;IACF;IACA,IAAI,cAAc,SAAA,wBAA2C;KAC3D,QAAQ,qBAAqB,cAAc,KAAe;KAC1D;IACF;IACA,IAAI,cAAc,SAAA,wBAA2C;KAC3D,QAAQ,cAAc,cAAc,GAAsB;KAC1D;IACF;IACA,IAAI,cAAc,SAAA,OAA0B;KAC1C,MAAM,WAAW;KACjB,MAAM,UAAU,gBAAgB,QAAQ,IAAI,SAAS,EAAE;KACvD,IAAI,CAAC,SAAS;MACZ,QAAQ,KACN,2EAA2E,SAAS,GAAG,sGAEzF;MACA;KACF;KAEA,IAAI,CAAC,SAAS,SAAS;MACrB,IAAI,QAAQ,WAAW,aAAa,QAAQ,SAAS;MACrD,QAAQ,OAAO,IAAI,MAAM,SAAS,KAAK,CAAC;MACxC,gBAAgB,QAAQ,OAAO,SAAS,EAAE;MAC1C,QAAQ,QAAQ,UAAU,SAAS,KAAK;MACxC;KACF;KAGA,IAAI,UAAU,UACZ,IAAI,SAAS,MAAM;MACjB,IAAI,QAAQ,WAAW,aAAa,QAAQ,SAAS;MACrD,QAAQ,QAAQ,SAAS,MAAM;MAC/B,gBAAgB,QAAQ,OAAO,SAAS,EAAE;MAC1C,QAAQ,QAAQ,SAAS,SAAS,MAAM;KAC1C,OACE,QAAQ,QAAQ,UAAU,SAAS,MAAM;UAEtC;MAEL,IAAI,QAAQ,WAAW,aAAa,QAAQ,SAAS;MACrD,QAAQ,QAAQ,SAAS,MAAM;MAC/B,gBAAgB,QAAQ,OAAO,SAAS,EAAE;KAC5C;KACA;IACF;GACF;GACA,QAAQ,YAAY,OAAO;EAC7B;EACA,UAAU,UAAsB;GAO9B,MAAM,eACH,MAAM,UAAiC,UAAU;GACpD,MAAM,kBAAkB,iBAAiB,UAAU;GACnD,MAAM,gBAAgB,qBAAqB,KAAK;GAMhD,IAAI,cAAc;IAChB,kBAAkB,cAAc,mBAAmB;IACnD,IAAI,mBAAmB,CAAC,aAAa,iBACnC,kBAAkB,mBAAmB;GAEzC;GAEA,IAAI,iBAAiB;IAEnB,WAAW;IACX,IAAI,gBAAgB,SAClB,gBAAgB,QAAQ,aAAa;IAEvC,aAAa,UAAU;KAAE,GAAG;KAAM,YAAY;IAAM,EAAE;IAEtD,IAAI,cAAc,iBAAiB;KAEjC,IAAI,cACF,wBAAwB,IAAI;KAI9B,iBAAiB,YAAY,OAAO;KACpC,sBAAsB,KAAK,IAAI,CAAC;IAClC;IAEA,IAAI,CAAC,cAAc,mBAAmB,eAAe;KACnD,MAAM,QAAQ,IAAIA,qBAAyB,KAAK;KAChD,6BAA6B,UAAU;KACvC,mBAAmB,KAAK;KACxB,oBAAoB,KAAK;IAC3B;GACF;GAGA,QAAQ,UAAU,KAAK;EACzB;CACF,CAAC;CAaD,UAAU,UAAU;CAUpB,MAAM,gBAAgB,OAA2B,IAAI;CACrD,MAAM,oBAAoB,OAAO,UAAU;CAC3C,gBAAgB;EACd,MAAM,OAAO,cAAc;EAC3B,cAAc,UAAU;EACxB,MAAM,cAAc,kBAAkB;EACtC,kBAAkB,UAAU;EAM5B,IAAI,gBAAgB,YAAY;GAC9B,6BAA6B,UAAU;GACvC,mBAAmB,IAAI;GACvB,kBACE,4EACF;EACF;EAEA,IAAI,QAAQ,SAAS,OAAO;GAC1B,kBAAkB,MAAM,mBAAmB;GAC3C,WAAW;GACX,IAAI,gBAAgB,SAClB,gBAAgB,QAAQ,aAAa;GAEvC,aAAa,YACX,QAAQ,aAAa;IAAE,GAAG;IAAS,YAAY;GAAM,IAAI,OAC3D;EACF;CAGF,GAAG,CAAC,OAAO,UAAU,CAAC;CAKtB,MAAM,OAAO,aAET,QACA,OAAkB,CAAC,GACnB,YACe;EACf,OAAO,IAAI,SAAS,SAAS,WAAW;GACtC,MAAM,SAAS,UAAU;GACzB,IACE,UACA,mBAAmB,WACnB,OAAO,eAAe,OAAO,QAC7B;IACA,uBAAO,IAAI,MAAM,mBAAmB,CAAC;IACrC;GACF;GAEA,MAAM,KAAK,OAAO,WAAW;GAC7B,IAAI;GAGJ,MAAM,iBACJ,YACC,aAAa,WAAW,YAAY,WAAW,aAAa;GAC/D,MAAM,gBAAgB,iBACjB,UACA,SAAqC;GAC1C,MAAM,UAAU,iBACZ,KAAA,IACC,SAAqC;GAK1C,MAAM,mBACJ,YAAY,KAAA,IACR,UACA,gBACE,KAAA,IACA,sBAAsB;GAE9B,IAAI,kBACF,YAAY,iBAAiB;IAC3B,MAAM,UAAU,gBAAgB,QAAQ,IAAI,EAAE;IAC9C,gBAAgB,QAAQ,OAAO,EAAE;IACjC,MAAM,eAAe,eAAe,OAAO,mBAAmB,iBAAiB;IAC/E,SAAS,QAAQ,UAAU,YAAY;IACvC,OAAO,IAAI,MAAM,YAAY,CAAC;GAChC,GAAG,gBAAgB;GASrB,MAAM,UAAU,KAAK,UAAU;IAL7B;IACA;IACA;IACA,MAAA;GAE6B,CAAU;GAEzC,gBAAgB,QAAQ,IAAI,IAAI;IAC9B;IACS;IACT,QAAQ;IACR;IACA;IACA,QAAQ;GACV,CAAC;GAMD,IAAI,UAAU,OAAO,eAAe,OAAO,MAAM;IAC/C,OAAO,KAAK,OAAO;IACnB,MAAM,UAAU,gBAAgB,QAAQ,IAAI,EAAE;IAC9C,IAAI,SAAS,QAAQ,SAAS;GAChC;EACF,CAAC;CACH,GAEA,CAAC,CACH;CAEA,MAAM,YAAY,aAAoB;EAGpC,CAAC,UAAU,WAAW,MAAA,CAAO,KAC3B,KAAK,UAAU;GAAE,OAAO;GAAU,MAAA;EAAiC,CAAC,CACtE;EACA,cAAc,QAAQ;EACtB,QAAQ,gBAAgB,UAAU,QAAQ;CAC5C;CAEA,MAAM,OAAO;CAEb,MAAM,QAAQ,SAAS;CACvB,MAAM,OAAO,SAAS;CAMtB,MAEE,OAAO;CACT,MAAM,aAAa,SAAS;CAC5B,MAAM,QAAQ,SAAS,QAAS;CAChC,MAAM,QAAQ;CACd,MAAM,kBAAkB;CACxB,gBAAgB,UAAU;CAI1B,MAAM,OADO,cAAc,gBAAgB,IAAI,GAAG,CAAC,IAAI,CACvC;CAChB,MAAM,mBAAmB;EAIvB,QADuB,MAAM,QAA0B,MAAM,UAAU,GAAA,CAC1D,QAAQ,SAAS,SAAS,CAAC,CAAC,QAAQ,UAAU,UAAU;CACvE;CAGA,IAAI,SAAS,UAAU,SAAS,MAAM,YAAY,GAChD,QAAQ,KACN,iBACE,SAAS,QACT,iDACA,SAAS,KACb;CAQF,OAAO;AACT;AAOA,SAAS,mBAAmB,SAAwC;CAClE,OAAO;EACL,QAAQ,oBAAoB;EAC5B,QAAQ,MAAM;EACd,OAAO,QAAQ,QAAQ;CACzB,CAAC,CAAC,KAAK,IAAI;AACb;AAEA,SAAgB,mBAEd,SAQA;CACA,MAAM,EAAE,UAAU;CAClB,MAAM,CAAC,OAAO,YAAY,eACxB,0BAAgC,CAClC;CACA,MAAM,UAAU,uBAAO,IAAI,IAAY,CAAC;CAExC,gBAAgB;EACd,MAAM,aAAa,UAAwB;GACzC,IAAI,OAAO,MAAM,SAAS,UAAU;GACpC,IAAI;GACJ,IAAI;IACF,UAAU,KAAK,MAAM,MAAM,IAAI;GACjC,QAAQ;IACN;GACF;GACA,IAAI,QAAQ,SAAS,oBAAoB;GACzC,MAAM,MAAM,mBAAmB,OAAO;GACtC,IAAI,QAAQ,QAAQ,IAAI,GAAG,GAAG;GAC9B,QAAQ,QAAQ,IAAI,GAAG;GACvB,UAAU,SAAS,oBAA0B,MAAM,OAAO,CAAC;EAC7D;EAEA,MAAM,iBAAiB,WAAW,SAAS;EAC3C,aAAa,MAAM,oBAAoB,WAAW,SAAS;CAC7D,GAAG,CAAC,KAAK,CAAC;CAEV,MAAM,kBAAkB,kBAAkB;EACxC,QAAQ,QAAQ,MAAM;EACtB,SAAS,0BAAgC,CAAC;CAC5C,GAAG,CAAC,CAAC;CAEL,MAAM,qBAAqB,aACxB,eAAuB,MAAM,iBAAiB,eAAe,CAAC,GAC/D,CAAC,MAAM,gBAAgB,CACzB;CAEA,OAAO;EACL,GAAG;EACH;EACA;CACF;AACF"}
|
|
@@ -75,6 +75,14 @@ declare function isErrorRetryable(err: unknown): boolean;
|
|
|
75
75
|
* invocation, which runs the new code and succeeds.
|
|
76
76
|
*/
|
|
77
77
|
declare function isDurableObjectCodeUpdateReset(error: unknown): boolean;
|
|
78
|
+
/**
|
|
79
|
+
* Whether an error (or anything in its `cause` chain, or a raw error-message
|
|
80
|
+
* string) is a Durable Object memory-limit reset — see
|
|
81
|
+
* {@link MEMORY_LIMIT_RESET_PATTERN}. Unlike {@link isPlatformTransientError},
|
|
82
|
+
* re-running the same work re-OOMs deterministically, so callers must NOT defer
|
|
83
|
+
* it like a transient; they should bound retries tightly and then seal (#1825).
|
|
84
|
+
*/
|
|
85
|
+
declare function isDurableObjectMemoryLimitReset(error: unknown): boolean;
|
|
78
86
|
/**
|
|
79
87
|
* Whether an error (or anything in its `cause` chain) is a transient failure
|
|
80
88
|
* of the PLATFORM rather than of the code that threw it:
|
|
@@ -98,12 +106,13 @@ declare function isDurableObjectCodeUpdateReset(error: unknown): boolean;
|
|
|
98
106
|
declare function isPlatformTransientError(error: unknown): boolean;
|
|
99
107
|
//#endregion
|
|
100
108
|
export {
|
|
101
|
-
|
|
102
|
-
|
|
109
|
+
isPlatformTransientError as a,
|
|
110
|
+
validateRetryOptions as c,
|
|
111
|
+
isErrorRetryable as i,
|
|
103
112
|
isDurableObjectCodeUpdateReset as n,
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
113
|
+
jitterBackoff as o,
|
|
114
|
+
isDurableObjectMemoryLimitReset as r,
|
|
115
|
+
tryN as s,
|
|
107
116
|
RetryOptions as t
|
|
108
117
|
};
|
|
109
|
-
//# sourceMappingURL=retries-
|
|
118
|
+
//# sourceMappingURL=retries-CvHJwSuh.d.ts.map
|
package/dist/retries.d.ts
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import {
|
|
2
|
-
a as
|
|
3
|
-
|
|
2
|
+
a as isPlatformTransientError,
|
|
3
|
+
c as validateRetryOptions,
|
|
4
|
+
i as isErrorRetryable,
|
|
4
5
|
n as isDurableObjectCodeUpdateReset,
|
|
5
|
-
o as
|
|
6
|
-
r as
|
|
7
|
-
s as
|
|
6
|
+
o as jitterBackoff,
|
|
7
|
+
r as isDurableObjectMemoryLimitReset,
|
|
8
|
+
s as tryN,
|
|
8
9
|
t as RetryOptions
|
|
9
|
-
} from "./retries-
|
|
10
|
+
} from "./retries-CvHJwSuh.js";
|
|
10
11
|
export {
|
|
11
12
|
RetryOptions,
|
|
12
13
|
isDurableObjectCodeUpdateReset,
|
|
14
|
+
isDurableObjectMemoryLimitReset,
|
|
13
15
|
isErrorRetryable,
|
|
14
16
|
isPlatformTransientError,
|
|
15
17
|
jitterBackoff,
|
package/dist/retries.js
CHANGED
|
@@ -107,6 +107,38 @@ const SUPERSEDED_ISOLATE_PATTERN = /reset because its code was updated|this scri
|
|
|
107
107
|
* as the platform's failure, not the callback's.
|
|
108
108
|
*/
|
|
109
109
|
const CONNECTION_LOST_PATTERN = /network connection lost/i;
|
|
110
|
+
/**
|
|
111
|
+
* The Durable Object memory-limit reset — the isolate exceeded its 128 MB limit
|
|
112
|
+
* and was reset by the platform (workerd surfaces this verbatim as
|
|
113
|
+
* "Durable Object's isolate exceeded its memory limit and was reset."; the D1
|
|
114
|
+
* sibling is "D1 DB's isolate exceeded its memory limit and was reset.").
|
|
115
|
+
*
|
|
116
|
+
* The match is the broad shared fragment "exceeded its memory limit" rather than
|
|
117
|
+
* the full "...and was reset" sentence: real-world surfacings truncate or reword
|
|
118
|
+
* the tail (some log pipelines clip the message; D1/storage wrappers re-prefix
|
|
119
|
+
* it), and a customer-reported loop (#1825) showed lines carrying only the
|
|
120
|
+
* "exceeded its memory limit" fragment. Missing a surfacing here means the
|
|
121
|
+
* circuit breaker never engages, so we err toward the broader match — and even a
|
|
122
|
+
* false positive is fail-safe (a tightly-bounded retry-then-seal, not data loss).
|
|
123
|
+
*
|
|
124
|
+
* This is DELIBERATELY a separate class from `SUPERSEDED_ISOLATE_PATTERN` /
|
|
125
|
+
* {@link isPlatformTransientError}, and is NOT folded into them. A supersede or
|
|
126
|
+
* connection-lost transient means "re-run the same work and it succeeds on a
|
|
127
|
+
* healthy isolate" — those classes can be deferred and retried *indefinitely*. A
|
|
128
|
+
* memory-limit reset is the opposite: re-running the SAME memory-heavy work
|
|
129
|
+
* deterministically re-OOMs (the footprint, not the platform, is the cause), so
|
|
130
|
+
* deferring it indefinitely would PRESERVE the one-shot row and re-run the
|
|
131
|
+
* doomed work forever (amplifying the loop and cost — see #1825). It is a
|
|
132
|
+
* poison-pill signal: callers must bound retries tightly and then SEAL.
|
|
133
|
+
*
|
|
134
|
+
* Accordingly the schedule executor (`_executeScheduleCallback`) and the
|
|
135
|
+
* alarm-boundary circuit breaker (`Agent.alarm`) treat it as its OWN class: a
|
|
136
|
+
* memory-limit reset is re-thrown (row preserved) so it reaches the breaker,
|
|
137
|
+
* which tolerates a few strikes (`maxAlarmMemoryLimitStrikes`) and then seals +
|
|
138
|
+
* purges the looping row — i.e. *bounded* deferral, never the unbounded deferral
|
|
139
|
+
* the transient classes get.
|
|
140
|
+
*/
|
|
141
|
+
const MEMORY_LIMIT_RESET_PATTERN = /exceeded its memory limit/i;
|
|
110
142
|
function errorMessageOf(error) {
|
|
111
143
|
return error instanceof Error ? error.message : typeof error === "string" ? error : "";
|
|
112
144
|
}
|
|
@@ -134,6 +166,17 @@ function isDurableObjectCodeUpdateReset(error) {
|
|
|
134
166
|
return false;
|
|
135
167
|
}
|
|
136
168
|
/**
|
|
169
|
+
* Whether an error (or anything in its `cause` chain, or a raw error-message
|
|
170
|
+
* string) is a Durable Object memory-limit reset — see
|
|
171
|
+
* {@link MEMORY_LIMIT_RESET_PATTERN}. Unlike {@link isPlatformTransientError},
|
|
172
|
+
* re-running the same work re-OOMs deterministically, so callers must NOT defer
|
|
173
|
+
* it like a transient; they should bound retries tightly and then seal (#1825).
|
|
174
|
+
*/
|
|
175
|
+
function isDurableObjectMemoryLimitReset(error) {
|
|
176
|
+
for (const e of selfAndCauses(error)) if (MEMORY_LIMIT_RESET_PATTERN.test(errorMessageOf(e))) return true;
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
137
180
|
* Whether an error (or anything in its `cause` chain) is a transient failure
|
|
138
181
|
* of the PLATFORM rather than of the code that threw it:
|
|
139
182
|
*
|
|
@@ -163,6 +206,6 @@ function isPlatformTransientError(error) {
|
|
|
163
206
|
return false;
|
|
164
207
|
}
|
|
165
208
|
//#endregion
|
|
166
|
-
export { isDurableObjectCodeUpdateReset, isErrorRetryable, isPlatformTransientError, jitterBackoff, tryN, validateRetryOptions };
|
|
209
|
+
export { isDurableObjectCodeUpdateReset, isDurableObjectMemoryLimitReset, isErrorRetryable, isPlatformTransientError, jitterBackoff, tryN, validateRetryOptions };
|
|
167
210
|
|
|
168
211
|
//# sourceMappingURL=retries.js.map
|
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\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) 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 *\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 (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 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, 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 *\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 (isErrorRetryable(e)) return true;\n }\n return false;\n}\n"],"mappings":"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"}
|
package/dist/serializable.d.ts
CHANGED
package/dist/skills/compile.js
CHANGED
|
@@ -15,7 +15,7 @@ import { build } from "esbuild";
|
|
|
15
15
|
* @module
|
|
16
16
|
*/
|
|
17
17
|
/** Skill-script extensions that can (and must) be compiled ahead of time. */
|
|
18
|
-
const COMPILABLE_SCRIPT_EXTENSIONS = new Set([
|
|
18
|
+
const COMPILABLE_SCRIPT_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
19
19
|
".js",
|
|
20
20
|
".mjs",
|
|
21
21
|
".ts",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compile.js","names":[],"sources":["../../src/skills/compile.ts"],"sourcesContent":["import { build } from \"esbuild\";\n\n/**\n * Build-time skill-script compiler.\n *\n * Skill scripts must be self-contained JavaScript modules before they can run:\n * the Worker runtime executes them directly through the sandbox and does **not**\n * ship an in-Worker bundler. This module compiles a skill script (TypeScript or\n * multi-file JavaScript) into a single self-contained ESM bundle using esbuild.\n *\n * It runs in Node (build time) only — the Agents Vite plugin uses it to compile\n * bundled skills, and skills served from R2 or other dynamic sources should be\n * compiled with {@link compileSkillScript} before upload.\n *\n * @module\n */\n\n/** Skill-script extensions that can (and must) be compiled ahead of time. */\nconst COMPILABLE_SCRIPT_EXTENSIONS = new Set([\".js\", \".mjs\", \".ts\", \".tsx\"]);\n\nexport interface CompiledSkillScript {\n /** Self-contained ESM source, ready to embed and run without a bundler. */\n content: string;\n /** Always `true`; mirrors the `precompiled` flag on skill resources. */\n precompiled: true;\n}\n\nexport interface CompileSkillScriptOptions {\n /**\n * JavaScript target for the emitted bundle. Defaults to `es2022`, which the\n * Workers runtime supports.\n */\n target?: string;\n}\n\nfunction extensionOf(path: string): string {\n const file = path.split(\"/\").at(-1) ?? path;\n const index = file.lastIndexOf(\".\");\n return index === -1 ? \"\" : file.slice(index).toLowerCase();\n}\n\n/**\n * Whether a resource path is a skill script that should be compiled ahead of\n * time (i.e. has a `.js`, `.mjs`, `.ts`, or `.tsx` extension).\n */\nexport function isCompilableSkillScript(path: string): boolean {\n return COMPILABLE_SCRIPT_EXTENSIONS.has(extensionOf(path));\n}\n\n/**\n * Compile a skill script file into a single self-contained ESM module.\n *\n * Resolves and inlines sibling imports relative to `entryPath`, strips\n * TypeScript types, and emits ESM so the script can run in the Worker sandbox\n * without an in-Worker bundler.\n *\n * @param entryPath Absolute path to the skill script file on disk.\n */\nexport async function compileSkillScript(\n entryPath: string,\n options: CompileSkillScriptOptions = {}\n): Promise<CompiledSkillScript> {\n const result = await build({\n entryPoints: [entryPath],\n bundle: true,\n write: false,\n format: \"esm\",\n platform: \"browser\",\n target: options.target ?? \"es2022\",\n logLevel: \"silent\",\n legalComments: \"none\"\n });\n\n const output = result.outputFiles?.[0];\n if (!output) {\n throw new Error(\n `esbuild produced no output when compiling skill script \"${entryPath}\".`\n );\n }\n\n return { content: output.text, precompiled: true };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAkBA,MAAM,+
|
|
1
|
+
{"version":3,"file":"compile.js","names":[],"sources":["../../src/skills/compile.ts"],"sourcesContent":["import { build } from \"esbuild\";\n\n/**\n * Build-time skill-script compiler.\n *\n * Skill scripts must be self-contained JavaScript modules before they can run:\n * the Worker runtime executes them directly through the sandbox and does **not**\n * ship an in-Worker bundler. This module compiles a skill script (TypeScript or\n * multi-file JavaScript) into a single self-contained ESM bundle using esbuild.\n *\n * It runs in Node (build time) only — the Agents Vite plugin uses it to compile\n * bundled skills, and skills served from R2 or other dynamic sources should be\n * compiled with {@link compileSkillScript} before upload.\n *\n * @module\n */\n\n/** Skill-script extensions that can (and must) be compiled ahead of time. */\nconst COMPILABLE_SCRIPT_EXTENSIONS = new Set([\".js\", \".mjs\", \".ts\", \".tsx\"]);\n\nexport interface CompiledSkillScript {\n /** Self-contained ESM source, ready to embed and run without a bundler. */\n content: string;\n /** Always `true`; mirrors the `precompiled` flag on skill resources. */\n precompiled: true;\n}\n\nexport interface CompileSkillScriptOptions {\n /**\n * JavaScript target for the emitted bundle. Defaults to `es2022`, which the\n * Workers runtime supports.\n */\n target?: string;\n}\n\nfunction extensionOf(path: string): string {\n const file = path.split(\"/\").at(-1) ?? path;\n const index = file.lastIndexOf(\".\");\n return index === -1 ? \"\" : file.slice(index).toLowerCase();\n}\n\n/**\n * Whether a resource path is a skill script that should be compiled ahead of\n * time (i.e. has a `.js`, `.mjs`, `.ts`, or `.tsx` extension).\n */\nexport function isCompilableSkillScript(path: string): boolean {\n return COMPILABLE_SCRIPT_EXTENSIONS.has(extensionOf(path));\n}\n\n/**\n * Compile a skill script file into a single self-contained ESM module.\n *\n * Resolves and inlines sibling imports relative to `entryPath`, strips\n * TypeScript types, and emits ESM so the script can run in the Worker sandbox\n * without an in-Worker bundler.\n *\n * @param entryPath Absolute path to the skill script file on disk.\n */\nexport async function compileSkillScript(\n entryPath: string,\n options: CompileSkillScriptOptions = {}\n): Promise<CompiledSkillScript> {\n const result = await build({\n entryPoints: [entryPath],\n bundle: true,\n write: false,\n format: \"esm\",\n platform: \"browser\",\n target: options.target ?? \"es2022\",\n logLevel: \"silent\",\n legalComments: \"none\"\n });\n\n const output = result.outputFiles?.[0];\n if (!output) {\n throw new Error(\n `esbuild produced no output when compiling skill script \"${entryPath}\".`\n );\n }\n\n return { content: output.text, precompiled: true };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAkBA,MAAM,+CAA+B,IAAI,IAAI;CAAC;CAAO;CAAQ;CAAO;AAAM,CAAC;AAiB3E,SAAS,YAAY,MAAsB;CACzC,MAAM,OAAO,KAAK,MAAM,GAAG,CAAC,CAAC,GAAG,EAAE,KAAK;CACvC,MAAM,QAAQ,KAAK,YAAY,GAAG;CAClC,OAAO,UAAU,KAAK,KAAK,KAAK,MAAM,KAAK,CAAC,CAAC,YAAY;AAC3D;;;;;AAMA,SAAgB,wBAAwB,MAAuB;CAC7D,OAAO,6BAA6B,IAAI,YAAY,IAAI,CAAC;AAC3D;;;;;;;;;;AAWA,eAAsB,mBACpB,WACA,UAAqC,CAAC,GACR;CAY9B,MAAM,UAAS,MAXM,MAAM;EACzB,aAAa,CAAC,SAAS;EACvB,QAAQ;EACR,OAAO;EACP,QAAQ;EACR,UAAU;EACV,QAAQ,QAAQ,UAAU;EAC1B,UAAU;EACV,eAAe;CACjB,CAAC,EAAA,CAEqB,cAAc;CACpC,IAAI,CAAC,QACH,MAAM,IAAI,MACR,2DAA2D,UAAU,GACvE;CAGF,OAAO;EAAE,SAAS,OAAO;EAAM,aAAa;CAAK;AACnD"}
|