@athenaintel/react 0.60.0 → 0.60.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.
Files changed (65) hide show
  1. package/README.md +73 -2
  2. package/dist/agent-runtime/src/client-tool-bridge.cjs +20 -2
  3. package/dist/agent-runtime/src/client-tool-bridge.cjs.map +1 -1
  4. package/dist/agent-runtime/src/client-tool-bridge.d.ts +9 -4
  5. package/dist/agent-runtime/src/client-tool-bridge.js +20 -2
  6. package/dist/agent-runtime/src/client-tool-bridge.js.map +1 -1
  7. package/dist/agent-runtime/src/client-tools.cjs.map +1 -1
  8. package/dist/agent-runtime/src/client-tools.d.ts +9 -0
  9. package/dist/agent-runtime/src/client-tools.js.map +1 -1
  10. package/dist/chat/AthenaChat.cjs +2 -0
  11. package/dist/chat/AthenaChat.cjs.map +1 -1
  12. package/dist/chat/AthenaChat.js +2 -0
  13. package/dist/chat/AthenaChat.js.map +1 -1
  14. package/dist/chat/StatewireClientToolBridge.cjs +69 -1
  15. package/dist/chat/StatewireClientToolBridge.cjs.map +1 -1
  16. package/dist/chat/StatewireClientToolBridge.d.ts +14 -1
  17. package/dist/chat/StatewireClientToolBridge.js +70 -2
  18. package/dist/chat/StatewireClientToolBridge.js.map +1 -1
  19. package/dist/chat/StatewireClientToolConsentCard.cjs +65 -0
  20. package/dist/chat/StatewireClientToolConsentCard.cjs.map +1 -0
  21. package/dist/chat/StatewireClientToolConsentCard.d.ts +24 -0
  22. package/dist/chat/StatewireClientToolConsentCard.js +65 -0
  23. package/dist/chat/StatewireClientToolConsentCard.js.map +1 -0
  24. package/dist/chat/client-tool-write-consent.cjs +111 -0
  25. package/dist/chat/client-tool-write-consent.cjs.map +1 -0
  26. package/dist/chat/client-tool-write-consent.d.ts +71 -0
  27. package/dist/chat/client-tool-write-consent.js +111 -0
  28. package/dist/chat/client-tool-write-consent.js.map +1 -0
  29. package/dist/chat/statewire-client-tools.cjs +30 -5
  30. package/dist/chat/statewire-client-tools.cjs.map +1 -1
  31. package/dist/chat/statewire-client-tools.d.ts +34 -2
  32. package/dist/chat/statewire-client-tools.js +30 -5
  33. package/dist/chat/statewire-client-tools.js.map +1 -1
  34. package/dist/chat-ui/mentions/suggestions/data/sources/asset-type-icons.cjs +13 -0
  35. package/dist/chat-ui/mentions/suggestions/data/sources/asset-type-icons.cjs.map +1 -1
  36. package/dist/chat-ui/mentions/suggestions/data/sources/asset-type-icons.d.ts +12 -0
  37. package/dist/chat-ui/mentions/suggestions/data/sources/asset-type-icons.js +13 -0
  38. package/dist/chat-ui/mentions/suggestions/data/sources/asset-type-icons.js.map +1 -1
  39. package/dist/chat-ui/mentions/suggestions/data/sources/index.cjs +2 -1
  40. package/dist/chat-ui/mentions/suggestions/data/sources/index.cjs.map +1 -1
  41. package/dist/chat-ui/mentions/suggestions/data/sources/index.js +3 -2
  42. package/dist/chat-ui/mentions/suggestions/data/sources/index.js.map +1 -1
  43. package/dist/chat-ui/mentions/use-asset-icon-app-url.cjs +2 -1
  44. package/dist/chat-ui/mentions/use-asset-icon-app-url.cjs.map +1 -1
  45. package/dist/chat-ui/mentions/use-asset-icon-app-url.d.ts +2 -1
  46. package/dist/chat-ui/mentions/use-asset-icon-app-url.js +2 -1
  47. package/dist/chat-ui/mentions/use-asset-icon-app-url.js.map +1 -1
  48. package/dist/chat-ui/statewire.cjs +4 -0
  49. package/dist/chat-ui/statewire.cjs.map +1 -1
  50. package/dist/chat-ui/statewire.d.ts +2 -0
  51. package/dist/chat-ui/statewire.js +4 -0
  52. package/dist/chat-ui/statewire.js.map +1 -1
  53. package/dist/index.cjs +5 -0
  54. package/dist/index.cjs.map +1 -1
  55. package/dist/index.d.ts +6 -2
  56. package/dist/index.js +6 -1
  57. package/dist/index.js.map +1 -1
  58. package/dist/provider/AthenaProvider.cjs +22 -3
  59. package/dist/provider/AthenaProvider.cjs.map +1 -1
  60. package/dist/provider/AthenaProvider.d.ts +8 -1
  61. package/dist/provider/AthenaProvider.js +23 -4
  62. package/dist/provider/AthenaProvider.js.map +1 -1
  63. package/dist/react/package.json.cjs +1 -1
  64. package/dist/react/package.json.js +1 -1
  65. package/package.json +1 -1
package/README.md CHANGED
@@ -470,7 +470,9 @@ What the statewire transport wires up in `AthenaChat`:
470
470
  `@athenaintel/agent-runtime/typed-interrupt-kinds`'.
471
471
  - **Frontend tools:** tools in `frontendTools` with a local `execute` are
472
472
  declared to the backend as `client_tools` and executed by the mounted
473
- client-tool bridge, which resumes the run with each tool's result.
473
+ client-tool bridge, which resumes the run with each tool's result. One that
474
+ changes something can ask the person first, and every one can be stopped
475
+ (see [Frontend tools that ask first, and stop](#frontend-tools-that-ask-first-and-stop)).
474
476
  - **Mid-run sends and the queue:** a message sent while a run is active
475
477
  queues on the server. The queue panel above the composer
476
478
  (`StatewireQueuedMessages`) lists queued entries with remove, "Send now"
@@ -1147,6 +1149,70 @@ reference standard's kinds (`@athenaintel/references`):
1147
1149
  - **Not yet:** a table row (a lakehouse or Kronos row, by key) waits for a
1148
1150
  permission-checked row reader on the server (#54759).
1149
1151
 
1152
+ ## Frontend tools that ask first, and stop
1153
+
1154
+ A tool in `frontendTools` with an `execute` runs in the person's browser, as
1155
+ your app's own code, and by default it runs without asking. Two things let it
1156
+ behave like the rest of the chat.
1157
+
1158
+ **Ask before a tool that changes something.** Wrap the entry in
1159
+ `requiresWriteConsent`:
1160
+
1161
+ ```tsx
1162
+ import { AthenaProvider, requiresWriteConsent } from '@athenaintel/react';
1163
+
1164
+ const frontendTools = {
1165
+ get_note: { description: '…', parameters, execute: readNote },
1166
+ save_note: requiresWriteConsent(
1167
+ { description: '…', parameters, execute: saveNote },
1168
+ { summary: 'save a note on this record' },
1169
+ ),
1170
+ };
1171
+ ```
1172
+
1173
+ Before the tool's first call in a conversation runs, `AthenaChat` shows a card
1174
+ above the composer ("The agent wants to save a note on this record.") with
1175
+ **Allow** and **Decline**. Allow covers the rest of that conversation; Decline
1176
+ answers the agent with an error for the waiting calls, and a later call asks
1177
+ again. Calls to unmarked tools in the same turn still run. The answer is the
1178
+ person's own: nothing is remembered across a reload, and someone else signing
1179
+ in to the same mounted chat is asked for themselves. The mark also reaches
1180
+ Athena (`requires_write`), which then never answers the call for the person
1181
+ while their tab is away.
1182
+
1183
+ - A host with its own chat mounts `StatewireClientToolConsentCard` (also in
1184
+ `@athenaintel/react/chat-ui/statewire`; `agentName` names who is asking), or
1185
+ draws its own on `useClientToolWriteConsent()`: `{ request, allow, decline }`,
1186
+ `request` holding the waiting tools (`name`, `summary`) or `null`.
1187
+ - To ask in a dialog of your own instead, pass `confirmClientToolWrites` on
1188
+ `<AthenaProvider>`: it receives the same request plus a `signal`, and
1189
+ resolves `true` (allow the conversation) or `false` (decline). The signal
1190
+ aborts when the question has no call left to answer: close the dialog then
1191
+ (its promise need not settle, and a late answer is ignored). A confirmer
1192
+ that throws counts as a decline.
1193
+ - With no card mounted and no confirmer, the run waits and an
1194
+ `sdk.config.warning` says so.
1195
+
1196
+ The consent is asked in the browser. It is a courtesy to the person, never a
1197
+ permission boundary: your tool still calls your backend, or Athena's, as the
1198
+ signed-in person, and those checks decide what it may do.
1199
+
1200
+ **Stop when the run stops.** `execute` receives `abortSignal` in its second
1201
+ argument. It aborts when the call's run is stopped, the call is answered
1202
+ elsewhere, or the person opens another conversation; whatever the tool returns
1203
+ after that is discarded. A tool that takes visible time (a paced walkthrough,
1204
+ typing shown to the person) should watch it:
1205
+
1206
+ ```tsx
1207
+ execute: async (args, { abortSignal }) => {
1208
+ for (const step of steps) {
1209
+ if (abortSignal.aborted) return { stopped: true };
1210
+ await show(step);
1211
+ }
1212
+ return { shown: steps.length };
1213
+ },
1214
+ ```
1215
+
1150
1216
  ## Chat voice in AthenaChat
1151
1217
 
1152
1218
  `AthenaChat` includes realtime voice alongside dictation where the deployment
@@ -1343,6 +1409,10 @@ Both use the same bearer / API key as session creation. The full recipe
1343
1409
  template skill `voice-session-guide.md`. `createAthenaVoiceSession` is also
1344
1410
  exported if you hold the bridge state yourself.
1345
1411
 
1412
+ ## What's new in 0.60.1
1413
+
1414
+ **The `@` menu's asset icons draw in apps a computer serves.** In those apps, Marathon's `/_athena/auth` mints `appUrl` as the app's own origin, which serves no Athena icons. So 0.60.0 asked the app itself for the SVGs, and every row fell back to its type's Lucide icon. The icons, mailbox logos, chips and drive cards now load from the deployment's frontend there (`staging-app.athenaintel.com`, `app.athenaintel.com`), the same frontend "Open in Athena" links go to (`assetIconAppUrl`, on `athenaLinkAppUrl`).
1415
+
1346
1416
  ## What's new in 0.60.0
1347
1417
 
1348
1418
  **The `@` menu draws each asset's type icon, as Olympus does.** Asset rows used to show the same generic file icon in every app that didn't serve Athena's icon files itself. Now each row shows the icon Olympus draws for its type, from Olympus's own type → icon table. An email whose mailbox is recorded on it (a Gmail or Outlook draft or message) shows that mailbox's logo. The SVGs load from the Athena web app the chat talks to (`<appUrl>/assets/icons/files/`), so there's nothing to configure. Each chat uses its own provider's app, and a host that ships its own copy still points at it with `configureAssetIconBasePath`. Where an SVG can't load (a strict `img-src` CSP, offline), the row shows the type's Lucide icon. The mention chips and the drive cards draw the same icons. A renderer of your own draws them with `assetTypeToThumb(type, { appUrl: useAssetIconAppUrl() })`. The search reads no metadata; a page with email rows reads theirs alone (`getAssetsByIds`).
@@ -2548,7 +2618,8 @@ app. `AthenaChat` renders them as chips with nothing to configure, and so do `Ch
2548
2618
 
2549
2619
  - **the asset type's icon**, the one Olympus draws for that type. The SVGs load from the Athena web
2550
2620
  app the chat talks to (`<appUrl>/assets/icons/files/`, public files, no credential), each chat
2551
- from its own provider's. A host that ships its own copy of the type glyphs points the chips at it
2621
+ from its own provider's; in an app a computer serves, where Marathon mints `appUrl` as the app's
2622
+ own origin, from the deployment's frontend, as "Open in Athena" links do. A host that ships its own copy of the type glyphs points the chips at it
2552
2623
  with `configureAssetIconBasePath({ basePath })`; brand logos (Gmail, Outlook, Power BI, Slack)
2553
2624
  still come from the app. Outside a provider, or where the SVG cannot load (a strict `img-src`
2554
2625
  CSP, offline), the chip draws a Lucide icon for the type. A renderer of your own draws the same
@@ -35,7 +35,8 @@ async function executeClientToolBatch(options) {
35
35
  writeDeclinedMessage = CLIENT_TOOL_WRITE_DECLINED_MESSAGE,
36
36
  missingToolMessage = defaultMissingClientToolMessage,
37
37
  onCallSettled,
38
- onExecutionStart
38
+ onExecutionStart,
39
+ signal
39
40
  } = options;
40
41
  const registry = new Map(tools.map((tool) => [tool.name, tool]));
41
42
  let writesDeclined = false;
@@ -70,7 +71,8 @@ async function executeClientToolBatch(options) {
70
71
  envelope = clientTools.clientToolSuccessEnvelope(
71
72
  jsonSafeClientToolResult(
72
73
  await tool.run(clientTools.clientToolCallArgs(call), {
73
- toolCallId: call.interrupt_id
74
+ toolCallId: call.interrupt_id,
75
+ ...signal ? { signal } : {}
74
76
  })
75
77
  )
76
78
  );
@@ -143,6 +145,15 @@ function useStatewireClientToolBridge(options) {
143
145
  }, [stopResumeReplay, tracker]);
144
146
  const liveRequestIdsRef = require$$0.useRef(/* @__PURE__ */ new Set());
145
147
  liveRequestIdsRef.current = new Set((inputRequests ?? []).map((request) => request.id));
148
+ const abortControllersRef = require$$0.useRef(/* @__PURE__ */ new Map());
149
+ require$$0.useEffect(() => {
150
+ const liveRequestIds = new Set((inputRequests ?? []).map((request) => request.id));
151
+ for (const [requestId, controller] of abortControllersRef.current) {
152
+ if (liveRequestIds.has(requestId)) continue;
153
+ abortControllersRef.current.delete(requestId);
154
+ controller.abort();
155
+ }
156
+ }, [inputRequests]);
146
157
  const startResumeReplay = require$$0.useCallback(
147
158
  (claim, requestId) => {
148
159
  if (!activeRef.current || resumeClaimsRef.current.has(claim)) return;
@@ -200,9 +211,12 @@ function useStatewireClientToolBridge(options) {
200
211
  onCallSettled,
201
212
  sendResume
202
213
  } = optionsRef.current;
214
+ const abortController = new AbortController();
215
+ abortControllersRef.current.set(request.id, abortController);
203
216
  const results = await executeClientToolBatch({
204
217
  calls: payload.context.requests,
205
218
  tools,
219
+ signal: abortController.signal,
206
220
  // Live while the request still exists AND this surface may answer it:
207
221
  // mounted, or already past the point of no return (handlers started —
208
222
  // the detached execution must finish and settle, never re-execute).
@@ -218,6 +232,10 @@ function useStatewireClientToolBridge(options) {
218
232
  ...writeDeclinedMessage !== void 0 ? { writeDeclinedMessage } : {},
219
233
  ...missingToolMessage ? { missingToolMessage } : {},
220
234
  ...onCallSettled ? { onCallSettled } : {}
235
+ }).finally(() => {
236
+ if (abortControllersRef.current.get(request.id) === abortController) {
237
+ abortControllersRef.current.delete(request.id);
238
+ }
221
239
  });
222
240
  if (results === null) {
223
241
  releaseClaim(claim);
@@ -1 +1 @@
1
- {"version":3,"file":"client-tool-bridge.cjs","sources":["../../../../agent-runtime/src/client-tool-bridge.ts"],"sourcesContent":["/**\n * The statewire client-tool bridge loop, shared by every surface that executes\n * frontend tool calls (Olympus, the React SDK, the Office add-in, mobile).\n *\n * Tools declared through `runConfig.custom.client_tools` have no backend\n * implementation: a call parks the run on an interrupt that arrives as a\n * statewire input request. A bridge with handlers for the whole batch claims\n * it (exactly-once per ownership domain), runs those handlers, and resumes\n * with per-call result envelopes. Other attached tabs leave it for the\n * executing surface; a thrown handler still returns an actionable error.\n *\n * Wire-adjacent correctness invariants this module owns, so they can never\n * drift apart per surface again:\n * - exactly-once claim per request, with un-claim on every path that did NOT\n * run any handler (abandoned before execution, consent dismissed, unmount\n * before the first handler) so a re-emitted request is executable instead\n * of stranding the run;\n * - once a batch has STARTED executing handlers, its claim is never released\n * by unmount: handlers cannot be cancelled, so releasing would let another\n * mount re-claim the re-emitted request and re-execute side effects that\n * already ran (double-executing a mutating tool). The completed wire-safe\n * result is cached and may be replayed across reconnects/remounts instead;\n * - liveness checks between awaits (the run can be stopped or answered\n * elsewhere while a handler runs) so a stale request is never resumed;\n * - per-call JSON wire-safety, so one unrepresentable handler result answers\n * that one call with an error instead of failing the whole batch.\n *\n * Like `interrupts.ts`, this is parameterized rather than self-wiring: the\n * host component reads `useStatewireRuns().inputRequests` and\n * `useStatewireSendCommand()` itself (both mockable per surface) and passes\n * them in, together with its ownership tracker and write-consent policy.\n */\n\nimport type { StatewireThread } from '@assistant-ui/react-statewire';\nimport { useCallback, useEffect, useRef } from 'react';\nimport {\n type ClientToolCallRequest,\n type ClientToolRequestTracker,\n type ClientToolResultEnvelope,\n clientToolCallArgs,\n clientToolErrorEnvelope,\n clientToolResultsResumeValue,\n clientToolSuccessEnvelope,\n isClientToolInterrupt,\n} from './client-tools';\n\n/** A locally executable tool, normalized across the per-surface tool shapes. */\nexport interface ClientToolBridgeTool {\n /** Snake_case tool id the model calls (e.g. `update_sheet_range`). */\n name: string;\n /** Mutating tools gate on the surface's write-consent flow (`confirmWrites`). */\n requiresWrite?: boolean;\n run: (\n args: Record<string, unknown>,\n context: { toolCallId: string },\n ) => Promise<unknown>;\n}\n\n/**\n * Batch write-consent outcome. `proceed` runs the write calls, `declined`\n * answers them with the declined message while reads still run, `abandoned`\n * releases the claim without resuming (the request died, or the consent UI was\n * dismissed by the run moving on). Grant persistence (a thread set, a stored\n * preference) is the surface's own policy, applied inside its callback.\n */\nexport type ClientToolWriteConsentDecision = 'proceed' | 'declined' | 'abandoned';\n\nexport interface ClientToolCallSettledEvent {\n toolName: string;\n status: 'success' | 'error' | 'declined';\n errorType?: string;\n}\n\n/** Default resume error for a write call the user declined. */\nexport const CLIENT_TOOL_WRITE_DECLINED_MESSAGE =\n 'The user declined this edit. Do not retry it; ask the user how to proceed.';\n\n/** Default resume error for a call naming a tool this surface does not have. */\nexport function defaultMissingClientToolMessage(toolName: string): string {\n return `This surface has no tool named '${toolName}'.`;\n}\n\n/** Statewire request ids are unique per run, but a thread scope keeps a\n * process-wide claim unambiguous when two conversations are open at once. */\nexport function clientToolClaimKey(threadId: string, requestId: string): string {\n return `${threadId}:${requestId}`;\n}\n\nconst CLIENT_TOOL_RESUME_RETRY_DELAYS_MS = [2_000, 5_000, 10_000, 15_000, 30_000];\nconst MAX_CLIENT_TOOL_RESUME_ATTEMPTS = 25;\n\nfunction clientToolResumeRetryDelayMs(attemptNumber: number): number {\n const index = Math.min(\n Math.max(attemptNumber - 1, 0),\n CLIENT_TOOL_RESUME_RETRY_DELAYS_MS.length - 1,\n );\n return CLIENT_TOOL_RESUME_RETRY_DELAYS_MS[index] ?? 30_000;\n}\n\n/**\n * A wire-safe copy of a handler's return value: handlers can return values\n * JSON cannot represent (DOM nodes, host objects, bigints), which must never\n * reach the transport. Throws for an unrepresentable value so the caller can\n * answer that one call with an error instead of stranding the whole batch.\n */\nexport function jsonSafeClientToolResult(value: unknown): unknown {\n const serialized = JSON.stringify(value);\n return serialized === undefined ? null : JSON.parse(serialized);\n}\n\n/** A parked, unanswered client-tool input request. */\nexport function isPendingClientToolRequest(\n request: StatewireThread.InputRequestState,\n): request is StatewireThread.CustomInputRequest {\n if (request.type !== 'interrupt' || 'response' in request) return false;\n return isClientToolInterrupt((request as { payload?: unknown }).payload);\n}\n\nexport interface ExecuteClientToolBatchOptions {\n calls: readonly ClientToolCallRequest[];\n tools: readonly ClientToolBridgeTool[];\n /** Whether the parked request still exists and this surface may answer it.\n * Checked before consent resolution, before every call, and before the\n * results are handed back for resume. */\n isLive: () => boolean;\n /** Batch write gate; absent means writes run without consent (surfaces that\n * only register read tools). Called once per batch, before any handler. */\n confirmWrites?: (batch: {\n writeToolNames: readonly string[];\n }) => Promise<ClientToolWriteConsentDecision>;\n writeDeclinedMessage?: string;\n missingToolMessage?: (toolName: string) => string;\n /** Per-call telemetry (the Office add-in's tool events). */\n onCallSettled?: (event: ClientToolCallSettledEvent) => void;\n /**\n * Called once, after the write-consent gate resolves and immediately before\n * the first handler runs — the point where side effects begin. There is no\n * await between this callback and the first handler invocation, so a caller\n * marking \"execution started\" here can never miss a handler that ran.\n */\n onExecutionStart?: () => void;\n}\n\n/**\n * Execute one batched client-tool park: consent, per-call execution with\n * wire-safe envelopes, liveness aborts. Returns the per-interrupt result map\n * to resume with, or `null` when the batch was abandoned (the request died,\n * consent was abandoned, or the surface went away) — the caller must release\n * its claim and must NOT resume.\n */\nexport async function executeClientToolBatch(\n options: ExecuteClientToolBatchOptions,\n): Promise<Record<string, ClientToolResultEnvelope> | null> {\n const {\n calls,\n tools,\n isLive,\n confirmWrites,\n writeDeclinedMessage = CLIENT_TOOL_WRITE_DECLINED_MESSAGE,\n missingToolMessage = defaultMissingClientToolMessage,\n onCallSettled,\n onExecutionStart,\n } = options;\n const registry = new Map(tools.map((tool) => [tool.name, tool]));\n\n // One consent covers the whole batch (and whatever scope the surface's\n // callback persists): writes are declined together while reads still run.\n let writesDeclined = false;\n const writeToolNames = calls\n .filter((call) => registry.get(call.tool_name)?.requiresWrite === true)\n .map((call) => call.tool_name);\n if (writeToolNames.length > 0 && confirmWrites) {\n const decision = await confirmWrites({ writeToolNames });\n if (decision === 'abandoned' || !isLive()) {\n // The run was stopped (or answered elsewhere) while consent was\n // pending: abort before any handler mutates the user's asset.\n return null;\n }\n writesDeclined = decision === 'declined';\n }\n\n onExecutionStart?.();\n\n const results: Record<string, ClientToolResultEnvelope> = {};\n for (const call of calls) {\n if (!isLive()) return null;\n const tool = registry.get(call.tool_name);\n let envelope: ClientToolResultEnvelope;\n let status: ClientToolCallSettledEvent['status'] = 'success';\n let errorType: string | undefined;\n if (!tool) {\n envelope = clientToolErrorEnvelope(missingToolMessage(call.tool_name));\n status = 'error';\n errorType = 'UnknownClientTool';\n } else if (tool.requiresWrite === true && writesDeclined) {\n envelope = clientToolErrorEnvelope(writeDeclinedMessage);\n status = 'declined';\n } else {\n try {\n envelope = clientToolSuccessEnvelope(\n jsonSafeClientToolResult(\n await tool.run(clientToolCallArgs(call), {\n toolCallId: call.interrupt_id,\n }),\n ),\n );\n } catch (error) {\n envelope = clientToolErrorEnvelope(error);\n status = 'error';\n errorType = error instanceof Error ? error.name : typeof error;\n }\n }\n results[call.interrupt_id] = envelope;\n onCallSettled?.({\n toolName: call.tool_name,\n status,\n ...(errorType !== undefined ? { errorType } : {}),\n });\n }\n\n // Abandoned mid-flight: resuming a dead request would error, and a\n // re-emitted one must be executable again.\n return isLive() ? results : null;\n}\n\nexport interface UseStatewireClientToolBridgeOptions {\n threadId: string;\n tools: readonly ClientToolBridgeTool[];\n /**\n * Ownership domain for parked-request claims. Pass a module-scoped tracker\n * to make ownership process-wide across every mount of the surface (two\n * mounted surfaces of one conversation execute each request exactly once),\n * or a per-mount tracker to scope ownership to one mounted bridge.\n */\n tracker: ClientToolRequestTracker;\n /** The surface's `useStatewireRuns().inputRequests` read. */\n inputRequests: readonly StatewireThread.InputRequestState[] | undefined;\n /** Dispatch the `run/input` resume on the surface's transport. The value is\n * already wire-safe (per-call JSON round-trip inside the batch engine). */\n /** Dispatch the answer and settle only once the Statewire host accepts or\n * rejects the command. The bridge deliberately keeps replaying while this\n * promise is pending: `run/input` removes the request optimistically before\n * the server has accepted the answer. */\n sendResume: (\n requestId: string,\n value: Record<string, unknown>,\n ) => PromiseLike<unknown>;\n /** Batch write gate, with the parked request's id so the surface can close\n * its consent UI the instant that request disappears. */\n confirmWrites?: (batch: {\n requestId: string;\n writeToolNames: readonly string[];\n }) => Promise<ClientToolWriteConsentDecision>;\n writeDeclinedMessage?: string;\n missingToolMessage?: (toolName: string) => string;\n onCallSettled?: (event: ClientToolCallSettledEvent) => void;\n /** Log prefix for resume-dispatch failures. */\n logLabel?: string;\n}\n\n/**\n * The claim lifecycle around {@link executeClientToolBatch}: claim the pending\n * request once per ownership domain, execute, resume — releasing the claim on\n * every path that ran no handler, so re-emitted requests recover.\n *\n * Unmount splits by whether side effects began:\n * - a claim whose batch has NOT started executing (claimed this tick, or\n * parked on write consent) is released, so another mount — or a remount —\n * can execute the re-emitted request instead of stranding the parked run;\n * - a claim whose batch HAS started stays held: handlers cannot be cancelled,\n * so releasing would let a sibling mount re-claim and re-execute a mutation\n * whose side effects already ran. The detached execution caches its result\n * and attempts one resume through its captured transport; a later mount can\n * replay the same result without repeating the side effect.\n */\nexport function useStatewireClientToolBridge(\n options: UseStatewireClientToolBridgeOptions,\n): void {\n const { threadId, tracker, inputRequests, logLabel = '[AthenaSDK]' } = options;\n const optionsRef = useRef(options);\n optionsRef.current = options;\n\n const toolNames = new Set(options.tools.map((tool) => tool.name));\n const pendingRequest = inputRequests?.find((request) => {\n if (!isPendingClientToolRequest(request)) return false;\n const payload = (request as StatewireThread.CustomInputRequest).payload;\n if (!isClientToolInterrupt(payload)) return false;\n // Every attached tab sees the same park. An observer without these tools\n // must not claim it and race the executing surface with an unknown-tool\n // error (for example, Olympus watching an Excel conversation).\n // A cached result already belongs to this tracker and can be replayed\n // without loading its handlers again.\n return (\n tracker.getCachedResume(clientToolClaimKey(threadId, request.id)) !== undefined ||\n payload.context.requests.every((call) => toolNames.has(call.tool_name))\n );\n });\n\n // Claims this bridge holds but has not resumed, split by whether their\n // batch has begun running handlers. The ref Set instances are stable (only\n // their contents mutate), so the unmount cleanup sees every claim.\n const ownedClaimsRef = useRef<Set<string>>(new Set());\n const startedClaimsRef = useRef<Set<string>>(new Set());\n const resumeOwnerRef = useRef(Symbol('client-tool-resume-owner'));\n const resumeClaimsRef = useRef<Set<string>>(new Set());\n const resumeTimersRef = useRef<\n Map<string, { requestId: string; timer: ReturnType<typeof setTimeout> }>\n >(new Map());\n const activeRef = useRef(true);\n const releaseClaim = useCallback(\n (claim: string) => {\n // Release the shared tracker entry only while THIS mount still owns it.\n // A detached batch can settle after the unmount cleanup already released\n // (and disowned) its claim; by then the tracker entry may belong to\n // another mount that claimed, executed, and resumed — releasing it here\n // would let a stale re-emission of the same request execute again.\n const owned = ownedClaimsRef.current.delete(claim);\n startedClaimsRef.current.delete(claim);\n if (owned) tracker.release(claim);\n },\n [tracker],\n );\n const stopResumeReplay = useCallback(\n (claim: string) => {\n const timerEntry = resumeTimersRef.current.get(claim);\n if (timerEntry) clearTimeout(timerEntry.timer);\n resumeTimersRef.current.delete(claim);\n if (resumeClaimsRef.current.delete(claim)) {\n tracker.releaseResumeReplay(claim, resumeOwnerRef.current);\n }\n },\n [tracker],\n );\n useEffect(() => {\n activeRef.current = true;\n const ownedClaims = ownedClaimsRef.current;\n const startedClaims = startedClaimsRef.current;\n return () => {\n activeRef.current = false;\n for (const claim of Array.from(resumeClaimsRef.current)) {\n stopResumeReplay(claim);\n }\n for (const claim of ownedClaims) {\n // Started batches keep their claim — their detached execution settles\n // it (resume, or release on a failed dispatch). Releasing here would\n // double-execute the side effects that already ran.\n if (startedClaims.has(claim)) continue;\n ownedClaims.delete(claim);\n tracker.release(claim);\n }\n };\n }, [stopResumeReplay, tracker]);\n\n // Handlers (and consent) are async, so the run can be stopped or the request\n // answered elsewhere while we await. The live ids let us abandon an\n // unfinished local execution when its request is no longer present. Once a\n // result is cached, only command settlement may stop its replay: Statewire\n // removes submitted requests optimistically before the host accepts them.\n const liveRequestIdsRef = useRef<ReadonlySet<string>>(new Set());\n liveRequestIdsRef.current = new Set((inputRequests ?? []).map((request) => request.id));\n\n const startResumeReplay = useCallback(\n (claim: string, requestId: string) => {\n if (!activeRef.current || resumeClaimsRef.current.has(claim)) return;\n if (!tracker.claimResumeReplay(claim, resumeOwnerRef.current)) return;\n resumeClaimsRef.current.add(claim);\n\n const dispatch = (): void => {\n resumeTimersRef.current.delete(claim);\n if (!activeRef.current) {\n stopResumeReplay(claim);\n return;\n }\n const attempt = tracker.takeResumeAttempt(claim, {\n owner: resumeOwnerRef.current,\n maxAttempts: MAX_CLIENT_TOOL_RESUME_ATTEMPTS,\n });\n if (!attempt) {\n stopResumeReplay(claim);\n return;\n }\n\n try {\n const settled = optionsRef.current.sendResume(requestId, attempt.value);\n // A fulfilled command result is the authoritative acknowledgement:\n // the host accepted and applied the answer. The local input request\n // disappearing is NOT an acknowledgement because the Statewire\n // reducer does that optimistically as soon as the command is\n // submitted.\n void Promise.resolve(settled).then(\n () => {\n // Keep the execution claim, but discard the cached result once\n // the host accepts it. A stale pending-request render can no\n // longer replay the command or re-execute the local mutation.\n tracker.settleResume(claim);\n stopResumeReplay(claim);\n },\n (error) => {\n // A rejected attempt may have raced another still-pending\n // replay. Keep the bounded loop alive; any accepted sibling\n // stops it, while the global attempt cap prevents storms.\n console.error(`${logLabel} failed to resume client tool results:`, error);\n },\n );\n } catch (error) {\n // Keep the cached result and retry it. Re-executing a mutating local\n // handler would be unsafe; replaying its id-stamped result is safe.\n console.error(`${logLabel} failed to resume client tool results:`, error);\n }\n\n if (\n attempt.attemptNumber >= MAX_CLIENT_TOOL_RESUME_ATTEMPTS ||\n !activeRef.current ||\n !resumeClaimsRef.current.has(claim)\n ) {\n stopResumeReplay(claim);\n return;\n }\n const timer = setTimeout(dispatch, clientToolResumeRetryDelayMs(attempt.attemptNumber));\n resumeTimersRef.current.set(claim, { requestId, timer });\n };\n\n dispatch();\n },\n [logLabel, stopResumeReplay, tracker],\n );\n\n const executeRequest = useCallback(\n async (request: StatewireThread.CustomInputRequest) => {\n const payload = request.payload;\n if (!isClientToolInterrupt(payload)) return;\n const claim = clientToolClaimKey(threadId, request.id);\n const {\n tools,\n confirmWrites,\n writeDeclinedMessage,\n missingToolMessage,\n onCallSettled,\n sendResume,\n } = optionsRef.current;\n\n const results = await executeClientToolBatch({\n calls: payload.context.requests,\n tools,\n // Live while the request still exists AND this surface may answer it:\n // mounted, or already past the point of no return (handlers started —\n // the detached execution must finish and settle, never re-execute).\n // After unmount the request-id snapshot freezes at its last observed\n // state, which keeps a started batch's own request visible to it.\n isLive: () =>\n liveRequestIdsRef.current.has(request.id) &&\n (activeRef.current || startedClaimsRef.current.has(claim)),\n onExecutionStart: () => {\n startedClaimsRef.current.add(claim);\n },\n ...(confirmWrites\n ? {\n confirmWrites: ({ writeToolNames }: { writeToolNames: readonly string[] }) =>\n confirmWrites({ requestId: request.id, writeToolNames }),\n }\n : {}),\n ...(writeDeclinedMessage !== undefined ? { writeDeclinedMessage } : {}),\n ...(missingToolMessage ? { missingToolMessage } : {}),\n ...(onCallSettled ? { onCallSettled } : {}),\n });\n if (results === null) {\n releaseClaim(claim);\n return;\n }\n\n const resumeValue = clientToolResultsResumeValue(results);\n tracker.cacheResume(claim, resumeValue);\n // Execution is complete, but the exactly-once claim remains. A pending\n // re-emission must replay this cached value, never run the handlers again.\n ownedClaimsRef.current.delete(claim);\n startedClaimsRef.current.delete(claim);\n\n if (!activeRef.current) {\n // The handler crossed an unmount. Preserve the pre-existing best-effort\n // settle through its captured transport, but leave the cached result\n // available for a later mount if this dispatch is lost.\n if (tracker.claimResumeReplay(claim, resumeOwnerRef.current)) {\n const attempt = tracker.takeResumeAttempt(claim, {\n owner: resumeOwnerRef.current,\n maxAttempts: MAX_CLIENT_TOOL_RESUME_ATTEMPTS,\n });\n if (attempt) {\n try {\n void Promise.resolve(sendResume(request.id, attempt.value)).then(\n () => tracker.settleResume(claim),\n (error) => {\n console.error(`${logLabel} failed to resume client tool results:`, error);\n },\n );\n } catch (error) {\n console.error(`${logLabel} failed to resume client tool results:`, error);\n }\n }\n // Ownership only fences this best-effort dispatch; a future mount\n // may claim and replay the cached value if it did not settle.\n tracker.releaseResumeReplay(claim, resumeOwnerRef.current);\n }\n return;\n }\n\n startResumeReplay(claim, request.id);\n },\n [logLabel, releaseClaim, startResumeReplay, threadId, tracker],\n );\n\n useEffect(() => {\n if (!pendingRequest) return;\n const claim = clientToolClaimKey(threadId, pendingRequest.id);\n if (!tracker.claim(claim)) {\n // The local handler already ran. This is the reconnect/remount path:\n // replay its cached answer instead of double-executing the side effect.\n if (tracker.getCachedResume(claim)) {\n startResumeReplay(claim, pendingRequest.id);\n }\n return;\n }\n ownedClaimsRef.current.add(claim);\n void executeRequest(pendingRequest);\n }, [executeRequest, pendingRequest, startResumeReplay, threadId, tracker]);\n}\n"],"names":["isClientToolInterrupt","clientToolErrorEnvelope","clientToolSuccessEnvelope","clientToolCallArgs","useRef","useCallback","useEffect","clientToolResultsResumeValue"],"mappings":"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"}
1
+ {"version":3,"file":"client-tool-bridge.cjs","sources":["../../../../agent-runtime/src/client-tool-bridge.ts"],"sourcesContent":["/**\n * The statewire client-tool bridge loop, shared by every surface that executes\n * frontend tool calls (Olympus, the React SDK, the Office add-in, mobile).\n *\n * Tools declared through `runConfig.custom.client_tools` have no backend\n * implementation: a call parks the run on an interrupt that arrives as a\n * statewire input request. A bridge with handlers for the whole batch claims\n * it (exactly-once per ownership domain), runs those handlers, and resumes\n * with per-call result envelopes. Other attached tabs leave it for the\n * executing surface; a thrown handler still returns an actionable error.\n *\n * Wire-adjacent correctness invariants this module owns, so they can never\n * drift apart per surface again:\n * - exactly-once claim per request, with un-claim on every path that did NOT\n * run any handler (abandoned before execution, consent dismissed, unmount\n * before the first handler) so a re-emitted request is executable instead\n * of stranding the run;\n * - once a batch has STARTED executing handlers, its claim is never released\n * by unmount: handlers cannot be cancelled, so releasing would let another\n * mount re-claim the re-emitted request and re-execute side effects that\n * already ran (double-executing a mutating tool). The completed wire-safe\n * result is cached and may be replayed across reconnects/remounts instead;\n * - liveness checks between awaits (the run can be stopped or answered\n * elsewhere while a handler runs) so a stale request is never resumed;\n * - per-call JSON wire-safety, so one unrepresentable handler result answers\n * that one call with an error instead of failing the whole batch.\n *\n * Like `interrupts.ts`, this is parameterized rather than self-wiring: the\n * host component reads `useStatewireRuns().inputRequests` and\n * `useStatewireSendCommand()` itself (both mockable per surface) and passes\n * them in, together with its ownership tracker and write-consent policy.\n */\n\nimport type { StatewireThread } from '@assistant-ui/react-statewire';\nimport { useCallback, useEffect, useRef } from 'react';\nimport {\n type ClientToolCallRequest,\n type ClientToolHandlerContext,\n type ClientToolRequestTracker,\n type ClientToolResultEnvelope,\n clientToolCallArgs,\n clientToolErrorEnvelope,\n clientToolResultsResumeValue,\n clientToolSuccessEnvelope,\n isClientToolInterrupt,\n} from './client-tools';\n\n/** A locally executable tool, normalized across the per-surface tool shapes. */\nexport interface ClientToolBridgeTool {\n /** Snake_case tool id the model calls (e.g. `update_sheet_range`). */\n name: string;\n /** Mutating tools gate on the surface's write-consent flow (`confirmWrites`). */\n requiresWrite?: boolean;\n run: (\n args: Record<string, unknown>,\n context: ClientToolHandlerContext,\n ) => Promise<unknown>;\n}\n\n/**\n * Batch write-consent outcome. `proceed` runs the write calls, `declined`\n * answers them with the declined message while reads still run, `abandoned`\n * releases the claim without resuming (the request died, or the consent UI was\n * dismissed by the run moving on). Grant persistence (a thread set, a stored\n * preference) is the surface's own policy, applied inside its callback.\n */\nexport type ClientToolWriteConsentDecision = 'proceed' | 'declined' | 'abandoned';\n\nexport interface ClientToolCallSettledEvent {\n toolName: string;\n status: 'success' | 'error' | 'declined';\n errorType?: string;\n}\n\n/** Default resume error for a write call the user declined. */\nexport const CLIENT_TOOL_WRITE_DECLINED_MESSAGE =\n 'The user declined this edit. Do not retry it; ask the user how to proceed.';\n\n/** Default resume error for a call naming a tool this surface does not have. */\nexport function defaultMissingClientToolMessage(toolName: string): string {\n return `This surface has no tool named '${toolName}'.`;\n}\n\n/** Statewire request ids are unique per run, but a thread scope keeps a\n * process-wide claim unambiguous when two conversations are open at once. */\nexport function clientToolClaimKey(threadId: string, requestId: string): string {\n return `${threadId}:${requestId}`;\n}\n\nconst CLIENT_TOOL_RESUME_RETRY_DELAYS_MS = [2_000, 5_000, 10_000, 15_000, 30_000];\nconst MAX_CLIENT_TOOL_RESUME_ATTEMPTS = 25;\n\nfunction clientToolResumeRetryDelayMs(attemptNumber: number): number {\n const index = Math.min(\n Math.max(attemptNumber - 1, 0),\n CLIENT_TOOL_RESUME_RETRY_DELAYS_MS.length - 1,\n );\n return CLIENT_TOOL_RESUME_RETRY_DELAYS_MS[index] ?? 30_000;\n}\n\n/**\n * A wire-safe copy of a handler's return value: handlers can return values\n * JSON cannot represent (DOM nodes, host objects, bigints), which must never\n * reach the transport. Throws for an unrepresentable value so the caller can\n * answer that one call with an error instead of stranding the whole batch.\n */\nexport function jsonSafeClientToolResult(value: unknown): unknown {\n const serialized = JSON.stringify(value);\n return serialized === undefined ? null : JSON.parse(serialized);\n}\n\n/** A parked, unanswered client-tool input request. */\nexport function isPendingClientToolRequest(\n request: StatewireThread.InputRequestState,\n): request is StatewireThread.CustomInputRequest {\n if (request.type !== 'interrupt' || 'response' in request) return false;\n return isClientToolInterrupt((request as { payload?: unknown }).payload);\n}\n\nexport interface ExecuteClientToolBatchOptions {\n calls: readonly ClientToolCallRequest[];\n tools: readonly ClientToolBridgeTool[];\n /** Whether the parked request still exists and this surface may answer it.\n * Checked before consent resolution, before every call, and before the\n * results are handed back for resume. */\n isLive: () => boolean;\n /** Batch write gate; absent means writes run without consent (surfaces that\n * only register read tools). Called once per batch, before any handler. */\n confirmWrites?: (batch: {\n writeToolNames: readonly string[];\n }) => Promise<ClientToolWriteConsentDecision>;\n writeDeclinedMessage?: string;\n missingToolMessage?: (toolName: string) => string;\n /** Per-call telemetry (the Office add-in's tool events). */\n onCallSettled?: (event: ClientToolCallSettledEvent) => void;\n /**\n * Called once, after the write-consent gate resolves and immediately before\n * the first handler runs — the point where side effects begin. There is no\n * await between this callback and the first handler invocation, so a caller\n * marking \"execution started\" here can never miss a handler that ran.\n */\n onExecutionStart?: () => void;\n /**\n * Handed to every handler of the batch (`context.signal`). The caller aborts\n * it when the parked request goes away mid-execution, so a handler that\n * takes visible time can stop; the batch's results are discarded either way\n * (`isLive` is what decides that).\n */\n signal?: AbortSignal;\n}\n\n/**\n * Execute one batched client-tool park: consent, per-call execution with\n * wire-safe envelopes, liveness aborts. Returns the per-interrupt result map\n * to resume with, or `null` when the batch was abandoned (the request died,\n * consent was abandoned, or the surface went away) — the caller must release\n * its claim and must NOT resume.\n */\nexport async function executeClientToolBatch(\n options: ExecuteClientToolBatchOptions,\n): Promise<Record<string, ClientToolResultEnvelope> | null> {\n const {\n calls,\n tools,\n isLive,\n confirmWrites,\n writeDeclinedMessage = CLIENT_TOOL_WRITE_DECLINED_MESSAGE,\n missingToolMessage = defaultMissingClientToolMessage,\n onCallSettled,\n onExecutionStart,\n signal,\n } = options;\n const registry = new Map(tools.map((tool) => [tool.name, tool]));\n\n // One consent covers the whole batch (and whatever scope the surface's\n // callback persists): writes are declined together while reads still run.\n let writesDeclined = false;\n const writeToolNames = calls\n .filter((call) => registry.get(call.tool_name)?.requiresWrite === true)\n .map((call) => call.tool_name);\n if (writeToolNames.length > 0 && confirmWrites) {\n const decision = await confirmWrites({ writeToolNames });\n if (decision === 'abandoned' || !isLive()) {\n // The run was stopped (or answered elsewhere) while consent was\n // pending: abort before any handler mutates the user's asset.\n return null;\n }\n writesDeclined = decision === 'declined';\n }\n\n onExecutionStart?.();\n\n const results: Record<string, ClientToolResultEnvelope> = {};\n for (const call of calls) {\n if (!isLive()) return null;\n const tool = registry.get(call.tool_name);\n let envelope: ClientToolResultEnvelope;\n let status: ClientToolCallSettledEvent['status'] = 'success';\n let errorType: string | undefined;\n if (!tool) {\n envelope = clientToolErrorEnvelope(missingToolMessage(call.tool_name));\n status = 'error';\n errorType = 'UnknownClientTool';\n } else if (tool.requiresWrite === true && writesDeclined) {\n envelope = clientToolErrorEnvelope(writeDeclinedMessage);\n status = 'declined';\n } else {\n try {\n envelope = clientToolSuccessEnvelope(\n jsonSafeClientToolResult(\n await tool.run(clientToolCallArgs(call), {\n toolCallId: call.interrupt_id,\n ...(signal ? { signal } : {}),\n }),\n ),\n );\n } catch (error) {\n envelope = clientToolErrorEnvelope(error);\n status = 'error';\n errorType = error instanceof Error ? error.name : typeof error;\n }\n }\n results[call.interrupt_id] = envelope;\n onCallSettled?.({\n toolName: call.tool_name,\n status,\n ...(errorType !== undefined ? { errorType } : {}),\n });\n }\n\n // Abandoned mid-flight: resuming a dead request would error, and a\n // re-emitted one must be executable again.\n return isLive() ? results : null;\n}\n\nexport interface UseStatewireClientToolBridgeOptions {\n threadId: string;\n tools: readonly ClientToolBridgeTool[];\n /**\n * Ownership domain for parked-request claims. Pass a module-scoped tracker\n * to make ownership process-wide across every mount of the surface (two\n * mounted surfaces of one conversation execute each request exactly once),\n * or a per-mount tracker to scope ownership to one mounted bridge.\n */\n tracker: ClientToolRequestTracker;\n /** The surface's `useStatewireRuns().inputRequests` read. */\n inputRequests: readonly StatewireThread.InputRequestState[] | undefined;\n /** Dispatch the `run/input` resume on the surface's transport. The value is\n * already wire-safe (per-call JSON round-trip inside the batch engine). */\n /** Dispatch the answer and settle only once the Statewire host accepts or\n * rejects the command. The bridge deliberately keeps replaying while this\n * promise is pending: `run/input` removes the request optimistically before\n * the server has accepted the answer. */\n sendResume: (\n requestId: string,\n value: Record<string, unknown>,\n ) => PromiseLike<unknown>;\n /** Batch write gate, with the parked request's id so the surface can close\n * its consent UI the instant that request disappears. */\n confirmWrites?: (batch: {\n requestId: string;\n writeToolNames: readonly string[];\n }) => Promise<ClientToolWriteConsentDecision>;\n writeDeclinedMessage?: string;\n missingToolMessage?: (toolName: string) => string;\n onCallSettled?: (event: ClientToolCallSettledEvent) => void;\n /** Log prefix for resume-dispatch failures. */\n logLabel?: string;\n}\n\n/**\n * The claim lifecycle around {@link executeClientToolBatch}: claim the pending\n * request once per ownership domain, execute, resume — releasing the claim on\n * every path that ran no handler, so re-emitted requests recover.\n *\n * Unmount splits by whether side effects began:\n * - a claim whose batch has NOT started executing (claimed this tick, or\n * parked on write consent) is released, so another mount — or a remount —\n * can execute the re-emitted request instead of stranding the parked run;\n * - a claim whose batch HAS started stays held: handlers cannot be cancelled,\n * so releasing would let a sibling mount re-claim and re-execute a mutation\n * whose side effects already ran. The detached execution caches its result\n * and attempts one resume through its captured transport; a later mount can\n * replay the same result without repeating the side effect.\n */\nexport function useStatewireClientToolBridge(\n options: UseStatewireClientToolBridgeOptions,\n): void {\n const { threadId, tracker, inputRequests, logLabel = '[AthenaSDK]' } = options;\n const optionsRef = useRef(options);\n optionsRef.current = options;\n\n const toolNames = new Set(options.tools.map((tool) => tool.name));\n const pendingRequest = inputRequests?.find((request) => {\n if (!isPendingClientToolRequest(request)) return false;\n const payload = (request as StatewireThread.CustomInputRequest).payload;\n if (!isClientToolInterrupt(payload)) return false;\n // Every attached tab sees the same park. An observer without these tools\n // must not claim it and race the executing surface with an unknown-tool\n // error (for example, Olympus watching an Excel conversation).\n // A cached result already belongs to this tracker and can be replayed\n // without loading its handlers again.\n return (\n tracker.getCachedResume(clientToolClaimKey(threadId, request.id)) !== undefined ||\n payload.context.requests.every((call) => toolNames.has(call.tool_name))\n );\n });\n\n // Claims this bridge holds but has not resumed, split by whether their\n // batch has begun running handlers. The ref Set instances are stable (only\n // their contents mutate), so the unmount cleanup sees every claim.\n const ownedClaimsRef = useRef<Set<string>>(new Set());\n const startedClaimsRef = useRef<Set<string>>(new Set());\n const resumeOwnerRef = useRef(Symbol('client-tool-resume-owner'));\n const resumeClaimsRef = useRef<Set<string>>(new Set());\n const resumeTimersRef = useRef<\n Map<string, { requestId: string; timer: ReturnType<typeof setTimeout> }>\n >(new Map());\n const activeRef = useRef(true);\n const releaseClaim = useCallback(\n (claim: string) => {\n // Release the shared tracker entry only while THIS mount still owns it.\n // A detached batch can settle after the unmount cleanup already released\n // (and disowned) its claim; by then the tracker entry may belong to\n // another mount that claimed, executed, and resumed — releasing it here\n // would let a stale re-emission of the same request execute again.\n const owned = ownedClaimsRef.current.delete(claim);\n startedClaimsRef.current.delete(claim);\n if (owned) tracker.release(claim);\n },\n [tracker],\n );\n const stopResumeReplay = useCallback(\n (claim: string) => {\n const timerEntry = resumeTimersRef.current.get(claim);\n if (timerEntry) clearTimeout(timerEntry.timer);\n resumeTimersRef.current.delete(claim);\n if (resumeClaimsRef.current.delete(claim)) {\n tracker.releaseResumeReplay(claim, resumeOwnerRef.current);\n }\n },\n [tracker],\n );\n useEffect(() => {\n activeRef.current = true;\n const ownedClaims = ownedClaimsRef.current;\n const startedClaims = startedClaimsRef.current;\n return () => {\n activeRef.current = false;\n for (const claim of Array.from(resumeClaimsRef.current)) {\n stopResumeReplay(claim);\n }\n for (const claim of ownedClaims) {\n // Started batches keep their claim — their detached execution settles\n // it (resume, or release on a failed dispatch). Releasing here would\n // double-execute the side effects that already ran.\n if (startedClaims.has(claim)) continue;\n ownedClaims.delete(claim);\n tracker.release(claim);\n }\n };\n }, [stopResumeReplay, tracker]);\n\n // Handlers (and consent) are async, so the run can be stopped or the request\n // answered elsewhere while we await. The live ids let us abandon an\n // unfinished local execution when its request is no longer present. Once a\n // result is cached, only command settlement may stop its replay: Statewire\n // removes submitted requests optimistically before the host accepts them.\n const liveRequestIdsRef = useRef<ReadonlySet<string>>(new Set());\n liveRequestIdsRef.current = new Set((inputRequests ?? []).map((request) => request.id));\n\n // One abort signal per executing request, handed to its handlers. It fires\n // when the request leaves the mounted surface's view (stop, an answer from\n // elsewhere, another conversation opened): the results would be discarded,\n // so a handler that is still showing the person something can stop. It does\n // not fire on unmount: a started batch finishes and settles (see above).\n const abortControllersRef = useRef<Map<string, AbortController>>(new Map());\n useEffect(() => {\n const liveRequestIds = new Set((inputRequests ?? []).map((request) => request.id));\n for (const [requestId, controller] of abortControllersRef.current) {\n if (liveRequestIds.has(requestId)) continue;\n abortControllersRef.current.delete(requestId);\n controller.abort();\n }\n }, [inputRequests]);\n\n const startResumeReplay = useCallback(\n (claim: string, requestId: string) => {\n if (!activeRef.current || resumeClaimsRef.current.has(claim)) return;\n if (!tracker.claimResumeReplay(claim, resumeOwnerRef.current)) return;\n resumeClaimsRef.current.add(claim);\n\n const dispatch = (): void => {\n resumeTimersRef.current.delete(claim);\n if (!activeRef.current) {\n stopResumeReplay(claim);\n return;\n }\n const attempt = tracker.takeResumeAttempt(claim, {\n owner: resumeOwnerRef.current,\n maxAttempts: MAX_CLIENT_TOOL_RESUME_ATTEMPTS,\n });\n if (!attempt) {\n stopResumeReplay(claim);\n return;\n }\n\n try {\n const settled = optionsRef.current.sendResume(requestId, attempt.value);\n // A fulfilled command result is the authoritative acknowledgement:\n // the host accepted and applied the answer. The local input request\n // disappearing is NOT an acknowledgement because the Statewire\n // reducer does that optimistically as soon as the command is\n // submitted.\n void Promise.resolve(settled).then(\n () => {\n // Keep the execution claim, but discard the cached result once\n // the host accepts it. A stale pending-request render can no\n // longer replay the command or re-execute the local mutation.\n tracker.settleResume(claim);\n stopResumeReplay(claim);\n },\n (error) => {\n // A rejected attempt may have raced another still-pending\n // replay. Keep the bounded loop alive; any accepted sibling\n // stops it, while the global attempt cap prevents storms.\n console.error(`${logLabel} failed to resume client tool results:`, error);\n },\n );\n } catch (error) {\n // Keep the cached result and retry it. Re-executing a mutating local\n // handler would be unsafe; replaying its id-stamped result is safe.\n console.error(`${logLabel} failed to resume client tool results:`, error);\n }\n\n if (\n attempt.attemptNumber >= MAX_CLIENT_TOOL_RESUME_ATTEMPTS ||\n !activeRef.current ||\n !resumeClaimsRef.current.has(claim)\n ) {\n stopResumeReplay(claim);\n return;\n }\n const timer = setTimeout(dispatch, clientToolResumeRetryDelayMs(attempt.attemptNumber));\n resumeTimersRef.current.set(claim, { requestId, timer });\n };\n\n dispatch();\n },\n [logLabel, stopResumeReplay, tracker],\n );\n\n const executeRequest = useCallback(\n async (request: StatewireThread.CustomInputRequest) => {\n const payload = request.payload;\n if (!isClientToolInterrupt(payload)) return;\n const claim = clientToolClaimKey(threadId, request.id);\n const {\n tools,\n confirmWrites,\n writeDeclinedMessage,\n missingToolMessage,\n onCallSettled,\n sendResume,\n } = optionsRef.current;\n\n const abortController = new AbortController();\n abortControllersRef.current.set(request.id, abortController);\n const results = await executeClientToolBatch({\n calls: payload.context.requests,\n tools,\n signal: abortController.signal,\n // Live while the request still exists AND this surface may answer it:\n // mounted, or already past the point of no return (handlers started —\n // the detached execution must finish and settle, never re-execute).\n // After unmount the request-id snapshot freezes at its last observed\n // state, which keeps a started batch's own request visible to it.\n isLive: () =>\n liveRequestIdsRef.current.has(request.id) &&\n (activeRef.current || startedClaimsRef.current.has(claim)),\n onExecutionStart: () => {\n startedClaimsRef.current.add(claim);\n },\n ...(confirmWrites\n ? {\n confirmWrites: ({ writeToolNames }: { writeToolNames: readonly string[] }) =>\n confirmWrites({ requestId: request.id, writeToolNames }),\n }\n : {}),\n ...(writeDeclinedMessage !== undefined ? { writeDeclinedMessage } : {}),\n ...(missingToolMessage ? { missingToolMessage } : {}),\n ...(onCallSettled ? { onCallSettled } : {}),\n }).finally(() => {\n if (abortControllersRef.current.get(request.id) === abortController) {\n abortControllersRef.current.delete(request.id);\n }\n });\n if (results === null) {\n releaseClaim(claim);\n return;\n }\n\n const resumeValue = clientToolResultsResumeValue(results);\n tracker.cacheResume(claim, resumeValue);\n // Execution is complete, but the exactly-once claim remains. A pending\n // re-emission must replay this cached value, never run the handlers again.\n ownedClaimsRef.current.delete(claim);\n startedClaimsRef.current.delete(claim);\n\n if (!activeRef.current) {\n // The handler crossed an unmount. Preserve the pre-existing best-effort\n // settle through its captured transport, but leave the cached result\n // available for a later mount if this dispatch is lost.\n if (tracker.claimResumeReplay(claim, resumeOwnerRef.current)) {\n const attempt = tracker.takeResumeAttempt(claim, {\n owner: resumeOwnerRef.current,\n maxAttempts: MAX_CLIENT_TOOL_RESUME_ATTEMPTS,\n });\n if (attempt) {\n try {\n void Promise.resolve(sendResume(request.id, attempt.value)).then(\n () => tracker.settleResume(claim),\n (error) => {\n console.error(`${logLabel} failed to resume client tool results:`, error);\n },\n );\n } catch (error) {\n console.error(`${logLabel} failed to resume client tool results:`, error);\n }\n }\n // Ownership only fences this best-effort dispatch; a future mount\n // may claim and replay the cached value if it did not settle.\n tracker.releaseResumeReplay(claim, resumeOwnerRef.current);\n }\n return;\n }\n\n startResumeReplay(claim, request.id);\n },\n [logLabel, releaseClaim, startResumeReplay, threadId, tracker],\n );\n\n useEffect(() => {\n if (!pendingRequest) return;\n const claim = clientToolClaimKey(threadId, pendingRequest.id);\n if (!tracker.claim(claim)) {\n // The local handler already ran. This is the reconnect/remount path:\n // replay its cached answer instead of double-executing the side effect.\n if (tracker.getCachedResume(claim)) {\n startResumeReplay(claim, pendingRequest.id);\n }\n return;\n }\n ownedClaimsRef.current.add(claim);\n void executeRequest(pendingRequest);\n }, [executeRequest, pendingRequest, startResumeReplay, threadId, tracker]);\n}\n"],"names":["isClientToolInterrupt","clientToolErrorEnvelope","clientToolSuccessEnvelope","clientToolCallArgs","useRef","useCallback","useEffect","clientToolResultsResumeValue"],"mappings":"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"}
@@ -31,16 +31,14 @@
31
31
  * them in, together with its ownership tracker and write-consent policy.
32
32
  */
33
33
  import type { StatewireThread } from '@assistant-ui/react-statewire';
34
- import { type ClientToolCallRequest, type ClientToolRequestTracker, type ClientToolResultEnvelope } from './client-tools';
34
+ import { type ClientToolCallRequest, type ClientToolHandlerContext, type ClientToolRequestTracker, type ClientToolResultEnvelope } from './client-tools';
35
35
  /** A locally executable tool, normalized across the per-surface tool shapes. */
36
36
  export interface ClientToolBridgeTool {
37
37
  /** Snake_case tool id the model calls (e.g. `update_sheet_range`). */
38
38
  name: string;
39
39
  /** Mutating tools gate on the surface's write-consent flow (`confirmWrites`). */
40
40
  requiresWrite?: boolean;
41
- run: (args: Record<string, unknown>, context: {
42
- toolCallId: string;
43
- }) => Promise<unknown>;
41
+ run: (args: Record<string, unknown>, context: ClientToolHandlerContext) => Promise<unknown>;
44
42
  }
45
43
  /**
46
44
  * Batch write-consent outcome. `proceed` runs the write calls, `declined`
@@ -94,6 +92,13 @@ export interface ExecuteClientToolBatchOptions {
94
92
  * marking "execution started" here can never miss a handler that ran.
95
93
  */
96
94
  onExecutionStart?: () => void;
95
+ /**
96
+ * Handed to every handler of the batch (`context.signal`). The caller aborts
97
+ * it when the parked request goes away mid-execution, so a handler that
98
+ * takes visible time can stop; the batch's results are discarded either way
99
+ * (`isLive` is what decides that).
100
+ */
101
+ signal?: AbortSignal;
97
102
  }
98
103
  /**
99
104
  * Execute one batched client-tool park: consent, per-call execution with
@@ -33,7 +33,8 @@ async function executeClientToolBatch(options) {
33
33
  writeDeclinedMessage = CLIENT_TOOL_WRITE_DECLINED_MESSAGE,
34
34
  missingToolMessage = defaultMissingClientToolMessage,
35
35
  onCallSettled,
36
- onExecutionStart
36
+ onExecutionStart,
37
+ signal
37
38
  } = options;
38
39
  const registry = new Map(tools.map((tool) => [tool.name, tool]));
39
40
  let writesDeclined = false;
@@ -68,7 +69,8 @@ async function executeClientToolBatch(options) {
68
69
  envelope = clientToolSuccessEnvelope(
69
70
  jsonSafeClientToolResult(
70
71
  await tool.run(clientToolCallArgs(call), {
71
- toolCallId: call.interrupt_id
72
+ toolCallId: call.interrupt_id,
73
+ ...signal ? { signal } : {}
72
74
  })
73
75
  )
74
76
  );
@@ -141,6 +143,15 @@ function useStatewireClientToolBridge(options) {
141
143
  }, [stopResumeReplay, tracker]);
142
144
  const liveRequestIdsRef = useRef(/* @__PURE__ */ new Set());
143
145
  liveRequestIdsRef.current = new Set((inputRequests ?? []).map((request) => request.id));
146
+ const abortControllersRef = useRef(/* @__PURE__ */ new Map());
147
+ useEffect(() => {
148
+ const liveRequestIds = new Set((inputRequests ?? []).map((request) => request.id));
149
+ for (const [requestId, controller] of abortControllersRef.current) {
150
+ if (liveRequestIds.has(requestId)) continue;
151
+ abortControllersRef.current.delete(requestId);
152
+ controller.abort();
153
+ }
154
+ }, [inputRequests]);
144
155
  const startResumeReplay = useCallback(
145
156
  (claim, requestId) => {
146
157
  if (!activeRef.current || resumeClaimsRef.current.has(claim)) return;
@@ -198,9 +209,12 @@ function useStatewireClientToolBridge(options) {
198
209
  onCallSettled,
199
210
  sendResume
200
211
  } = optionsRef.current;
212
+ const abortController = new AbortController();
213
+ abortControllersRef.current.set(request.id, abortController);
201
214
  const results = await executeClientToolBatch({
202
215
  calls: payload.context.requests,
203
216
  tools,
217
+ signal: abortController.signal,
204
218
  // Live while the request still exists AND this surface may answer it:
205
219
  // mounted, or already past the point of no return (handlers started —
206
220
  // the detached execution must finish and settle, never re-execute).
@@ -216,6 +230,10 @@ function useStatewireClientToolBridge(options) {
216
230
  ...writeDeclinedMessage !== void 0 ? { writeDeclinedMessage } : {},
217
231
  ...missingToolMessage ? { missingToolMessage } : {},
218
232
  ...onCallSettled ? { onCallSettled } : {}
233
+ }).finally(() => {
234
+ if (abortControllersRef.current.get(request.id) === abortController) {
235
+ abortControllersRef.current.delete(request.id);
236
+ }
219
237
  });
220
238
  if (results === null) {
221
239
  releaseClaim(claim);
@@ -1 +1 @@
1
- {"version":3,"file":"client-tool-bridge.js","sources":["../../../../agent-runtime/src/client-tool-bridge.ts"],"sourcesContent":["/**\n * The statewire client-tool bridge loop, shared by every surface that executes\n * frontend tool calls (Olympus, the React SDK, the Office add-in, mobile).\n *\n * Tools declared through `runConfig.custom.client_tools` have no backend\n * implementation: a call parks the run on an interrupt that arrives as a\n * statewire input request. A bridge with handlers for the whole batch claims\n * it (exactly-once per ownership domain), runs those handlers, and resumes\n * with per-call result envelopes. Other attached tabs leave it for the\n * executing surface; a thrown handler still returns an actionable error.\n *\n * Wire-adjacent correctness invariants this module owns, so they can never\n * drift apart per surface again:\n * - exactly-once claim per request, with un-claim on every path that did NOT\n * run any handler (abandoned before execution, consent dismissed, unmount\n * before the first handler) so a re-emitted request is executable instead\n * of stranding the run;\n * - once a batch has STARTED executing handlers, its claim is never released\n * by unmount: handlers cannot be cancelled, so releasing would let another\n * mount re-claim the re-emitted request and re-execute side effects that\n * already ran (double-executing a mutating tool). The completed wire-safe\n * result is cached and may be replayed across reconnects/remounts instead;\n * - liveness checks between awaits (the run can be stopped or answered\n * elsewhere while a handler runs) so a stale request is never resumed;\n * - per-call JSON wire-safety, so one unrepresentable handler result answers\n * that one call with an error instead of failing the whole batch.\n *\n * Like `interrupts.ts`, this is parameterized rather than self-wiring: the\n * host component reads `useStatewireRuns().inputRequests` and\n * `useStatewireSendCommand()` itself (both mockable per surface) and passes\n * them in, together with its ownership tracker and write-consent policy.\n */\n\nimport type { StatewireThread } from '@assistant-ui/react-statewire';\nimport { useCallback, useEffect, useRef } from 'react';\nimport {\n type ClientToolCallRequest,\n type ClientToolRequestTracker,\n type ClientToolResultEnvelope,\n clientToolCallArgs,\n clientToolErrorEnvelope,\n clientToolResultsResumeValue,\n clientToolSuccessEnvelope,\n isClientToolInterrupt,\n} from './client-tools';\n\n/** A locally executable tool, normalized across the per-surface tool shapes. */\nexport interface ClientToolBridgeTool {\n /** Snake_case tool id the model calls (e.g. `update_sheet_range`). */\n name: string;\n /** Mutating tools gate on the surface's write-consent flow (`confirmWrites`). */\n requiresWrite?: boolean;\n run: (\n args: Record<string, unknown>,\n context: { toolCallId: string },\n ) => Promise<unknown>;\n}\n\n/**\n * Batch write-consent outcome. `proceed` runs the write calls, `declined`\n * answers them with the declined message while reads still run, `abandoned`\n * releases the claim without resuming (the request died, or the consent UI was\n * dismissed by the run moving on). Grant persistence (a thread set, a stored\n * preference) is the surface's own policy, applied inside its callback.\n */\nexport type ClientToolWriteConsentDecision = 'proceed' | 'declined' | 'abandoned';\n\nexport interface ClientToolCallSettledEvent {\n toolName: string;\n status: 'success' | 'error' | 'declined';\n errorType?: string;\n}\n\n/** Default resume error for a write call the user declined. */\nexport const CLIENT_TOOL_WRITE_DECLINED_MESSAGE =\n 'The user declined this edit. Do not retry it; ask the user how to proceed.';\n\n/** Default resume error for a call naming a tool this surface does not have. */\nexport function defaultMissingClientToolMessage(toolName: string): string {\n return `This surface has no tool named '${toolName}'.`;\n}\n\n/** Statewire request ids are unique per run, but a thread scope keeps a\n * process-wide claim unambiguous when two conversations are open at once. */\nexport function clientToolClaimKey(threadId: string, requestId: string): string {\n return `${threadId}:${requestId}`;\n}\n\nconst CLIENT_TOOL_RESUME_RETRY_DELAYS_MS = [2_000, 5_000, 10_000, 15_000, 30_000];\nconst MAX_CLIENT_TOOL_RESUME_ATTEMPTS = 25;\n\nfunction clientToolResumeRetryDelayMs(attemptNumber: number): number {\n const index = Math.min(\n Math.max(attemptNumber - 1, 0),\n CLIENT_TOOL_RESUME_RETRY_DELAYS_MS.length - 1,\n );\n return CLIENT_TOOL_RESUME_RETRY_DELAYS_MS[index] ?? 30_000;\n}\n\n/**\n * A wire-safe copy of a handler's return value: handlers can return values\n * JSON cannot represent (DOM nodes, host objects, bigints), which must never\n * reach the transport. Throws for an unrepresentable value so the caller can\n * answer that one call with an error instead of stranding the whole batch.\n */\nexport function jsonSafeClientToolResult(value: unknown): unknown {\n const serialized = JSON.stringify(value);\n return serialized === undefined ? null : JSON.parse(serialized);\n}\n\n/** A parked, unanswered client-tool input request. */\nexport function isPendingClientToolRequest(\n request: StatewireThread.InputRequestState,\n): request is StatewireThread.CustomInputRequest {\n if (request.type !== 'interrupt' || 'response' in request) return false;\n return isClientToolInterrupt((request as { payload?: unknown }).payload);\n}\n\nexport interface ExecuteClientToolBatchOptions {\n calls: readonly ClientToolCallRequest[];\n tools: readonly ClientToolBridgeTool[];\n /** Whether the parked request still exists and this surface may answer it.\n * Checked before consent resolution, before every call, and before the\n * results are handed back for resume. */\n isLive: () => boolean;\n /** Batch write gate; absent means writes run without consent (surfaces that\n * only register read tools). Called once per batch, before any handler. */\n confirmWrites?: (batch: {\n writeToolNames: readonly string[];\n }) => Promise<ClientToolWriteConsentDecision>;\n writeDeclinedMessage?: string;\n missingToolMessage?: (toolName: string) => string;\n /** Per-call telemetry (the Office add-in's tool events). */\n onCallSettled?: (event: ClientToolCallSettledEvent) => void;\n /**\n * Called once, after the write-consent gate resolves and immediately before\n * the first handler runs — the point where side effects begin. There is no\n * await between this callback and the first handler invocation, so a caller\n * marking \"execution started\" here can never miss a handler that ran.\n */\n onExecutionStart?: () => void;\n}\n\n/**\n * Execute one batched client-tool park: consent, per-call execution with\n * wire-safe envelopes, liveness aborts. Returns the per-interrupt result map\n * to resume with, or `null` when the batch was abandoned (the request died,\n * consent was abandoned, or the surface went away) — the caller must release\n * its claim and must NOT resume.\n */\nexport async function executeClientToolBatch(\n options: ExecuteClientToolBatchOptions,\n): Promise<Record<string, ClientToolResultEnvelope> | null> {\n const {\n calls,\n tools,\n isLive,\n confirmWrites,\n writeDeclinedMessage = CLIENT_TOOL_WRITE_DECLINED_MESSAGE,\n missingToolMessage = defaultMissingClientToolMessage,\n onCallSettled,\n onExecutionStart,\n } = options;\n const registry = new Map(tools.map((tool) => [tool.name, tool]));\n\n // One consent covers the whole batch (and whatever scope the surface's\n // callback persists): writes are declined together while reads still run.\n let writesDeclined = false;\n const writeToolNames = calls\n .filter((call) => registry.get(call.tool_name)?.requiresWrite === true)\n .map((call) => call.tool_name);\n if (writeToolNames.length > 0 && confirmWrites) {\n const decision = await confirmWrites({ writeToolNames });\n if (decision === 'abandoned' || !isLive()) {\n // The run was stopped (or answered elsewhere) while consent was\n // pending: abort before any handler mutates the user's asset.\n return null;\n }\n writesDeclined = decision === 'declined';\n }\n\n onExecutionStart?.();\n\n const results: Record<string, ClientToolResultEnvelope> = {};\n for (const call of calls) {\n if (!isLive()) return null;\n const tool = registry.get(call.tool_name);\n let envelope: ClientToolResultEnvelope;\n let status: ClientToolCallSettledEvent['status'] = 'success';\n let errorType: string | undefined;\n if (!tool) {\n envelope = clientToolErrorEnvelope(missingToolMessage(call.tool_name));\n status = 'error';\n errorType = 'UnknownClientTool';\n } else if (tool.requiresWrite === true && writesDeclined) {\n envelope = clientToolErrorEnvelope(writeDeclinedMessage);\n status = 'declined';\n } else {\n try {\n envelope = clientToolSuccessEnvelope(\n jsonSafeClientToolResult(\n await tool.run(clientToolCallArgs(call), {\n toolCallId: call.interrupt_id,\n }),\n ),\n );\n } catch (error) {\n envelope = clientToolErrorEnvelope(error);\n status = 'error';\n errorType = error instanceof Error ? error.name : typeof error;\n }\n }\n results[call.interrupt_id] = envelope;\n onCallSettled?.({\n toolName: call.tool_name,\n status,\n ...(errorType !== undefined ? { errorType } : {}),\n });\n }\n\n // Abandoned mid-flight: resuming a dead request would error, and a\n // re-emitted one must be executable again.\n return isLive() ? results : null;\n}\n\nexport interface UseStatewireClientToolBridgeOptions {\n threadId: string;\n tools: readonly ClientToolBridgeTool[];\n /**\n * Ownership domain for parked-request claims. Pass a module-scoped tracker\n * to make ownership process-wide across every mount of the surface (two\n * mounted surfaces of one conversation execute each request exactly once),\n * or a per-mount tracker to scope ownership to one mounted bridge.\n */\n tracker: ClientToolRequestTracker;\n /** The surface's `useStatewireRuns().inputRequests` read. */\n inputRequests: readonly StatewireThread.InputRequestState[] | undefined;\n /** Dispatch the `run/input` resume on the surface's transport. The value is\n * already wire-safe (per-call JSON round-trip inside the batch engine). */\n /** Dispatch the answer and settle only once the Statewire host accepts or\n * rejects the command. The bridge deliberately keeps replaying while this\n * promise is pending: `run/input` removes the request optimistically before\n * the server has accepted the answer. */\n sendResume: (\n requestId: string,\n value: Record<string, unknown>,\n ) => PromiseLike<unknown>;\n /** Batch write gate, with the parked request's id so the surface can close\n * its consent UI the instant that request disappears. */\n confirmWrites?: (batch: {\n requestId: string;\n writeToolNames: readonly string[];\n }) => Promise<ClientToolWriteConsentDecision>;\n writeDeclinedMessage?: string;\n missingToolMessage?: (toolName: string) => string;\n onCallSettled?: (event: ClientToolCallSettledEvent) => void;\n /** Log prefix for resume-dispatch failures. */\n logLabel?: string;\n}\n\n/**\n * The claim lifecycle around {@link executeClientToolBatch}: claim the pending\n * request once per ownership domain, execute, resume — releasing the claim on\n * every path that ran no handler, so re-emitted requests recover.\n *\n * Unmount splits by whether side effects began:\n * - a claim whose batch has NOT started executing (claimed this tick, or\n * parked on write consent) is released, so another mount — or a remount —\n * can execute the re-emitted request instead of stranding the parked run;\n * - a claim whose batch HAS started stays held: handlers cannot be cancelled,\n * so releasing would let a sibling mount re-claim and re-execute a mutation\n * whose side effects already ran. The detached execution caches its result\n * and attempts one resume through its captured transport; a later mount can\n * replay the same result without repeating the side effect.\n */\nexport function useStatewireClientToolBridge(\n options: UseStatewireClientToolBridgeOptions,\n): void {\n const { threadId, tracker, inputRequests, logLabel = '[AthenaSDK]' } = options;\n const optionsRef = useRef(options);\n optionsRef.current = options;\n\n const toolNames = new Set(options.tools.map((tool) => tool.name));\n const pendingRequest = inputRequests?.find((request) => {\n if (!isPendingClientToolRequest(request)) return false;\n const payload = (request as StatewireThread.CustomInputRequest).payload;\n if (!isClientToolInterrupt(payload)) return false;\n // Every attached tab sees the same park. An observer without these tools\n // must not claim it and race the executing surface with an unknown-tool\n // error (for example, Olympus watching an Excel conversation).\n // A cached result already belongs to this tracker and can be replayed\n // without loading its handlers again.\n return (\n tracker.getCachedResume(clientToolClaimKey(threadId, request.id)) !== undefined ||\n payload.context.requests.every((call) => toolNames.has(call.tool_name))\n );\n });\n\n // Claims this bridge holds but has not resumed, split by whether their\n // batch has begun running handlers. The ref Set instances are stable (only\n // their contents mutate), so the unmount cleanup sees every claim.\n const ownedClaimsRef = useRef<Set<string>>(new Set());\n const startedClaimsRef = useRef<Set<string>>(new Set());\n const resumeOwnerRef = useRef(Symbol('client-tool-resume-owner'));\n const resumeClaimsRef = useRef<Set<string>>(new Set());\n const resumeTimersRef = useRef<\n Map<string, { requestId: string; timer: ReturnType<typeof setTimeout> }>\n >(new Map());\n const activeRef = useRef(true);\n const releaseClaim = useCallback(\n (claim: string) => {\n // Release the shared tracker entry only while THIS mount still owns it.\n // A detached batch can settle after the unmount cleanup already released\n // (and disowned) its claim; by then the tracker entry may belong to\n // another mount that claimed, executed, and resumed — releasing it here\n // would let a stale re-emission of the same request execute again.\n const owned = ownedClaimsRef.current.delete(claim);\n startedClaimsRef.current.delete(claim);\n if (owned) tracker.release(claim);\n },\n [tracker],\n );\n const stopResumeReplay = useCallback(\n (claim: string) => {\n const timerEntry = resumeTimersRef.current.get(claim);\n if (timerEntry) clearTimeout(timerEntry.timer);\n resumeTimersRef.current.delete(claim);\n if (resumeClaimsRef.current.delete(claim)) {\n tracker.releaseResumeReplay(claim, resumeOwnerRef.current);\n }\n },\n [tracker],\n );\n useEffect(() => {\n activeRef.current = true;\n const ownedClaims = ownedClaimsRef.current;\n const startedClaims = startedClaimsRef.current;\n return () => {\n activeRef.current = false;\n for (const claim of Array.from(resumeClaimsRef.current)) {\n stopResumeReplay(claim);\n }\n for (const claim of ownedClaims) {\n // Started batches keep their claim — their detached execution settles\n // it (resume, or release on a failed dispatch). Releasing here would\n // double-execute the side effects that already ran.\n if (startedClaims.has(claim)) continue;\n ownedClaims.delete(claim);\n tracker.release(claim);\n }\n };\n }, [stopResumeReplay, tracker]);\n\n // Handlers (and consent) are async, so the run can be stopped or the request\n // answered elsewhere while we await. The live ids let us abandon an\n // unfinished local execution when its request is no longer present. Once a\n // result is cached, only command settlement may stop its replay: Statewire\n // removes submitted requests optimistically before the host accepts them.\n const liveRequestIdsRef = useRef<ReadonlySet<string>>(new Set());\n liveRequestIdsRef.current = new Set((inputRequests ?? []).map((request) => request.id));\n\n const startResumeReplay = useCallback(\n (claim: string, requestId: string) => {\n if (!activeRef.current || resumeClaimsRef.current.has(claim)) return;\n if (!tracker.claimResumeReplay(claim, resumeOwnerRef.current)) return;\n resumeClaimsRef.current.add(claim);\n\n const dispatch = (): void => {\n resumeTimersRef.current.delete(claim);\n if (!activeRef.current) {\n stopResumeReplay(claim);\n return;\n }\n const attempt = tracker.takeResumeAttempt(claim, {\n owner: resumeOwnerRef.current,\n maxAttempts: MAX_CLIENT_TOOL_RESUME_ATTEMPTS,\n });\n if (!attempt) {\n stopResumeReplay(claim);\n return;\n }\n\n try {\n const settled = optionsRef.current.sendResume(requestId, attempt.value);\n // A fulfilled command result is the authoritative acknowledgement:\n // the host accepted and applied the answer. The local input request\n // disappearing is NOT an acknowledgement because the Statewire\n // reducer does that optimistically as soon as the command is\n // submitted.\n void Promise.resolve(settled).then(\n () => {\n // Keep the execution claim, but discard the cached result once\n // the host accepts it. A stale pending-request render can no\n // longer replay the command or re-execute the local mutation.\n tracker.settleResume(claim);\n stopResumeReplay(claim);\n },\n (error) => {\n // A rejected attempt may have raced another still-pending\n // replay. Keep the bounded loop alive; any accepted sibling\n // stops it, while the global attempt cap prevents storms.\n console.error(`${logLabel} failed to resume client tool results:`, error);\n },\n );\n } catch (error) {\n // Keep the cached result and retry it. Re-executing a mutating local\n // handler would be unsafe; replaying its id-stamped result is safe.\n console.error(`${logLabel} failed to resume client tool results:`, error);\n }\n\n if (\n attempt.attemptNumber >= MAX_CLIENT_TOOL_RESUME_ATTEMPTS ||\n !activeRef.current ||\n !resumeClaimsRef.current.has(claim)\n ) {\n stopResumeReplay(claim);\n return;\n }\n const timer = setTimeout(dispatch, clientToolResumeRetryDelayMs(attempt.attemptNumber));\n resumeTimersRef.current.set(claim, { requestId, timer });\n };\n\n dispatch();\n },\n [logLabel, stopResumeReplay, tracker],\n );\n\n const executeRequest = useCallback(\n async (request: StatewireThread.CustomInputRequest) => {\n const payload = request.payload;\n if (!isClientToolInterrupt(payload)) return;\n const claim = clientToolClaimKey(threadId, request.id);\n const {\n tools,\n confirmWrites,\n writeDeclinedMessage,\n missingToolMessage,\n onCallSettled,\n sendResume,\n } = optionsRef.current;\n\n const results = await executeClientToolBatch({\n calls: payload.context.requests,\n tools,\n // Live while the request still exists AND this surface may answer it:\n // mounted, or already past the point of no return (handlers started —\n // the detached execution must finish and settle, never re-execute).\n // After unmount the request-id snapshot freezes at its last observed\n // state, which keeps a started batch's own request visible to it.\n isLive: () =>\n liveRequestIdsRef.current.has(request.id) &&\n (activeRef.current || startedClaimsRef.current.has(claim)),\n onExecutionStart: () => {\n startedClaimsRef.current.add(claim);\n },\n ...(confirmWrites\n ? {\n confirmWrites: ({ writeToolNames }: { writeToolNames: readonly string[] }) =>\n confirmWrites({ requestId: request.id, writeToolNames }),\n }\n : {}),\n ...(writeDeclinedMessage !== undefined ? { writeDeclinedMessage } : {}),\n ...(missingToolMessage ? { missingToolMessage } : {}),\n ...(onCallSettled ? { onCallSettled } : {}),\n });\n if (results === null) {\n releaseClaim(claim);\n return;\n }\n\n const resumeValue = clientToolResultsResumeValue(results);\n tracker.cacheResume(claim, resumeValue);\n // Execution is complete, but the exactly-once claim remains. A pending\n // re-emission must replay this cached value, never run the handlers again.\n ownedClaimsRef.current.delete(claim);\n startedClaimsRef.current.delete(claim);\n\n if (!activeRef.current) {\n // The handler crossed an unmount. Preserve the pre-existing best-effort\n // settle through its captured transport, but leave the cached result\n // available for a later mount if this dispatch is lost.\n if (tracker.claimResumeReplay(claim, resumeOwnerRef.current)) {\n const attempt = tracker.takeResumeAttempt(claim, {\n owner: resumeOwnerRef.current,\n maxAttempts: MAX_CLIENT_TOOL_RESUME_ATTEMPTS,\n });\n if (attempt) {\n try {\n void Promise.resolve(sendResume(request.id, attempt.value)).then(\n () => tracker.settleResume(claim),\n (error) => {\n console.error(`${logLabel} failed to resume client tool results:`, error);\n },\n );\n } catch (error) {\n console.error(`${logLabel} failed to resume client tool results:`, error);\n }\n }\n // Ownership only fences this best-effort dispatch; a future mount\n // may claim and replay the cached value if it did not settle.\n tracker.releaseResumeReplay(claim, resumeOwnerRef.current);\n }\n return;\n }\n\n startResumeReplay(claim, request.id);\n },\n [logLabel, releaseClaim, startResumeReplay, threadId, tracker],\n );\n\n useEffect(() => {\n if (!pendingRequest) return;\n const claim = clientToolClaimKey(threadId, pendingRequest.id);\n if (!tracker.claim(claim)) {\n // The local handler already ran. This is the reconnect/remount path:\n // replay its cached answer instead of double-executing the side effect.\n if (tracker.getCachedResume(claim)) {\n startResumeReplay(claim, pendingRequest.id);\n }\n return;\n }\n ownedClaimsRef.current.add(claim);\n void executeRequest(pendingRequest);\n }, [executeRequest, pendingRequest, startResumeReplay, threadId, tracker]);\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"client-tool-bridge.js","sources":["../../../../agent-runtime/src/client-tool-bridge.ts"],"sourcesContent":["/**\n * The statewire client-tool bridge loop, shared by every surface that executes\n * frontend tool calls (Olympus, the React SDK, the Office add-in, mobile).\n *\n * Tools declared through `runConfig.custom.client_tools` have no backend\n * implementation: a call parks the run on an interrupt that arrives as a\n * statewire input request. A bridge with handlers for the whole batch claims\n * it (exactly-once per ownership domain), runs those handlers, and resumes\n * with per-call result envelopes. Other attached tabs leave it for the\n * executing surface; a thrown handler still returns an actionable error.\n *\n * Wire-adjacent correctness invariants this module owns, so they can never\n * drift apart per surface again:\n * - exactly-once claim per request, with un-claim on every path that did NOT\n * run any handler (abandoned before execution, consent dismissed, unmount\n * before the first handler) so a re-emitted request is executable instead\n * of stranding the run;\n * - once a batch has STARTED executing handlers, its claim is never released\n * by unmount: handlers cannot be cancelled, so releasing would let another\n * mount re-claim the re-emitted request and re-execute side effects that\n * already ran (double-executing a mutating tool). The completed wire-safe\n * result is cached and may be replayed across reconnects/remounts instead;\n * - liveness checks between awaits (the run can be stopped or answered\n * elsewhere while a handler runs) so a stale request is never resumed;\n * - per-call JSON wire-safety, so one unrepresentable handler result answers\n * that one call with an error instead of failing the whole batch.\n *\n * Like `interrupts.ts`, this is parameterized rather than self-wiring: the\n * host component reads `useStatewireRuns().inputRequests` and\n * `useStatewireSendCommand()` itself (both mockable per surface) and passes\n * them in, together with its ownership tracker and write-consent policy.\n */\n\nimport type { StatewireThread } from '@assistant-ui/react-statewire';\nimport { useCallback, useEffect, useRef } from 'react';\nimport {\n type ClientToolCallRequest,\n type ClientToolHandlerContext,\n type ClientToolRequestTracker,\n type ClientToolResultEnvelope,\n clientToolCallArgs,\n clientToolErrorEnvelope,\n clientToolResultsResumeValue,\n clientToolSuccessEnvelope,\n isClientToolInterrupt,\n} from './client-tools';\n\n/** A locally executable tool, normalized across the per-surface tool shapes. */\nexport interface ClientToolBridgeTool {\n /** Snake_case tool id the model calls (e.g. `update_sheet_range`). */\n name: string;\n /** Mutating tools gate on the surface's write-consent flow (`confirmWrites`). */\n requiresWrite?: boolean;\n run: (\n args: Record<string, unknown>,\n context: ClientToolHandlerContext,\n ) => Promise<unknown>;\n}\n\n/**\n * Batch write-consent outcome. `proceed` runs the write calls, `declined`\n * answers them with the declined message while reads still run, `abandoned`\n * releases the claim without resuming (the request died, or the consent UI was\n * dismissed by the run moving on). Grant persistence (a thread set, a stored\n * preference) is the surface's own policy, applied inside its callback.\n */\nexport type ClientToolWriteConsentDecision = 'proceed' | 'declined' | 'abandoned';\n\nexport interface ClientToolCallSettledEvent {\n toolName: string;\n status: 'success' | 'error' | 'declined';\n errorType?: string;\n}\n\n/** Default resume error for a write call the user declined. */\nexport const CLIENT_TOOL_WRITE_DECLINED_MESSAGE =\n 'The user declined this edit. Do not retry it; ask the user how to proceed.';\n\n/** Default resume error for a call naming a tool this surface does not have. */\nexport function defaultMissingClientToolMessage(toolName: string): string {\n return `This surface has no tool named '${toolName}'.`;\n}\n\n/** Statewire request ids are unique per run, but a thread scope keeps a\n * process-wide claim unambiguous when two conversations are open at once. */\nexport function clientToolClaimKey(threadId: string, requestId: string): string {\n return `${threadId}:${requestId}`;\n}\n\nconst CLIENT_TOOL_RESUME_RETRY_DELAYS_MS = [2_000, 5_000, 10_000, 15_000, 30_000];\nconst MAX_CLIENT_TOOL_RESUME_ATTEMPTS = 25;\n\nfunction clientToolResumeRetryDelayMs(attemptNumber: number): number {\n const index = Math.min(\n Math.max(attemptNumber - 1, 0),\n CLIENT_TOOL_RESUME_RETRY_DELAYS_MS.length - 1,\n );\n return CLIENT_TOOL_RESUME_RETRY_DELAYS_MS[index] ?? 30_000;\n}\n\n/**\n * A wire-safe copy of a handler's return value: handlers can return values\n * JSON cannot represent (DOM nodes, host objects, bigints), which must never\n * reach the transport. Throws for an unrepresentable value so the caller can\n * answer that one call with an error instead of stranding the whole batch.\n */\nexport function jsonSafeClientToolResult(value: unknown): unknown {\n const serialized = JSON.stringify(value);\n return serialized === undefined ? null : JSON.parse(serialized);\n}\n\n/** A parked, unanswered client-tool input request. */\nexport function isPendingClientToolRequest(\n request: StatewireThread.InputRequestState,\n): request is StatewireThread.CustomInputRequest {\n if (request.type !== 'interrupt' || 'response' in request) return false;\n return isClientToolInterrupt((request as { payload?: unknown }).payload);\n}\n\nexport interface ExecuteClientToolBatchOptions {\n calls: readonly ClientToolCallRequest[];\n tools: readonly ClientToolBridgeTool[];\n /** Whether the parked request still exists and this surface may answer it.\n * Checked before consent resolution, before every call, and before the\n * results are handed back for resume. */\n isLive: () => boolean;\n /** Batch write gate; absent means writes run without consent (surfaces that\n * only register read tools). Called once per batch, before any handler. */\n confirmWrites?: (batch: {\n writeToolNames: readonly string[];\n }) => Promise<ClientToolWriteConsentDecision>;\n writeDeclinedMessage?: string;\n missingToolMessage?: (toolName: string) => string;\n /** Per-call telemetry (the Office add-in's tool events). */\n onCallSettled?: (event: ClientToolCallSettledEvent) => void;\n /**\n * Called once, after the write-consent gate resolves and immediately before\n * the first handler runs — the point where side effects begin. There is no\n * await between this callback and the first handler invocation, so a caller\n * marking \"execution started\" here can never miss a handler that ran.\n */\n onExecutionStart?: () => void;\n /**\n * Handed to every handler of the batch (`context.signal`). The caller aborts\n * it when the parked request goes away mid-execution, so a handler that\n * takes visible time can stop; the batch's results are discarded either way\n * (`isLive` is what decides that).\n */\n signal?: AbortSignal;\n}\n\n/**\n * Execute one batched client-tool park: consent, per-call execution with\n * wire-safe envelopes, liveness aborts. Returns the per-interrupt result map\n * to resume with, or `null` when the batch was abandoned (the request died,\n * consent was abandoned, or the surface went away) — the caller must release\n * its claim and must NOT resume.\n */\nexport async function executeClientToolBatch(\n options: ExecuteClientToolBatchOptions,\n): Promise<Record<string, ClientToolResultEnvelope> | null> {\n const {\n calls,\n tools,\n isLive,\n confirmWrites,\n writeDeclinedMessage = CLIENT_TOOL_WRITE_DECLINED_MESSAGE,\n missingToolMessage = defaultMissingClientToolMessage,\n onCallSettled,\n onExecutionStart,\n signal,\n } = options;\n const registry = new Map(tools.map((tool) => [tool.name, tool]));\n\n // One consent covers the whole batch (and whatever scope the surface's\n // callback persists): writes are declined together while reads still run.\n let writesDeclined = false;\n const writeToolNames = calls\n .filter((call) => registry.get(call.tool_name)?.requiresWrite === true)\n .map((call) => call.tool_name);\n if (writeToolNames.length > 0 && confirmWrites) {\n const decision = await confirmWrites({ writeToolNames });\n if (decision === 'abandoned' || !isLive()) {\n // The run was stopped (or answered elsewhere) while consent was\n // pending: abort before any handler mutates the user's asset.\n return null;\n }\n writesDeclined = decision === 'declined';\n }\n\n onExecutionStart?.();\n\n const results: Record<string, ClientToolResultEnvelope> = {};\n for (const call of calls) {\n if (!isLive()) return null;\n const tool = registry.get(call.tool_name);\n let envelope: ClientToolResultEnvelope;\n let status: ClientToolCallSettledEvent['status'] = 'success';\n let errorType: string | undefined;\n if (!tool) {\n envelope = clientToolErrorEnvelope(missingToolMessage(call.tool_name));\n status = 'error';\n errorType = 'UnknownClientTool';\n } else if (tool.requiresWrite === true && writesDeclined) {\n envelope = clientToolErrorEnvelope(writeDeclinedMessage);\n status = 'declined';\n } else {\n try {\n envelope = clientToolSuccessEnvelope(\n jsonSafeClientToolResult(\n await tool.run(clientToolCallArgs(call), {\n toolCallId: call.interrupt_id,\n ...(signal ? { signal } : {}),\n }),\n ),\n );\n } catch (error) {\n envelope = clientToolErrorEnvelope(error);\n status = 'error';\n errorType = error instanceof Error ? error.name : typeof error;\n }\n }\n results[call.interrupt_id] = envelope;\n onCallSettled?.({\n toolName: call.tool_name,\n status,\n ...(errorType !== undefined ? { errorType } : {}),\n });\n }\n\n // Abandoned mid-flight: resuming a dead request would error, and a\n // re-emitted one must be executable again.\n return isLive() ? results : null;\n}\n\nexport interface UseStatewireClientToolBridgeOptions {\n threadId: string;\n tools: readonly ClientToolBridgeTool[];\n /**\n * Ownership domain for parked-request claims. Pass a module-scoped tracker\n * to make ownership process-wide across every mount of the surface (two\n * mounted surfaces of one conversation execute each request exactly once),\n * or a per-mount tracker to scope ownership to one mounted bridge.\n */\n tracker: ClientToolRequestTracker;\n /** The surface's `useStatewireRuns().inputRequests` read. */\n inputRequests: readonly StatewireThread.InputRequestState[] | undefined;\n /** Dispatch the `run/input` resume on the surface's transport. The value is\n * already wire-safe (per-call JSON round-trip inside the batch engine). */\n /** Dispatch the answer and settle only once the Statewire host accepts or\n * rejects the command. The bridge deliberately keeps replaying while this\n * promise is pending: `run/input` removes the request optimistically before\n * the server has accepted the answer. */\n sendResume: (\n requestId: string,\n value: Record<string, unknown>,\n ) => PromiseLike<unknown>;\n /** Batch write gate, with the parked request's id so the surface can close\n * its consent UI the instant that request disappears. */\n confirmWrites?: (batch: {\n requestId: string;\n writeToolNames: readonly string[];\n }) => Promise<ClientToolWriteConsentDecision>;\n writeDeclinedMessage?: string;\n missingToolMessage?: (toolName: string) => string;\n onCallSettled?: (event: ClientToolCallSettledEvent) => void;\n /** Log prefix for resume-dispatch failures. */\n logLabel?: string;\n}\n\n/**\n * The claim lifecycle around {@link executeClientToolBatch}: claim the pending\n * request once per ownership domain, execute, resume — releasing the claim on\n * every path that ran no handler, so re-emitted requests recover.\n *\n * Unmount splits by whether side effects began:\n * - a claim whose batch has NOT started executing (claimed this tick, or\n * parked on write consent) is released, so another mount — or a remount —\n * can execute the re-emitted request instead of stranding the parked run;\n * - a claim whose batch HAS started stays held: handlers cannot be cancelled,\n * so releasing would let a sibling mount re-claim and re-execute a mutation\n * whose side effects already ran. The detached execution caches its result\n * and attempts one resume through its captured transport; a later mount can\n * replay the same result without repeating the side effect.\n */\nexport function useStatewireClientToolBridge(\n options: UseStatewireClientToolBridgeOptions,\n): void {\n const { threadId, tracker, inputRequests, logLabel = '[AthenaSDK]' } = options;\n const optionsRef = useRef(options);\n optionsRef.current = options;\n\n const toolNames = new Set(options.tools.map((tool) => tool.name));\n const pendingRequest = inputRequests?.find((request) => {\n if (!isPendingClientToolRequest(request)) return false;\n const payload = (request as StatewireThread.CustomInputRequest).payload;\n if (!isClientToolInterrupt(payload)) return false;\n // Every attached tab sees the same park. An observer without these tools\n // must not claim it and race the executing surface with an unknown-tool\n // error (for example, Olympus watching an Excel conversation).\n // A cached result already belongs to this tracker and can be replayed\n // without loading its handlers again.\n return (\n tracker.getCachedResume(clientToolClaimKey(threadId, request.id)) !== undefined ||\n payload.context.requests.every((call) => toolNames.has(call.tool_name))\n );\n });\n\n // Claims this bridge holds but has not resumed, split by whether their\n // batch has begun running handlers. The ref Set instances are stable (only\n // their contents mutate), so the unmount cleanup sees every claim.\n const ownedClaimsRef = useRef<Set<string>>(new Set());\n const startedClaimsRef = useRef<Set<string>>(new Set());\n const resumeOwnerRef = useRef(Symbol('client-tool-resume-owner'));\n const resumeClaimsRef = useRef<Set<string>>(new Set());\n const resumeTimersRef = useRef<\n Map<string, { requestId: string; timer: ReturnType<typeof setTimeout> }>\n >(new Map());\n const activeRef = useRef(true);\n const releaseClaim = useCallback(\n (claim: string) => {\n // Release the shared tracker entry only while THIS mount still owns it.\n // A detached batch can settle after the unmount cleanup already released\n // (and disowned) its claim; by then the tracker entry may belong to\n // another mount that claimed, executed, and resumed — releasing it here\n // would let a stale re-emission of the same request execute again.\n const owned = ownedClaimsRef.current.delete(claim);\n startedClaimsRef.current.delete(claim);\n if (owned) tracker.release(claim);\n },\n [tracker],\n );\n const stopResumeReplay = useCallback(\n (claim: string) => {\n const timerEntry = resumeTimersRef.current.get(claim);\n if (timerEntry) clearTimeout(timerEntry.timer);\n resumeTimersRef.current.delete(claim);\n if (resumeClaimsRef.current.delete(claim)) {\n tracker.releaseResumeReplay(claim, resumeOwnerRef.current);\n }\n },\n [tracker],\n );\n useEffect(() => {\n activeRef.current = true;\n const ownedClaims = ownedClaimsRef.current;\n const startedClaims = startedClaimsRef.current;\n return () => {\n activeRef.current = false;\n for (const claim of Array.from(resumeClaimsRef.current)) {\n stopResumeReplay(claim);\n }\n for (const claim of ownedClaims) {\n // Started batches keep their claim — their detached execution settles\n // it (resume, or release on a failed dispatch). Releasing here would\n // double-execute the side effects that already ran.\n if (startedClaims.has(claim)) continue;\n ownedClaims.delete(claim);\n tracker.release(claim);\n }\n };\n }, [stopResumeReplay, tracker]);\n\n // Handlers (and consent) are async, so the run can be stopped or the request\n // answered elsewhere while we await. The live ids let us abandon an\n // unfinished local execution when its request is no longer present. Once a\n // result is cached, only command settlement may stop its replay: Statewire\n // removes submitted requests optimistically before the host accepts them.\n const liveRequestIdsRef = useRef<ReadonlySet<string>>(new Set());\n liveRequestIdsRef.current = new Set((inputRequests ?? []).map((request) => request.id));\n\n // One abort signal per executing request, handed to its handlers. It fires\n // when the request leaves the mounted surface's view (stop, an answer from\n // elsewhere, another conversation opened): the results would be discarded,\n // so a handler that is still showing the person something can stop. It does\n // not fire on unmount: a started batch finishes and settles (see above).\n const abortControllersRef = useRef<Map<string, AbortController>>(new Map());\n useEffect(() => {\n const liveRequestIds = new Set((inputRequests ?? []).map((request) => request.id));\n for (const [requestId, controller] of abortControllersRef.current) {\n if (liveRequestIds.has(requestId)) continue;\n abortControllersRef.current.delete(requestId);\n controller.abort();\n }\n }, [inputRequests]);\n\n const startResumeReplay = useCallback(\n (claim: string, requestId: string) => {\n if (!activeRef.current || resumeClaimsRef.current.has(claim)) return;\n if (!tracker.claimResumeReplay(claim, resumeOwnerRef.current)) return;\n resumeClaimsRef.current.add(claim);\n\n const dispatch = (): void => {\n resumeTimersRef.current.delete(claim);\n if (!activeRef.current) {\n stopResumeReplay(claim);\n return;\n }\n const attempt = tracker.takeResumeAttempt(claim, {\n owner: resumeOwnerRef.current,\n maxAttempts: MAX_CLIENT_TOOL_RESUME_ATTEMPTS,\n });\n if (!attempt) {\n stopResumeReplay(claim);\n return;\n }\n\n try {\n const settled = optionsRef.current.sendResume(requestId, attempt.value);\n // A fulfilled command result is the authoritative acknowledgement:\n // the host accepted and applied the answer. The local input request\n // disappearing is NOT an acknowledgement because the Statewire\n // reducer does that optimistically as soon as the command is\n // submitted.\n void Promise.resolve(settled).then(\n () => {\n // Keep the execution claim, but discard the cached result once\n // the host accepts it. A stale pending-request render can no\n // longer replay the command or re-execute the local mutation.\n tracker.settleResume(claim);\n stopResumeReplay(claim);\n },\n (error) => {\n // A rejected attempt may have raced another still-pending\n // replay. Keep the bounded loop alive; any accepted sibling\n // stops it, while the global attempt cap prevents storms.\n console.error(`${logLabel} failed to resume client tool results:`, error);\n },\n );\n } catch (error) {\n // Keep the cached result and retry it. Re-executing a mutating local\n // handler would be unsafe; replaying its id-stamped result is safe.\n console.error(`${logLabel} failed to resume client tool results:`, error);\n }\n\n if (\n attempt.attemptNumber >= MAX_CLIENT_TOOL_RESUME_ATTEMPTS ||\n !activeRef.current ||\n !resumeClaimsRef.current.has(claim)\n ) {\n stopResumeReplay(claim);\n return;\n }\n const timer = setTimeout(dispatch, clientToolResumeRetryDelayMs(attempt.attemptNumber));\n resumeTimersRef.current.set(claim, { requestId, timer });\n };\n\n dispatch();\n },\n [logLabel, stopResumeReplay, tracker],\n );\n\n const executeRequest = useCallback(\n async (request: StatewireThread.CustomInputRequest) => {\n const payload = request.payload;\n if (!isClientToolInterrupt(payload)) return;\n const claim = clientToolClaimKey(threadId, request.id);\n const {\n tools,\n confirmWrites,\n writeDeclinedMessage,\n missingToolMessage,\n onCallSettled,\n sendResume,\n } = optionsRef.current;\n\n const abortController = new AbortController();\n abortControllersRef.current.set(request.id, abortController);\n const results = await executeClientToolBatch({\n calls: payload.context.requests,\n tools,\n signal: abortController.signal,\n // Live while the request still exists AND this surface may answer it:\n // mounted, or already past the point of no return (handlers started —\n // the detached execution must finish and settle, never re-execute).\n // After unmount the request-id snapshot freezes at its last observed\n // state, which keeps a started batch's own request visible to it.\n isLive: () =>\n liveRequestIdsRef.current.has(request.id) &&\n (activeRef.current || startedClaimsRef.current.has(claim)),\n onExecutionStart: () => {\n startedClaimsRef.current.add(claim);\n },\n ...(confirmWrites\n ? {\n confirmWrites: ({ writeToolNames }: { writeToolNames: readonly string[] }) =>\n confirmWrites({ requestId: request.id, writeToolNames }),\n }\n : {}),\n ...(writeDeclinedMessage !== undefined ? { writeDeclinedMessage } : {}),\n ...(missingToolMessage ? { missingToolMessage } : {}),\n ...(onCallSettled ? { onCallSettled } : {}),\n }).finally(() => {\n if (abortControllersRef.current.get(request.id) === abortController) {\n abortControllersRef.current.delete(request.id);\n }\n });\n if (results === null) {\n releaseClaim(claim);\n return;\n }\n\n const resumeValue = clientToolResultsResumeValue(results);\n tracker.cacheResume(claim, resumeValue);\n // Execution is complete, but the exactly-once claim remains. A pending\n // re-emission must replay this cached value, never run the handlers again.\n ownedClaimsRef.current.delete(claim);\n startedClaimsRef.current.delete(claim);\n\n if (!activeRef.current) {\n // The handler crossed an unmount. Preserve the pre-existing best-effort\n // settle through its captured transport, but leave the cached result\n // available for a later mount if this dispatch is lost.\n if (tracker.claimResumeReplay(claim, resumeOwnerRef.current)) {\n const attempt = tracker.takeResumeAttempt(claim, {\n owner: resumeOwnerRef.current,\n maxAttempts: MAX_CLIENT_TOOL_RESUME_ATTEMPTS,\n });\n if (attempt) {\n try {\n void Promise.resolve(sendResume(request.id, attempt.value)).then(\n () => tracker.settleResume(claim),\n (error) => {\n console.error(`${logLabel} failed to resume client tool results:`, error);\n },\n );\n } catch (error) {\n console.error(`${logLabel} failed to resume client tool results:`, error);\n }\n }\n // Ownership only fences this best-effort dispatch; a future mount\n // may claim and replay the cached value if it did not settle.\n tracker.releaseResumeReplay(claim, resumeOwnerRef.current);\n }\n return;\n }\n\n startResumeReplay(claim, request.id);\n },\n [logLabel, releaseClaim, startResumeReplay, threadId, tracker],\n );\n\n useEffect(() => {\n if (!pendingRequest) return;\n const claim = clientToolClaimKey(threadId, pendingRequest.id);\n if (!tracker.claim(claim)) {\n // The local handler already ran. This is the reconnect/remount path:\n // replay its cached answer instead of double-executing the side effect.\n if (tracker.getCachedResume(claim)) {\n startResumeReplay(claim, pendingRequest.id);\n }\n return;\n }\n ownedClaimsRef.current.add(claim);\n void executeRequest(pendingRequest);\n }, [executeRequest, pendingRequest, startResumeReplay, threadId, tracker]);\n}\n"],"names":[],"mappings":"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"}