@athenaintel/react 0.53.1 → 0.54.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/README.md +18 -1
  2. package/dist/agent-runtime/src/call-recap.cjs +2 -3
  3. package/dist/agent-runtime/src/call-recap.cjs.map +1 -1
  4. package/dist/agent-runtime/src/call-recap.d.ts +1 -2
  5. package/dist/agent-runtime/src/call-recap.js +1 -2
  6. package/dist/agent-runtime/src/call-recap.js.map +1 -1
  7. package/dist/agent-runtime/src/deep-agent-transport-recipe.cjs +7 -3
  8. package/dist/agent-runtime/src/deep-agent-transport-recipe.cjs.map +1 -1
  9. package/dist/agent-runtime/src/deep-agent-transport-recipe.d.ts +10 -2
  10. package/dist/agent-runtime/src/deep-agent-transport-recipe.js +8 -4
  11. package/dist/agent-runtime/src/deep-agent-transport-recipe.js.map +1 -1
  12. package/dist/agent-runtime/src/dictation-session.d.ts +158 -0
  13. package/dist/agent-runtime/src/edit-command.cjs +6 -0
  14. package/dist/agent-runtime/src/edit-command.cjs.map +1 -1
  15. package/dist/agent-runtime/src/edit-command.d.ts +8 -0
  16. package/dist/agent-runtime/src/edit-command.js +6 -0
  17. package/dist/agent-runtime/src/edit-command.js.map +1 -1
  18. package/dist/agent-runtime/src/generated/runconfig-contract.cjs +6 -0
  19. package/dist/agent-runtime/src/generated/runconfig-contract.cjs.map +1 -1
  20. package/dist/agent-runtime/src/generated/runconfig-contract.d.ts +13 -0
  21. package/dist/agent-runtime/src/generated/runconfig-contract.js +6 -0
  22. package/dist/agent-runtime/src/generated/runconfig-contract.js.map +1 -1
  23. package/dist/agent-runtime/src/index.d.ts +6 -6
  24. package/dist/agent-runtime/src/message-window.cjs +111 -7
  25. package/dist/agent-runtime/src/message-window.cjs.map +1 -1
  26. package/dist/agent-runtime/src/message-window.d.ts +66 -6
  27. package/dist/agent-runtime/src/message-window.js +112 -8
  28. package/dist/agent-runtime/src/message-window.js.map +1 -1
  29. package/dist/agent-runtime/src/offline-status.cjs.map +1 -1
  30. package/dist/agent-runtime/src/offline-status.d.ts +33 -0
  31. package/dist/agent-runtime/src/offline-status.js.map +1 -1
  32. package/dist/agent-runtime/src/runtime.d.ts +4 -3
  33. package/dist/agent-runtime/src/subagent-transcripts.cjs +23 -1
  34. package/dist/agent-runtime/src/subagent-transcripts.cjs.map +1 -1
  35. package/dist/agent-runtime/src/subagent-transcripts.js +25 -3
  36. package/dist/agent-runtime/src/subagent-transcripts.js.map +1 -1
  37. package/dist/agent-runtime/src/subagents.cjs +112 -0
  38. package/dist/agent-runtime/src/subagents.cjs.map +1 -1
  39. package/dist/agent-runtime/src/subagents.d.ts +103 -0
  40. package/dist/agent-runtime/src/subagents.js +113 -1
  41. package/dist/agent-runtime/src/subagents.js.map +1 -1
  42. package/dist/agent-runtime/src/thread-handoff-protocol.d.ts +37 -9
  43. package/dist/agent-runtime/src/thread-handoff.d.ts +1 -1
  44. package/dist/agent-runtime/src/tool-metadata.cjs +8 -0
  45. package/dist/agent-runtime/src/tool-metadata.cjs.map +1 -1
  46. package/dist/agent-runtime/src/tool-metadata.js +8 -0
  47. package/dist/agent-runtime/src/tool-metadata.js.map +1 -1
  48. package/dist/agent-runtime/src/use-deep-agent-thread.cjs +6 -0
  49. package/dist/agent-runtime/src/use-deep-agent-thread.cjs.map +1 -1
  50. package/dist/agent-runtime/src/use-deep-agent-thread.js +7 -1
  51. package/dist/agent-runtime/src/use-deep-agent-thread.js.map +1 -1
  52. package/dist/agent-runtime/src/use-deep-agent-window-sync.cjs +39 -10
  53. package/dist/agent-runtime/src/use-deep-agent-window-sync.cjs.map +1 -1
  54. package/dist/agent-runtime/src/use-deep-agent-window-sync.d.ts +14 -4
  55. package/dist/agent-runtime/src/use-deep-agent-window-sync.js +41 -12
  56. package/dist/agent-runtime/src/use-deep-agent-window-sync.js.map +1 -1
  57. package/dist/agent-runtime/src/use-thread-handoff-flow.d.ts +5 -9
  58. package/dist/agent-runtime/src/voice-presentation.cjs +34 -0
  59. package/dist/agent-runtime/src/voice-presentation.cjs.map +1 -1
  60. package/dist/agent-runtime/src/voice-presentation.d.ts +35 -0
  61. package/dist/agent-runtime/src/voice-presentation.js +34 -0
  62. package/dist/agent-runtime/src/voice-presentation.js.map +1 -1
  63. package/dist/assets/AthenaAssetEmbed.cjs +4 -2
  64. package/dist/assets/AthenaAssetEmbed.cjs.map +1 -1
  65. package/dist/assets/AthenaAssetEmbed.d.ts +17 -8
  66. package/dist/assets/AthenaAssetEmbed.js +4 -2
  67. package/dist/assets/AthenaAssetEmbed.js.map +1 -1
  68. package/dist/assets/use-asset-embed.cjs.map +1 -1
  69. package/dist/assets/use-asset-embed.d.ts +7 -5
  70. package/dist/assets/use-asset-embed.js.map +1 -1
  71. package/dist/auth/account-identity.cjs +69 -0
  72. package/dist/auth/account-identity.cjs.map +1 -0
  73. package/dist/auth/account-identity.d.ts +57 -0
  74. package/dist/auth/account-identity.js +69 -0
  75. package/dist/auth/account-identity.js.map +1 -0
  76. package/dist/chat/AthenaChat.cjs +1 -1
  77. package/dist/chat/AthenaChat.cjs.map +1 -1
  78. package/dist/chat/AthenaChat.js +1 -1
  79. package/dist/chat/AthenaChat.js.map +1 -1
  80. package/dist/chat/super-grouping/toolkit-map.cjs +1 -0
  81. package/dist/chat/super-grouping/toolkit-map.cjs.map +1 -1
  82. package/dist/chat/super-grouping/toolkit-map.js +1 -0
  83. package/dist/chat/super-grouping/toolkit-map.js.map +1 -1
  84. package/dist/chat/use-statewire-approval-request.cjs +5 -5
  85. package/dist/chat/use-statewire-approval-request.cjs.map +1 -1
  86. package/dist/chat/use-statewire-approval-request.d.ts +7 -0
  87. package/dist/chat/use-statewire-approval-request.js +5 -5
  88. package/dist/chat/use-statewire-approval-request.js.map +1 -1
  89. package/dist/chat-ui/mentions/use-account-workspace-id.cjs +18 -58
  90. package/dist/chat-ui/mentions/use-account-workspace-id.cjs.map +1 -1
  91. package/dist/chat-ui/mentions/use-account-workspace-id.js +18 -58
  92. package/dist/chat-ui/mentions/use-account-workspace-id.js.map +1 -1
  93. package/dist/chat-ui/messages/voice-delegation-card.cjs +93 -25
  94. package/dist/chat-ui/messages/voice-delegation-card.cjs.map +1 -1
  95. package/dist/chat-ui/messages/voice-delegation-card.d.ts +16 -7
  96. package/dist/chat-ui/messages/voice-delegation-card.js +95 -27
  97. package/dist/chat-ui/messages/voice-delegation-card.js.map +1 -1
  98. package/dist/chat-ui/thread/handoff-cursor.cjs +2 -2
  99. package/dist/chat-ui/thread/handoff-cursor.cjs.map +1 -1
  100. package/dist/chat-ui/thread/handoff-cursor.js +3 -3
  101. package/dist/chat-ui/thread/handoff-cursor.js.map +1 -1
  102. package/dist/collab/client.cjs +94 -6
  103. package/dist/collab/client.cjs.map +1 -1
  104. package/dist/collab/client.d.ts +27 -0
  105. package/dist/collab/client.js +94 -6
  106. package/dist/collab/client.js.map +1 -1
  107. package/dist/collab/index.d.ts +1 -1
  108. package/dist/keryx-http/src/initial-sync.cjs +121 -0
  109. package/dist/keryx-http/src/initial-sync.cjs.map +1 -0
  110. package/dist/keryx-http/src/initial-sync.d.ts +77 -0
  111. package/dist/keryx-http/src/initial-sync.js +104 -0
  112. package/dist/keryx-http/src/initial-sync.js.map +1 -0
  113. package/dist/keryx-http/src/unconfirmed-update-attributions.cjs +8 -0
  114. package/dist/keryx-http/src/unconfirmed-update-attributions.cjs.map +1 -1
  115. package/dist/keryx-http/src/unconfirmed-update-attributions.d.ts +6 -0
  116. package/dist/keryx-http/src/unconfirmed-update-attributions.js +8 -0
  117. package/dist/keryx-http/src/unconfirmed-update-attributions.js.map +1 -1
  118. package/dist/react/package.json.cjs +1 -1
  119. package/dist/react/package.json.js +1 -1
  120. package/dist/styles.css +1 -1
  121. package/dist/threads/api.cjs +1 -2
  122. package/dist/threads/api.cjs.map +1 -1
  123. package/dist/threads/api.d.ts +3 -2
  124. package/dist/threads/api.js +1 -2
  125. package/dist/threads/api.js.map +1 -1
  126. package/dist/threads/statewire-thread-list.cjs +5 -7
  127. package/dist/threads/statewire-thread-list.cjs.map +1 -1
  128. package/dist/threads/statewire-thread-list.js +7 -9
  129. package/dist/threads/statewire-thread-list.js.map +1 -1
  130. package/dist/tools/subagent-task-tool-ui.cjs +15 -5
  131. package/dist/tools/subagent-task-tool-ui.cjs.map +1 -1
  132. package/dist/tools/subagent-task-tool-ui.d.ts +5 -4
  133. package/dist/tools/subagent-task-tool-ui.js +16 -6
  134. package/dist/tools/subagent-task-tool-ui.js.map +1 -1
  135. package/package.json +1 -1
package/README.md CHANGED
@@ -951,6 +951,21 @@ Both use the same bearer / API key as session creation. The full recipe
951
951
  template skill `voice-session-guide.md`. `createAthenaVoiceSession` is also
952
952
  exported if you hold the bridge state yourself.
953
953
 
954
+ ## What's new in 0.54.0
955
+
956
+ **Older history says when it can't go deeper.** Agora holds one connection's loaded history in a document with a size limit (about a thousand messages, ~30% more since agora #50871). The "Load earlier messages" row used to keep a button that the host refused on every press, three times per approach to the top. The first refusal now ends paging for that conversation, and the row reads "Earlier messages can't be loaded here". An edit that shrinks the loaded view opens paging again. `useDeepAgentWindowSync` reports it as `canLoadOlder: false` for hosts that draw their own edge.
957
+
958
+ **A user bubble stays inside its message column.** A message holding a line that can't wrap, such as a code block or a long URL, used to widen `AthenaChat`'s right-aligned user bubble past its column and off the left edge of the page. The bubble is capped at its column now, so code blocks and tables scroll inside it and long words wrap. The SDK stylesheet applies the same cap to any `.aui-user-message-content` in its components layer, so hosts that render their own bubble get it, and their own width utilities still win.
959
+
960
+ **A reconnect keeps the history you paged in.** A re-attach declares the view it holds (agora reads the `x-athena-deep-agent-interests` header), so a reconnect onto another agora pod no longer drops older pages back to the newest window.
961
+
962
+ Also in this release:
963
+ - `AthenaChat` and the session list read `/me` once per credential.
964
+ - A subagent lane parked on an approval reads "Waiting for approval", with what it waits on.
965
+ - A request handed off from a voice call reads once.
966
+ - A handover's source waits the ticket's whole life.
967
+ - Word rooms in `@athenaintel/keryx-http` open HTTP-first where Keryx says it answers that way.
968
+
954
969
  ## What's new in 0.53.1
955
970
 
956
971
  **`npm install @athenaintel/react` works.** Since the April move to bun workspaces, the package carried the monorepo's bun-only `preinstall` check. npm, pnpm and yarn run an installed dependency's `preinstall`, so every install outside bun failed with "This monorepo requires bun", 0.53.0's included. The check now steps aside when it runs inside `node_modules`, as `@athenaintel/agent-runtime`'s does.
@@ -2073,7 +2088,9 @@ host that opens a cited place in Athena on the web writes it into the Spaces lin
2073
2088
 
2074
2089
  Use `AthenaAssetEmbed` to render a native Athena asset without chat, thread history, tabs, or the SDK split-pane layout. It defaults to the editable, full native asset experience (`readOnly={false}` and `displayMode="full"`). The embed token and the current user must still have edit permission; otherwise Athena renders the asset read-only.
2075
2090
 
2076
- Editing a Word document, an Athena document, a spreadsheet, a user interface or a deck through an embed needs a viewer who is signed in to Athena: give the provider the viewer's `token`, which the component hands the frame. A viewer it hands no token (a provider with only an `apiKey`, or none) is not signed in, and the embed is read-only for them, as a `readOnly` link is, whatever `readOnly={false}` asked for. Athena enforces this on its servers, for links minted before it too. Other asset types are unchanged.
2091
+ Editing, through an embed, any asset type that keeps comment threads (a Word document, an Athena document, a spreadsheet, a deck, a user interface, a notebook, a PDF, a text or data file, ...) needs a viewer signed in to Athena with their own access to the asset: give the provider the viewer's `token`, which the component hands the frame. A viewer it hands no token (a provider with only an `apiKey`, or none), and a signed-in person without access to the asset, see the embed read-only, as a `readOnly` link shows it, whatever `readOnly={false}` asked for. Athena enforces this on its servers, for links minted before it too. Other asset types are unchanged.
2092
+
2093
+ A host that signs its person in with a key it must never hand a frame (Athena's desktop app) passes `token` to `AthenaAssetEmbed` itself: the short-lived viewer token Athena mints for the link with that key (`POST /api/embed/{token}/viewer-token`; it names the person on that one link, read-only, for 15 minutes at most, so mint another before it expires, and Athena mints none for a notebook's link, which runs code). The component sends it only to the frame's origin, in place of the provider's `token`.
2077
2094
 
2078
2095
  With `displayMode="minimal"`, which runs on the link rather than an Athena sign-in, pass `allowQuickEdit` (and `readOnly={false}`) to let the viewer Quick Edit a spreadsheet, a Word document or a deck with ⌘J. Like editing, it is offered only to a signed-in viewer. Its runs are billed to the asset's workspace, with an hourly cap per link.
2079
2096
 
@@ -4,7 +4,6 @@ const core = require("@assistant-ui/core");
4
4
  const statewireState = require("./statewire-state.cjs");
5
5
  const toolResult = require("./tool-result.cjs");
6
6
  const voicePresentation = require("./voice-presentation.cjs");
7
- const VOICE_TYPED_ID_PREFIX = "voicetext_";
8
7
  const CALL_SOURCES = /* @__PURE__ */ new Set([
9
8
  voicePresentation.VOICE_SOURCE_SPOKEN,
10
9
  voicePresentation.VOICE_SOURCE_TYPED,
@@ -14,7 +13,7 @@ function isCallUserMessage(message) {
14
13
  var _a, _b;
15
14
  if (message.role !== "user") return false;
16
15
  if (CALL_SOURCES.has((_b = (_a = message.metadata) == null ? void 0 : _a.custom) == null ? void 0 : _b.source)) return true;
17
- return message.id.startsWith(voicePresentation.VOICE_DELEGATION_ID_PREFIX) || message.id.startsWith(VOICE_TYPED_ID_PREFIX);
16
+ return message.id.startsWith(voicePresentation.VOICE_DELEGATION_ID_PREFIX) || message.id.startsWith(voicePresentation.VOICE_TYPED_ID_PREFIX);
18
17
  }
19
18
  function mayHaveLiveCall(state) {
20
19
  if (!statewireState.isDeepAgentStateHydrated(state)) return true;
@@ -309,9 +308,9 @@ function callRecapItems(recap, awaitingApprovalToolCallIds = EMPTY_IDS) {
309
308
  }
310
309
  const CALL_RECAP_TITLE = "From the call";
311
310
  const CALL_RECAP_APPROVAL_LABEL = "Waiting for your approval";
311
+ exports.VOICE_TYPED_ID_PREFIX = voicePresentation.VOICE_TYPED_ID_PREFIX;
312
312
  exports.CALL_RECAP_APPROVAL_LABEL = CALL_RECAP_APPROVAL_LABEL;
313
313
  exports.CALL_RECAP_TITLE = CALL_RECAP_TITLE;
314
- exports.VOICE_TYPED_ID_PREFIX = VOICE_TYPED_ID_PREFIX;
315
314
  exports.callRecapAfter = callRecapAfter;
316
315
  exports.callRecapItems = callRecapItems;
317
316
  exports.isCallUserMessage = isCallUserMessage;
@@ -1 +1 @@
1
- {"version":3,"file":"call-recap.cjs","sources":["../../../../agent-runtime/src/call-recap.ts"],"sourcesContent":["/**\n * \"From the call\": what a voice call produced or mentioned, gathered where the\n * call's stretch of the transcript ends, so a person who talked to the agent\n * and later reads the conversation as text (on the same app or another) finds\n * the call's assets, links and open approvals in one place.\n *\n * Everything here is read from the transcript every app already renders, and\n * nothing is invented: an asset item is an asset link or `@`-mention in a\n * message, or a tool result that names the asset it made or read (`asset_id`\n * with a `title`); a link item is a web link in a message; an approval item is\n * a tool call of the call's that has no result yet, shown only while the\n * host's live approval request still names it (the presenter's\n * {@link callRecapItems}).\n *\n * **Where a call is in the transcript.** The host marks every turn a call\n * commits (`metadata.custom.source`, see `voice-presentation.ts`): spoken turns\n * (`voice`), messages typed into the call (`text`, id `voicetext_…`) and the\n * tasks the live model handed the thread (`voice_delegation`, id\n * `voicetask_…`). A typed message sent outside a call carries none of these. So\n * a call's stretch starts at its first marked turn and runs, through the\n * assistant work in between (the delegated runs), up to the next user message\n * that is not a call turn, or the end of the transcript. A spoken assistant\n * turn the converter joined onto the end of an earlier run's message starts\n * the stretch at that part, so the earlier run's artifacts stay out. A\n * delegated run's `send_voice_update` call (a tool only voice-delegated runs\n * get) marks the call too, so a windowed transcript whose loaded tail starts\n * inside a long delegated run still finds it.\n *\n * Two calls with no typed message between them read as one stretch: the\n * transcript does not say where one ended and the next began (the host keeps\n * no per-call marker), so their card collects both. A stretch that ends the\n * transcript may still be live; the presenter shows it once the host reports\n * no voice session (`state.voice` is null), see {@link mayHaveLiveCall}.\n *\n * **Cost.** Every message row asks {@link callRecapAfter} on every transcript\n * update. A row that does not end a stretch answers after looking at the next\n * message; a row that does walks back over its own stretch only. What a message\n * and each of its parts contribute is memoized per object (statewire replaces a\n * changed message and assistant-ui keeps every unchanged part), and an equal\n * recap is the same frozen object, so a row selecting it re-renders only when\n * the card's contents change. The transcript is never re-scanned per delta.\n */\n\nimport { getExternalStoreMessages, type ThreadMessage } from '@assistant-ui/core';\nimport { isDeepAgentStateHydrated, readDeepAgentStreamValues } from './statewire-state';\nimport {\n classifyToolResult,\n resultRecord,\n stringField,\n type ToolCallStatusLike,\n} from './tool-result';\nimport {\n isSpokenTurnTextPart,\n VOICE_DELEGATION_ID_PREFIX,\n VOICE_UPDATE_TOOL_NAME,\n VOICE_SOURCE_DELEGATION,\n VOICE_SOURCE_SPOKEN,\n VOICE_SOURCE_TYPED,\n} from './voice-presentation';\n\n/** Id prefix of a message typed into a live call (`voice/user-text`). */\nexport const VOICE_TYPED_ID_PREFIX = 'voicetext_';\n\n/** An asset the call created, read or linked. */\nexport interface CallRecapAsset {\n readonly kind: 'asset';\n /** Unique within a recap. */\n readonly key: string;\n readonly assetId: string;\n /** What the transcript calls it (the link's label, or the tool result's title); may be empty. */\n readonly title: string;\n /** The asset type the link or tool named (`document`, `spreadsheet`, …), when it named one. */\n readonly assetType: string | null;\n /** The link the transcript carried for it, when it came from a link. */\n readonly href: string | null;\n}\n\n/** A web page the call linked. */\nexport interface CallRecapLink {\n readonly kind: 'link';\n readonly key: string;\n readonly url: string;\n /** The link's own label, else the page's host and path. */\n readonly label: string;\n}\n\n/** A tool call of the call's that is waiting for the person's approval. */\nexport interface CallRecapApproval {\n readonly kind: 'approval';\n readonly key: string;\n readonly toolCallId: string;\n readonly toolName: string;\n}\n\nexport type CallRecapItem = CallRecapAsset | CallRecapLink | CallRecapApproval;\n\n/** A tool call in the stretch with no result yet: an approval while the host's request names it. */\nexport interface CallRecapOpenToolCall {\n readonly toolCallId: string;\n readonly toolName: string;\n}\n\nexport interface CallRecap {\n /** The message the stretch ends at; the card renders right after it. */\n readonly endMessageId: string;\n /** The stretch ends the transcript, so the call may still be live. */\n readonly trailing: boolean;\n /** Assets, in the order the stretch first mentioned them. */\n readonly assets: readonly CallRecapAsset[];\n /** Web links, in the order the stretch first mentioned them. */\n readonly links: readonly CallRecapLink[];\n /** Result-less tool calls, in transcript order. */\n readonly openToolCalls: readonly CallRecapOpenToolCall[];\n}\n\n/** The fields of an assistant-ui thread message this reads (structural, so any runtime's message fits). */\nexport interface CallRecapMessage {\n readonly id: string;\n readonly role: string;\n readonly content: readonly CallRecapPart[];\n readonly metadata?: { readonly custom?: Record<string, unknown> } | undefined;\n}\n\n/** The fields of a message part this reads. */\nexport interface CallRecapPart {\n readonly type: string;\n readonly text?: string;\n readonly toolCallId?: string;\n readonly toolName?: string;\n readonly result?: unknown;\n readonly isError?: boolean;\n /** An interim result a running tool published (`markPreliminaryToolResults`); the real one has not arrived. */\n readonly isPreliminary?: boolean;\n readonly status?: unknown;\n}\n\nconst CALL_SOURCES: ReadonlySet<unknown> = new Set([\n VOICE_SOURCE_SPOKEN,\n VOICE_SOURCE_TYPED,\n VOICE_SOURCE_DELEGATION,\n]);\n\n/**\n * Whether a user message is one of a call's turns: spoken, typed into the call,\n * or a task the live model delegated (by source, or by id on hosts that drop\n * the command's metadata).\n */\nexport function isCallUserMessage(\n message: Pick<CallRecapMessage, 'id' | 'role' | 'metadata'>,\n): boolean {\n if (message.role !== 'user') return false;\n if (CALL_SOURCES.has(message.metadata?.custom?.source)) return true;\n return (\n message.id.startsWith(VOICE_DELEGATION_ID_PREFIX) ||\n message.id.startsWith(VOICE_TYPED_ID_PREFIX)\n );\n}\n\n/**\n * Whether a stretch that ends the transcript may still be a live call: the\n * host reports a voice session (`state.voice`), or its state has not loaded\n * yet. A primitive read of the raw statewire state, safe as a selector.\n */\nexport function mayHaveLiveCall(state: unknown): boolean {\n if (!isDeepAgentStateHydrated(state)) return true;\n const voice: unknown = readDeepAgentStreamValues(state).voice;\n return voice !== null && voice !== undefined;\n}\n\n// ── Per-part entries ─────────────────────────────────────────────────────────\n\ntype Entry =\n | { readonly kind: 'asset'; readonly asset: CallRecapAsset }\n | { readonly kind: 'link'; readonly link: CallRecapLink }\n | { readonly kind: 'open-call'; readonly call: CallRecapOpenToolCall };\n\nconst NO_ENTRIES: readonly Entry[] = Object.freeze([]);\n\n/** `[label](href)` with an optional `\"title\"`; one level of brackets in the label, one of parens in the href. */\nconst MARKDOWN_LINK =\n /\\[((?:[^[\\]\\n]|\\[[^[\\]\\n]*\\])*)\\]\\(\\s*<?((?:[^()\\s<>]|\\([^()\\s]*\\))+)>?(?:\\s+\"[^\"\\n]*\")?\\s*\\)/g;\n/** A bare web address in prose. */\nconst BARE_URL = /\\bhttps?:\\/\\/[^\\s<>()[\\]\"'`]+/g;\n/** Sentence punctuation that follows an address rather than belonging to it. */\nconst TRAILING_PUNCTUATION = /[.,;:!?'\"*_]+$/;\n/** The Spaces route an asset link opens (`/dashboard/spaces?asset_ids=…`). */\nconst SPACES_PATH = /\\/dashboard\\/spaces\\/?$/;\n/** Past this a tool result is not read for an asset: a created asset's answer is small. */\nconst MAX_TOOL_RESULT_CHARS = 20_000;\n\nfunction assetEntry(asset: Omit<CallRecapAsset, 'kind' | 'key'>): Entry {\n return {\n kind: 'asset',\n asset: { kind: 'asset', key: `asset:${asset.assetId}`, ...asset },\n };\n}\n\n/** `sheet` is the old spelling of `spreadsheet` in Spaces links. */\nfunction normalizeAssetType(value: string | null): string | null {\n if (!value) return null;\n return value === 'sheet' ? 'spreadsheet' : value;\n}\n\nfunction labelOf(raw: string): string {\n return raw.trim().replace(/^@/, '').trim();\n}\n\n/** The page's host and path, for a link that carries no label of its own. */\nfunction displayUrl(url: URL): string {\n const host = url.hostname.replace(/^www\\./, '');\n const path = url.pathname === '/' ? '' : url.pathname.replace(/\\/$/, '');\n return `${host}${path}`;\n}\n\n/** The base a relative Spaces link (`/dashboard/spaces?asset_ids=…`) resolves against; only its query is read. */\nconst RELATIVE_LINK_BASE = 'https://app.athenaintel.com';\n\n/** The assets an Athena link names, the web page a link names, or nothing. */\nfunction entriesOfHref(href: string, rawLabel: string): readonly Entry[] {\n const label = labelOf(rawLabel);\n let url: URL;\n try {\n url = new URL(href, href.startsWith('/') ? RELATIVE_LINK_BASE : undefined);\n } catch {\n return [];\n }\n if (url.protocol === 'mention:') {\n if (url.hostname !== 'asset') return [];\n const assetId = url.searchParams.get('id') ?? url.searchParams.get('asset_id');\n if (!assetId?.startsWith('asset_')) return [];\n return [\n assetEntry({\n assetId,\n title: label || (url.searchParams.get('name') ?? ''),\n assetType: normalizeAssetType(\n url.searchParams.get('asset_type') ?? url.searchParams.get('type'),\n ),\n href,\n }),\n ];\n }\n if (url.protocol !== 'https:' && url.protocol !== 'http:') return [];\n const assetIds = url.searchParams.get('asset_ids');\n if (assetIds && SPACES_PATH.test(url.pathname)) {\n const ids = [\n ...new Set(\n assetIds\n .split(',')\n .map((id) => id.trim())\n .filter((id) => id.startsWith('asset_')),\n ),\n ];\n const assetType = normalizeAssetType(\n url.searchParams.get('type') ??\n url.searchParams.get('assetType') ??\n url.searchParams.get('asset_type'),\n );\n // A link that opens several assets side by side names each of them: one\n // item per asset, each opening only its own asset.\n return ids.map((assetId) => {\n if (ids.length === 1) return assetEntry({ assetId, title: label, assetType, href });\n const single = new URL(url.toString());\n single.searchParams.set('asset_ids', assetId);\n return assetEntry({\n assetId,\n title: label,\n assetType,\n href: href.startsWith('/') ? `${single.pathname}${single.search}` : single.toString(),\n });\n });\n }\n // Any other relative link is a route inside the app, not a page the call named.\n if (href.startsWith('/')) return [];\n const shown = label && label !== href ? label : displayUrl(url);\n const key = `${url.origin}${url.pathname.replace(/\\/$/, '')}${url.search}`;\n return [\n {\n kind: 'link',\n link: { kind: 'link', key: `link:${key}`, url: url.toString(), label: shown },\n },\n ];\n}\n\n/** Fenced code blocks and inline code spans: an address shown as code is not a link the message names. */\nconst CODE = /```[\\s\\S]*?(?:```|$)|`[^`\\n]*`/g;\n\n/** The asset and web links a text part carries, in the order it carries them. */\nfunction textEntries(raw: string): readonly Entry[] {\n // An address (`https://`, `mention://`) or a relative Spaces link (`](/dashboard/…`).\n if (!raw.includes('://') && !raw.includes('](/')) return NO_ENTRIES;\n // Blanked to the same length, so the rest keeps its offsets (the order the part names things in).\n const text = raw.includes('`') ? raw.replace(CODE, (code) => ' '.repeat(code.length)) : raw;\n const found: { index: number; entry: Entry }[] = [];\n const covered: [number, number][] = [];\n for (const match of text.matchAll(MARKDOWN_LINK)) {\n const [whole, label = '', href = ''] = match;\n covered.push([match.index, match.index + whole.length]);\n // `![alt](src)` is an image the message shows, not a link it names.\n if (text[match.index - 1] === '!') continue;\n for (const entry of entriesOfHref(href, label)) found.push({ index: match.index, entry });\n }\n for (const match of text.matchAll(BARE_URL)) {\n const start = match.index;\n if (covered.some(([from, to]) => start >= from && start < to)) continue;\n const href = match[0].replace(TRAILING_PUNCTUATION, '');\n for (const entry of entriesOfHref(href, '')) found.push({ index: start, entry });\n }\n if (found.length === 0) return NO_ENTRIES;\n found.sort((a, b) => a.index - b.index);\n return Object.freeze(found.map(({ entry }) => entry));\n}\n\n/** The asset a finished tool call reports (`asset_id` with a `title`), the open call, or nothing. */\nfunction toolCallEntries(part: CallRecapPart): readonly Entry[] {\n const toolCallId = part.toolCallId;\n const toolName = part.toolName ?? 'tool';\n if (!toolCallId) return NO_ENTRIES;\n if (part.result === undefined) {\n return Object.freeze([{ kind: 'open-call', call: { toolCallId, toolName } }]);\n }\n // A running tool's interim payload is not its outcome: the asset counts once the tool finished.\n if (part.isPreliminary === true) return NO_ENTRIES;\n if (typeof part.result === 'string' && part.result.length > MAX_TOOL_RESULT_CHARS)\n return NO_ENTRIES;\n const record = resultRecord(part.result);\n if (!record) return NO_ENTRIES;\n const assetId = stringField(record, ['asset_id', 'assetId']);\n const title = stringField(record, ['title', 'asset_title', 'name']);\n if (!assetId?.startsWith('asset_') || !title) return NO_ENTRIES;\n const outcome = classifyToolResult({\n status: { type: 'complete' } satisfies ToolCallStatusLike,\n result: part.result,\n isError: part.isError,\n });\n if (outcome.state !== 'complete') return NO_ENTRIES;\n return Object.freeze([\n assetEntry({\n assetId,\n title: title.trim(),\n assetType: normalizeAssetType(stringField(record, ['asset_type', 'assetType'])),\n href: null,\n }),\n ]);\n}\n\nconst partEntriesCache = new WeakMap<object, readonly Entry[]>();\n\nfunction partEntries(part: CallRecapPart): readonly Entry[] {\n const cached = partEntriesCache.get(part);\n if (cached !== undefined) return cached;\n const entries =\n part.type === 'text' && typeof part.text === 'string'\n ? textEntries(part.text)\n : part.type === 'tool-call'\n ? toolCallEntries(part)\n : NO_ENTRIES;\n partEntriesCache.set(part, entries);\n return entries;\n}\n\n// ── Per-message facts ────────────────────────────────────────────────────────\n\ninterface MessageFacts {\n /** A user message outside any call: it ends the stretch before it. */\n readonly breaksCall: boolean;\n /** The first part that belongs to a call (a call turn's first part, a spoken assistant part, a voice update), or -1. */\n readonly callFrom: number;\n}\n\nconst factsCache = new WeakMap<object, MessageFacts>();\n\n/** Whether the converter bound a part to its wire entry (`isSpokenTurnTextPart` reads that entry). */\nfunction isBoundPart(part: CallRecapPart): boolean {\n return getExternalStoreMessages<unknown>(part as ThreadMessage['content'][number]).length > 0;\n}\n\nfunction messageFacts(message: CallRecapMessage): MessageFacts {\n const cached = factsCache.get(message);\n if (cached !== undefined) return cached;\n let facts: MessageFacts;\n if (message.role === 'user') {\n const call = isCallUserMessage(message);\n facts = { breaksCall: !call, callFrom: call ? 0 : -1 };\n } else if (message.role === 'assistant') {\n // A spoken turn, or the interim-update tool only a run the live model\n // delegated is given (`send_voice_update`): a delegated run still marks its\n // call when a windowed transcript has not loaded the turns before it.\n let callFrom = message.content.findIndex(\n (part) =>\n (part.type === 'text' && isSpokenTurnTextPart(part)) ||\n (part.type === 'tool-call' && part.toolName === VOICE_UPDATE_TOOL_NAME),\n );\n // A message no converter bound reads by its own source (one spoken turn).\n if (\n callFrom < 0 &&\n message.metadata?.custom?.source === VOICE_SOURCE_SPOKEN &&\n !message.content.some(isBoundPart)\n ) {\n callFrom = 0;\n }\n facts = { breaksCall: false, callFrom };\n } else {\n facts = { breaksCall: false, callFrom: -1 };\n }\n factsCache.set(message, facts);\n return facts;\n}\n\n// ── The recap at a row ───────────────────────────────────────────────────────\n\ninterface RecapMemo {\n /** The stretch's messages, start first, as last read. */\n readonly span: readonly CallRecapMessage[];\n readonly startPart: number;\n readonly trailing: boolean;\n readonly recap: CallRecap;\n}\n\n/** Keyed by the stretch's last message: a settled stretch answers from here without rebuilding. */\nconst recapMemo = new WeakMap<object, RecapMemo>();\n\n/** Equal recaps share one frozen object (bounded; a miss only costs a rebuild). */\nconst internedRecaps = new Map<string, CallRecap>();\nconst MAX_INTERNED_RECAPS = 200;\n\nfunction recapSignature(recap: CallRecap): string {\n return JSON.stringify([\n recap.endMessageId,\n recap.trailing,\n recap.assets.map((asset) => [asset.assetId, asset.title, asset.assetType, asset.href]),\n recap.links.map((link) => [link.url, link.label]),\n recap.openToolCalls.map((call) => [call.toolCallId, call.toolName]),\n ]);\n}\n\nfunction intern(recap: CallRecap): CallRecap {\n const signature = recapSignature(recap);\n const existing = internedRecaps.get(signature);\n if (existing !== undefined) {\n // Most recently used last, so the bound evicts the stalest.\n internedRecaps.delete(signature);\n internedRecaps.set(signature, existing);\n return existing;\n }\n const frozen: CallRecap = Object.freeze({\n ...recap,\n assets: Object.freeze([...recap.assets]),\n links: Object.freeze([...recap.links]),\n openToolCalls: Object.freeze([...recap.openToolCalls]),\n });\n internedRecaps.set(signature, frozen);\n if (internedRecaps.size > MAX_INTERNED_RECAPS) {\n const oldest = internedRecaps.keys().next().value;\n if (oldest !== undefined) internedRecaps.delete(oldest);\n }\n return frozen;\n}\n\nfunction sameSpan(a: readonly CallRecapMessage[], b: readonly CallRecapMessage[]): boolean {\n if (a.length !== b.length) return false;\n for (let index = 0; index < a.length; index += 1) {\n if (a[index] !== b[index]) return false;\n }\n return true;\n}\n\nfunction buildRecap({\n span,\n startPart,\n trailing,\n}: {\n span: readonly CallRecapMessage[];\n startPart: number;\n trailing: boolean;\n}): CallRecap {\n const assets = new Map<string, CallRecapAsset>();\n const links = new Map<string, CallRecapLink>();\n const openToolCalls = new Map<string, CallRecapOpenToolCall>();\n span.forEach((message, position) => {\n const parts = position === 0 ? message.content.slice(startPart) : message.content;\n for (const part of parts) {\n for (const entry of partEntries(part)) {\n if (entry.kind === 'asset') {\n const known = assets.get(entry.asset.assetId);\n if (!known) {\n assets.set(entry.asset.assetId, entry.asset);\n } else if (\n (!known.title && entry.asset.title) ||\n (!known.assetType && entry.asset.assetType)\n ) {\n // A later mention fills what the first one left out; the first keeps its place.\n assets.set(entry.asset.assetId, {\n ...known,\n title: known.title || entry.asset.title,\n assetType: known.assetType ?? entry.asset.assetType,\n href: known.href ?? entry.asset.href,\n });\n }\n } else if (entry.kind === 'link') {\n if (!links.has(entry.link.key)) links.set(entry.link.key, entry.link);\n } else if (!openToolCalls.has(entry.call.toolCallId)) {\n openToolCalls.set(entry.call.toolCallId, entry.call);\n }\n }\n }\n });\n const end = span[span.length - 1];\n return {\n endMessageId: end?.id ?? '',\n trailing,\n assets: [...assets.values()],\n links: [...links.values()],\n openToolCalls: [...openToolCalls.values()],\n };\n}\n\n/**\n * The recap of the call stretch that ends at `messages[index]`, or `null` when\n * no call stretch ends there. Cheap for every other row (it looks at the next\n * message only); the same frozen object while the recap is unchanged, so it is\n * safe as a `useAuiState` selector result.\n */\nexport function callRecapAfter(\n messages: readonly CallRecapMessage[],\n index: number,\n): CallRecap | null {\n const message = messages[index];\n if (message === undefined) return null;\n const next = messages[index + 1];\n // Only a user message outside a call ends a stretch; an assistant message never does.\n if (next !== undefined && (next.role !== 'user' || isCallUserMessage(next))) return null;\n\n // Walk back over this stretch only: to the user message outside a call that opened it.\n let start = -1;\n let startPart = -1;\n for (let position = index; position >= 0; position -= 1) {\n const candidate = messages[position];\n if (candidate === undefined) break;\n const facts = messageFacts(candidate);\n if (facts.breaksCall) break;\n if (facts.callFrom >= 0) {\n start = position;\n startPart = facts.callFrom;\n }\n }\n if (start < 0) return null;\n\n const span = messages.slice(start, index + 1);\n const trailing = next === undefined;\n const memo = recapMemo.get(message);\n if (\n memo !== undefined &&\n memo.startPart === startPart &&\n memo.trailing === trailing &&\n sameSpan(memo.span, span)\n ) {\n return memo.recap;\n }\n const recap = intern(buildRecap({ span, startPart, trailing }));\n recapMemo.set(message, { span, startPart, trailing, recap });\n return recap;\n}\n\n/** Whether a recap has anything to show once approvals are matched (see {@link callRecapItems}). */\nexport function callRecapHasItems(\n recap: CallRecap,\n awaitingApprovalToolCallIds: ReadonlySet<string> = EMPTY_IDS,\n): boolean {\n return (\n recap.assets.length > 0 ||\n recap.links.length > 0 ||\n recap.openToolCalls.some((call) => awaitingApprovalToolCallIds.has(call.toolCallId))\n );\n}\n\nconst EMPTY_IDS: ReadonlySet<string> = new Set();\n\n/**\n * The card's items: the call's tool calls still waiting for the person's\n * approval (the host's live request names them), then its assets, then its\n * web links. Pure; hosts memoize it on the recap and the id set.\n */\nexport function callRecapItems(\n recap: CallRecap,\n awaitingApprovalToolCallIds: ReadonlySet<string> = EMPTY_IDS,\n): CallRecapItem[] {\n const approvals: CallRecapApproval[] = recap.openToolCalls\n .filter((call) => awaitingApprovalToolCallIds.has(call.toolCallId))\n .map((call) => ({\n kind: 'approval',\n key: `approval:${call.toolCallId}`,\n toolCallId: call.toolCallId,\n toolName: call.toolName,\n }));\n return [...approvals, ...recap.assets, ...recap.links];\n}\n\n/** The card's heading, worded once for every app. */\nexport const CALL_RECAP_TITLE = 'From the call';\n\n/** An approval item's copy, worded once for every app. */\nexport const CALL_RECAP_APPROVAL_LABEL = 'Waiting for your approval';\n"],"names":["VOICE_SOURCE_SPOKEN","VOICE_SOURCE_TYPED","VOICE_SOURCE_DELEGATION","VOICE_DELEGATION_ID_PREFIX","isDeepAgentStateHydrated","readDeepAgentStreamValues","resultRecord","stringField","classifyToolResult","getExternalStoreMessages","isSpokenTurnTextPart","VOICE_UPDATE_TOOL_NAME"],"mappings":"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"}
1
+ {"version":3,"file":"call-recap.cjs","sources":["../../../../agent-runtime/src/call-recap.ts"],"sourcesContent":["/**\n * \"From the call\": what a voice call produced or mentioned, gathered where the\n * call's stretch of the transcript ends, so a person who talked to the agent\n * and later reads the conversation as text (on the same app or another) finds\n * the call's assets, links and open approvals in one place.\n *\n * Everything here is read from the transcript every app already renders, and\n * nothing is invented: an asset item is an asset link or `@`-mention in a\n * message, or a tool result that names the asset it made or read (`asset_id`\n * with a `title`); a link item is a web link in a message; an approval item is\n * a tool call of the call's that has no result yet, shown only while the\n * host's live approval request still names it (the presenter's\n * {@link callRecapItems}).\n *\n * **Where a call is in the transcript.** The host marks every turn a call\n * commits (`metadata.custom.source`, see `voice-presentation.ts`): spoken turns\n * (`voice`), messages typed into the call (`text`, id `voicetext_…`) and the\n * tasks the live model handed the thread (`voice_delegation`, id\n * `voicetask_…`). A typed message sent outside a call carries none of these. So\n * a call's stretch starts at its first marked turn and runs, through the\n * assistant work in between (the delegated runs), up to the next user message\n * that is not a call turn, or the end of the transcript. A spoken assistant\n * turn the converter joined onto the end of an earlier run's message starts\n * the stretch at that part, so the earlier run's artifacts stay out. A\n * delegated run's `send_voice_update` call (a tool only voice-delegated runs\n * get) marks the call too, so a windowed transcript whose loaded tail starts\n * inside a long delegated run still finds it.\n *\n * Two calls with no typed message between them read as one stretch: the\n * transcript does not say where one ended and the next began (the host keeps\n * no per-call marker), so their card collects both. A stretch that ends the\n * transcript may still be live; the presenter shows it once the host reports\n * no voice session (`state.voice` is null), see {@link mayHaveLiveCall}.\n *\n * **Cost.** Every message row asks {@link callRecapAfter} on every transcript\n * update. A row that does not end a stretch answers after looking at the next\n * message; a row that does walks back over its own stretch only. What a message\n * and each of its parts contribute is memoized per object (statewire replaces a\n * changed message and assistant-ui keeps every unchanged part), and an equal\n * recap is the same frozen object, so a row selecting it re-renders only when\n * the card's contents change. The transcript is never re-scanned per delta.\n */\n\nimport { getExternalStoreMessages, type ThreadMessage } from '@assistant-ui/core';\nimport { isDeepAgentStateHydrated, readDeepAgentStreamValues } from './statewire-state';\nimport {\n classifyToolResult,\n resultRecord,\n stringField,\n type ToolCallStatusLike,\n} from './tool-result';\nimport {\n isSpokenTurnTextPart,\n VOICE_DELEGATION_ID_PREFIX,\n VOICE_UPDATE_TOOL_NAME,\n VOICE_SOURCE_DELEGATION,\n VOICE_SOURCE_SPOKEN,\n VOICE_SOURCE_TYPED,\n VOICE_TYPED_ID_PREFIX,\n} from './voice-presentation';\n\nexport { VOICE_TYPED_ID_PREFIX } from './voice-presentation';\n\n/** An asset the call created, read or linked. */\nexport interface CallRecapAsset {\n readonly kind: 'asset';\n /** Unique within a recap. */\n readonly key: string;\n readonly assetId: string;\n /** What the transcript calls it (the link's label, or the tool result's title); may be empty. */\n readonly title: string;\n /** The asset type the link or tool named (`document`, `spreadsheet`, …), when it named one. */\n readonly assetType: string | null;\n /** The link the transcript carried for it, when it came from a link. */\n readonly href: string | null;\n}\n\n/** A web page the call linked. */\nexport interface CallRecapLink {\n readonly kind: 'link';\n readonly key: string;\n readonly url: string;\n /** The link's own label, else the page's host and path. */\n readonly label: string;\n}\n\n/** A tool call of the call's that is waiting for the person's approval. */\nexport interface CallRecapApproval {\n readonly kind: 'approval';\n readonly key: string;\n readonly toolCallId: string;\n readonly toolName: string;\n}\n\nexport type CallRecapItem = CallRecapAsset | CallRecapLink | CallRecapApproval;\n\n/** A tool call in the stretch with no result yet: an approval while the host's request names it. */\nexport interface CallRecapOpenToolCall {\n readonly toolCallId: string;\n readonly toolName: string;\n}\n\nexport interface CallRecap {\n /** The message the stretch ends at; the card renders right after it. */\n readonly endMessageId: string;\n /** The stretch ends the transcript, so the call may still be live. */\n readonly trailing: boolean;\n /** Assets, in the order the stretch first mentioned them. */\n readonly assets: readonly CallRecapAsset[];\n /** Web links, in the order the stretch first mentioned them. */\n readonly links: readonly CallRecapLink[];\n /** Result-less tool calls, in transcript order. */\n readonly openToolCalls: readonly CallRecapOpenToolCall[];\n}\n\n/** The fields of an assistant-ui thread message this reads (structural, so any runtime's message fits). */\nexport interface CallRecapMessage {\n readonly id: string;\n readonly role: string;\n readonly content: readonly CallRecapPart[];\n readonly metadata?: { readonly custom?: Record<string, unknown> } | undefined;\n}\n\n/** The fields of a message part this reads. */\nexport interface CallRecapPart {\n readonly type: string;\n readonly text?: string;\n readonly toolCallId?: string;\n readonly toolName?: string;\n readonly result?: unknown;\n readonly isError?: boolean;\n /** An interim result a running tool published (`markPreliminaryToolResults`); the real one has not arrived. */\n readonly isPreliminary?: boolean;\n readonly status?: unknown;\n}\n\nconst CALL_SOURCES: ReadonlySet<unknown> = new Set([\n VOICE_SOURCE_SPOKEN,\n VOICE_SOURCE_TYPED,\n VOICE_SOURCE_DELEGATION,\n]);\n\n/**\n * Whether a user message is one of a call's turns: spoken, typed into the call,\n * or a task the live model delegated (by source, or by id on hosts that drop\n * the command's metadata).\n */\nexport function isCallUserMessage(\n message: Pick<CallRecapMessage, 'id' | 'role' | 'metadata'>,\n): boolean {\n if (message.role !== 'user') return false;\n if (CALL_SOURCES.has(message.metadata?.custom?.source)) return true;\n return (\n message.id.startsWith(VOICE_DELEGATION_ID_PREFIX) ||\n message.id.startsWith(VOICE_TYPED_ID_PREFIX)\n );\n}\n\n/**\n * Whether a stretch that ends the transcript may still be a live call: the\n * host reports a voice session (`state.voice`), or its state has not loaded\n * yet. A primitive read of the raw statewire state, safe as a selector.\n */\nexport function mayHaveLiveCall(state: unknown): boolean {\n if (!isDeepAgentStateHydrated(state)) return true;\n const voice: unknown = readDeepAgentStreamValues(state).voice;\n return voice !== null && voice !== undefined;\n}\n\n// ── Per-part entries ─────────────────────────────────────────────────────────\n\ntype Entry =\n | { readonly kind: 'asset'; readonly asset: CallRecapAsset }\n | { readonly kind: 'link'; readonly link: CallRecapLink }\n | { readonly kind: 'open-call'; readonly call: CallRecapOpenToolCall };\n\nconst NO_ENTRIES: readonly Entry[] = Object.freeze([]);\n\n/** `[label](href)` with an optional `\"title\"`; one level of brackets in the label, one of parens in the href. */\nconst MARKDOWN_LINK =\n /\\[((?:[^[\\]\\n]|\\[[^[\\]\\n]*\\])*)\\]\\(\\s*<?((?:[^()\\s<>]|\\([^()\\s]*\\))+)>?(?:\\s+\"[^\"\\n]*\")?\\s*\\)/g;\n/** A bare web address in prose. */\nconst BARE_URL = /\\bhttps?:\\/\\/[^\\s<>()[\\]\"'`]+/g;\n/** Sentence punctuation that follows an address rather than belonging to it. */\nconst TRAILING_PUNCTUATION = /[.,;:!?'\"*_]+$/;\n/** The Spaces route an asset link opens (`/dashboard/spaces?asset_ids=…`). */\nconst SPACES_PATH = /\\/dashboard\\/spaces\\/?$/;\n/** Past this a tool result is not read for an asset: a created asset's answer is small. */\nconst MAX_TOOL_RESULT_CHARS = 20_000;\n\nfunction assetEntry(asset: Omit<CallRecapAsset, 'kind' | 'key'>): Entry {\n return {\n kind: 'asset',\n asset: { kind: 'asset', key: `asset:${asset.assetId}`, ...asset },\n };\n}\n\n/** `sheet` is the old spelling of `spreadsheet` in Spaces links. */\nfunction normalizeAssetType(value: string | null): string | null {\n if (!value) return null;\n return value === 'sheet' ? 'spreadsheet' : value;\n}\n\nfunction labelOf(raw: string): string {\n return raw.trim().replace(/^@/, '').trim();\n}\n\n/** The page's host and path, for a link that carries no label of its own. */\nfunction displayUrl(url: URL): string {\n const host = url.hostname.replace(/^www\\./, '');\n const path = url.pathname === '/' ? '' : url.pathname.replace(/\\/$/, '');\n return `${host}${path}`;\n}\n\n/** The base a relative Spaces link (`/dashboard/spaces?asset_ids=…`) resolves against; only its query is read. */\nconst RELATIVE_LINK_BASE = 'https://app.athenaintel.com';\n\n/** The assets an Athena link names, the web page a link names, or nothing. */\nfunction entriesOfHref(href: string, rawLabel: string): readonly Entry[] {\n const label = labelOf(rawLabel);\n let url: URL;\n try {\n url = new URL(href, href.startsWith('/') ? RELATIVE_LINK_BASE : undefined);\n } catch {\n return [];\n }\n if (url.protocol === 'mention:') {\n if (url.hostname !== 'asset') return [];\n const assetId = url.searchParams.get('id') ?? url.searchParams.get('asset_id');\n if (!assetId?.startsWith('asset_')) return [];\n return [\n assetEntry({\n assetId,\n title: label || (url.searchParams.get('name') ?? ''),\n assetType: normalizeAssetType(\n url.searchParams.get('asset_type') ?? url.searchParams.get('type'),\n ),\n href,\n }),\n ];\n }\n if (url.protocol !== 'https:' && url.protocol !== 'http:') return [];\n const assetIds = url.searchParams.get('asset_ids');\n if (assetIds && SPACES_PATH.test(url.pathname)) {\n const ids = [\n ...new Set(\n assetIds\n .split(',')\n .map((id) => id.trim())\n .filter((id) => id.startsWith('asset_')),\n ),\n ];\n const assetType = normalizeAssetType(\n url.searchParams.get('type') ??\n url.searchParams.get('assetType') ??\n url.searchParams.get('asset_type'),\n );\n // A link that opens several assets side by side names each of them: one\n // item per asset, each opening only its own asset.\n return ids.map((assetId) => {\n if (ids.length === 1) return assetEntry({ assetId, title: label, assetType, href });\n const single = new URL(url.toString());\n single.searchParams.set('asset_ids', assetId);\n return assetEntry({\n assetId,\n title: label,\n assetType,\n href: href.startsWith('/') ? `${single.pathname}${single.search}` : single.toString(),\n });\n });\n }\n // Any other relative link is a route inside the app, not a page the call named.\n if (href.startsWith('/')) return [];\n const shown = label && label !== href ? label : displayUrl(url);\n const key = `${url.origin}${url.pathname.replace(/\\/$/, '')}${url.search}`;\n return [\n {\n kind: 'link',\n link: { kind: 'link', key: `link:${key}`, url: url.toString(), label: shown },\n },\n ];\n}\n\n/** Fenced code blocks and inline code spans: an address shown as code is not a link the message names. */\nconst CODE = /```[\\s\\S]*?(?:```|$)|`[^`\\n]*`/g;\n\n/** The asset and web links a text part carries, in the order it carries them. */\nfunction textEntries(raw: string): readonly Entry[] {\n // An address (`https://`, `mention://`) or a relative Spaces link (`](/dashboard/…`).\n if (!raw.includes('://') && !raw.includes('](/')) return NO_ENTRIES;\n // Blanked to the same length, so the rest keeps its offsets (the order the part names things in).\n const text = raw.includes('`') ? raw.replace(CODE, (code) => ' '.repeat(code.length)) : raw;\n const found: { index: number; entry: Entry }[] = [];\n const covered: [number, number][] = [];\n for (const match of text.matchAll(MARKDOWN_LINK)) {\n const [whole, label = '', href = ''] = match;\n covered.push([match.index, match.index + whole.length]);\n // `![alt](src)` is an image the message shows, not a link it names.\n if (text[match.index - 1] === '!') continue;\n for (const entry of entriesOfHref(href, label)) found.push({ index: match.index, entry });\n }\n for (const match of text.matchAll(BARE_URL)) {\n const start = match.index;\n if (covered.some(([from, to]) => start >= from && start < to)) continue;\n const href = match[0].replace(TRAILING_PUNCTUATION, '');\n for (const entry of entriesOfHref(href, '')) found.push({ index: start, entry });\n }\n if (found.length === 0) return NO_ENTRIES;\n found.sort((a, b) => a.index - b.index);\n return Object.freeze(found.map(({ entry }) => entry));\n}\n\n/** The asset a finished tool call reports (`asset_id` with a `title`), the open call, or nothing. */\nfunction toolCallEntries(part: CallRecapPart): readonly Entry[] {\n const toolCallId = part.toolCallId;\n const toolName = part.toolName ?? 'tool';\n if (!toolCallId) return NO_ENTRIES;\n if (part.result === undefined) {\n return Object.freeze([{ kind: 'open-call', call: { toolCallId, toolName } }]);\n }\n // A running tool's interim payload is not its outcome: the asset counts once the tool finished.\n if (part.isPreliminary === true) return NO_ENTRIES;\n if (typeof part.result === 'string' && part.result.length > MAX_TOOL_RESULT_CHARS)\n return NO_ENTRIES;\n const record = resultRecord(part.result);\n if (!record) return NO_ENTRIES;\n const assetId = stringField(record, ['asset_id', 'assetId']);\n const title = stringField(record, ['title', 'asset_title', 'name']);\n if (!assetId?.startsWith('asset_') || !title) return NO_ENTRIES;\n const outcome = classifyToolResult({\n status: { type: 'complete' } satisfies ToolCallStatusLike,\n result: part.result,\n isError: part.isError,\n });\n if (outcome.state !== 'complete') return NO_ENTRIES;\n return Object.freeze([\n assetEntry({\n assetId,\n title: title.trim(),\n assetType: normalizeAssetType(stringField(record, ['asset_type', 'assetType'])),\n href: null,\n }),\n ]);\n}\n\nconst partEntriesCache = new WeakMap<object, readonly Entry[]>();\n\nfunction partEntries(part: CallRecapPart): readonly Entry[] {\n const cached = partEntriesCache.get(part);\n if (cached !== undefined) return cached;\n const entries =\n part.type === 'text' && typeof part.text === 'string'\n ? textEntries(part.text)\n : part.type === 'tool-call'\n ? toolCallEntries(part)\n : NO_ENTRIES;\n partEntriesCache.set(part, entries);\n return entries;\n}\n\n// ── Per-message facts ────────────────────────────────────────────────────────\n\ninterface MessageFacts {\n /** A user message outside any call: it ends the stretch before it. */\n readonly breaksCall: boolean;\n /** The first part that belongs to a call (a call turn's first part, a spoken assistant part, a voice update), or -1. */\n readonly callFrom: number;\n}\n\nconst factsCache = new WeakMap<object, MessageFacts>();\n\n/** Whether the converter bound a part to its wire entry (`isSpokenTurnTextPart` reads that entry). */\nfunction isBoundPart(part: CallRecapPart): boolean {\n return getExternalStoreMessages<unknown>(part as ThreadMessage['content'][number]).length > 0;\n}\n\nfunction messageFacts(message: CallRecapMessage): MessageFacts {\n const cached = factsCache.get(message);\n if (cached !== undefined) return cached;\n let facts: MessageFacts;\n if (message.role === 'user') {\n const call = isCallUserMessage(message);\n facts = { breaksCall: !call, callFrom: call ? 0 : -1 };\n } else if (message.role === 'assistant') {\n // A spoken turn, or the interim-update tool only a run the live model\n // delegated is given (`send_voice_update`): a delegated run still marks its\n // call when a windowed transcript has not loaded the turns before it.\n let callFrom = message.content.findIndex(\n (part) =>\n (part.type === 'text' && isSpokenTurnTextPart(part)) ||\n (part.type === 'tool-call' && part.toolName === VOICE_UPDATE_TOOL_NAME),\n );\n // A message no converter bound reads by its own source (one spoken turn).\n if (\n callFrom < 0 &&\n message.metadata?.custom?.source === VOICE_SOURCE_SPOKEN &&\n !message.content.some(isBoundPart)\n ) {\n callFrom = 0;\n }\n facts = { breaksCall: false, callFrom };\n } else {\n facts = { breaksCall: false, callFrom: -1 };\n }\n factsCache.set(message, facts);\n return facts;\n}\n\n// ── The recap at a row ───────────────────────────────────────────────────────\n\ninterface RecapMemo {\n /** The stretch's messages, start first, as last read. */\n readonly span: readonly CallRecapMessage[];\n readonly startPart: number;\n readonly trailing: boolean;\n readonly recap: CallRecap;\n}\n\n/** Keyed by the stretch's last message: a settled stretch answers from here without rebuilding. */\nconst recapMemo = new WeakMap<object, RecapMemo>();\n\n/** Equal recaps share one frozen object (bounded; a miss only costs a rebuild). */\nconst internedRecaps = new Map<string, CallRecap>();\nconst MAX_INTERNED_RECAPS = 200;\n\nfunction recapSignature(recap: CallRecap): string {\n return JSON.stringify([\n recap.endMessageId,\n recap.trailing,\n recap.assets.map((asset) => [asset.assetId, asset.title, asset.assetType, asset.href]),\n recap.links.map((link) => [link.url, link.label]),\n recap.openToolCalls.map((call) => [call.toolCallId, call.toolName]),\n ]);\n}\n\nfunction intern(recap: CallRecap): CallRecap {\n const signature = recapSignature(recap);\n const existing = internedRecaps.get(signature);\n if (existing !== undefined) {\n // Most recently used last, so the bound evicts the stalest.\n internedRecaps.delete(signature);\n internedRecaps.set(signature, existing);\n return existing;\n }\n const frozen: CallRecap = Object.freeze({\n ...recap,\n assets: Object.freeze([...recap.assets]),\n links: Object.freeze([...recap.links]),\n openToolCalls: Object.freeze([...recap.openToolCalls]),\n });\n internedRecaps.set(signature, frozen);\n if (internedRecaps.size > MAX_INTERNED_RECAPS) {\n const oldest = internedRecaps.keys().next().value;\n if (oldest !== undefined) internedRecaps.delete(oldest);\n }\n return frozen;\n}\n\nfunction sameSpan(a: readonly CallRecapMessage[], b: readonly CallRecapMessage[]): boolean {\n if (a.length !== b.length) return false;\n for (let index = 0; index < a.length; index += 1) {\n if (a[index] !== b[index]) return false;\n }\n return true;\n}\n\nfunction buildRecap({\n span,\n startPart,\n trailing,\n}: {\n span: readonly CallRecapMessage[];\n startPart: number;\n trailing: boolean;\n}): CallRecap {\n const assets = new Map<string, CallRecapAsset>();\n const links = new Map<string, CallRecapLink>();\n const openToolCalls = new Map<string, CallRecapOpenToolCall>();\n span.forEach((message, position) => {\n const parts = position === 0 ? message.content.slice(startPart) : message.content;\n for (const part of parts) {\n for (const entry of partEntries(part)) {\n if (entry.kind === 'asset') {\n const known = assets.get(entry.asset.assetId);\n if (!known) {\n assets.set(entry.asset.assetId, entry.asset);\n } else if (\n (!known.title && entry.asset.title) ||\n (!known.assetType && entry.asset.assetType)\n ) {\n // A later mention fills what the first one left out; the first keeps its place.\n assets.set(entry.asset.assetId, {\n ...known,\n title: known.title || entry.asset.title,\n assetType: known.assetType ?? entry.asset.assetType,\n href: known.href ?? entry.asset.href,\n });\n }\n } else if (entry.kind === 'link') {\n if (!links.has(entry.link.key)) links.set(entry.link.key, entry.link);\n } else if (!openToolCalls.has(entry.call.toolCallId)) {\n openToolCalls.set(entry.call.toolCallId, entry.call);\n }\n }\n }\n });\n const end = span[span.length - 1];\n return {\n endMessageId: end?.id ?? '',\n trailing,\n assets: [...assets.values()],\n links: [...links.values()],\n openToolCalls: [...openToolCalls.values()],\n };\n}\n\n/**\n * The recap of the call stretch that ends at `messages[index]`, or `null` when\n * no call stretch ends there. Cheap for every other row (it looks at the next\n * message only); the same frozen object while the recap is unchanged, so it is\n * safe as a `useAuiState` selector result.\n */\nexport function callRecapAfter(\n messages: readonly CallRecapMessage[],\n index: number,\n): CallRecap | null {\n const message = messages[index];\n if (message === undefined) return null;\n const next = messages[index + 1];\n // Only a user message outside a call ends a stretch; an assistant message never does.\n if (next !== undefined && (next.role !== 'user' || isCallUserMessage(next))) return null;\n\n // Walk back over this stretch only: to the user message outside a call that opened it.\n let start = -1;\n let startPart = -1;\n for (let position = index; position >= 0; position -= 1) {\n const candidate = messages[position];\n if (candidate === undefined) break;\n const facts = messageFacts(candidate);\n if (facts.breaksCall) break;\n if (facts.callFrom >= 0) {\n start = position;\n startPart = facts.callFrom;\n }\n }\n if (start < 0) return null;\n\n const span = messages.slice(start, index + 1);\n const trailing = next === undefined;\n const memo = recapMemo.get(message);\n if (\n memo !== undefined &&\n memo.startPart === startPart &&\n memo.trailing === trailing &&\n sameSpan(memo.span, span)\n ) {\n return memo.recap;\n }\n const recap = intern(buildRecap({ span, startPart, trailing }));\n recapMemo.set(message, { span, startPart, trailing, recap });\n return recap;\n}\n\n/** Whether a recap has anything to show once approvals are matched (see {@link callRecapItems}). */\nexport function callRecapHasItems(\n recap: CallRecap,\n awaitingApprovalToolCallIds: ReadonlySet<string> = EMPTY_IDS,\n): boolean {\n return (\n recap.assets.length > 0 ||\n recap.links.length > 0 ||\n recap.openToolCalls.some((call) => awaitingApprovalToolCallIds.has(call.toolCallId))\n );\n}\n\nconst EMPTY_IDS: ReadonlySet<string> = new Set();\n\n/**\n * The card's items: the call's tool calls still waiting for the person's\n * approval (the host's live request names them), then its assets, then its\n * web links. Pure; hosts memoize it on the recap and the id set.\n */\nexport function callRecapItems(\n recap: CallRecap,\n awaitingApprovalToolCallIds: ReadonlySet<string> = EMPTY_IDS,\n): CallRecapItem[] {\n const approvals: CallRecapApproval[] = recap.openToolCalls\n .filter((call) => awaitingApprovalToolCallIds.has(call.toolCallId))\n .map((call) => ({\n kind: 'approval',\n key: `approval:${call.toolCallId}`,\n toolCallId: call.toolCallId,\n toolName: call.toolName,\n }));\n return [...approvals, ...recap.assets, ...recap.links];\n}\n\n/** The card's heading, worded once for every app. */\nexport const CALL_RECAP_TITLE = 'From the call';\n\n/** An approval item's copy, worded once for every app. */\nexport const CALL_RECAP_APPROVAL_LABEL = 'Waiting for your approval';\n"],"names":["VOICE_SOURCE_SPOKEN","VOICE_SOURCE_TYPED","VOICE_SOURCE_DELEGATION","VOICE_DELEGATION_ID_PREFIX","VOICE_TYPED_ID_PREFIX","isDeepAgentStateHydrated","readDeepAgentStreamValues","resultRecord","stringField","classifyToolResult","getExternalStoreMessages","isSpokenTurnTextPart","VOICE_UPDATE_TOOL_NAME"],"mappings":"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"}
@@ -40,8 +40,7 @@
40
40
  * recap is the same frozen object, so a row selecting it re-renders only when
41
41
  * the card's contents change. The transcript is never re-scanned per delta.
42
42
  */
43
- /** Id prefix of a message typed into a live call (`voice/user-text`). */
44
- export declare const VOICE_TYPED_ID_PREFIX = "voicetext_";
43
+ export { VOICE_TYPED_ID_PREFIX } from './voice-presentation';
45
44
  /** An asset the call created, read or linked. */
46
45
  export interface CallRecapAsset {
47
46
  readonly kind: 'asset';
@@ -1,8 +1,7 @@
1
1
  import { getExternalStoreMessages } from "@assistant-ui/core";
2
2
  import { isDeepAgentStateHydrated, readDeepAgentStreamValues } from "./statewire-state.js";
3
3
  import { resultRecord, stringField, classifyToolResult } from "./tool-result.js";
4
- import { VOICE_DELEGATION_ID_PREFIX, isSpokenTurnTextPart, VOICE_UPDATE_TOOL_NAME, VOICE_SOURCE_SPOKEN, VOICE_SOURCE_TYPED, VOICE_SOURCE_DELEGATION } from "./voice-presentation.js";
5
- const VOICE_TYPED_ID_PREFIX = "voicetext_";
4
+ import { VOICE_DELEGATION_ID_PREFIX, VOICE_TYPED_ID_PREFIX, isSpokenTurnTextPart, VOICE_UPDATE_TOOL_NAME, VOICE_SOURCE_SPOKEN, VOICE_SOURCE_TYPED, VOICE_SOURCE_DELEGATION } from "./voice-presentation.js";
6
5
  const CALL_SOURCES = /* @__PURE__ */ new Set([
7
6
  VOICE_SOURCE_SPOKEN,
8
7
  VOICE_SOURCE_TYPED,
@@ -1 +1 @@
1
- {"version":3,"file":"call-recap.js","sources":["../../../../agent-runtime/src/call-recap.ts"],"sourcesContent":["/**\n * \"From the call\": what a voice call produced or mentioned, gathered where the\n * call's stretch of the transcript ends, so a person who talked to the agent\n * and later reads the conversation as text (on the same app or another) finds\n * the call's assets, links and open approvals in one place.\n *\n * Everything here is read from the transcript every app already renders, and\n * nothing is invented: an asset item is an asset link or `@`-mention in a\n * message, or a tool result that names the asset it made or read (`asset_id`\n * with a `title`); a link item is a web link in a message; an approval item is\n * a tool call of the call's that has no result yet, shown only while the\n * host's live approval request still names it (the presenter's\n * {@link callRecapItems}).\n *\n * **Where a call is in the transcript.** The host marks every turn a call\n * commits (`metadata.custom.source`, see `voice-presentation.ts`): spoken turns\n * (`voice`), messages typed into the call (`text`, id `voicetext_…`) and the\n * tasks the live model handed the thread (`voice_delegation`, id\n * `voicetask_…`). A typed message sent outside a call carries none of these. So\n * a call's stretch starts at its first marked turn and runs, through the\n * assistant work in between (the delegated runs), up to the next user message\n * that is not a call turn, or the end of the transcript. A spoken assistant\n * turn the converter joined onto the end of an earlier run's message starts\n * the stretch at that part, so the earlier run's artifacts stay out. A\n * delegated run's `send_voice_update` call (a tool only voice-delegated runs\n * get) marks the call too, so a windowed transcript whose loaded tail starts\n * inside a long delegated run still finds it.\n *\n * Two calls with no typed message between them read as one stretch: the\n * transcript does not say where one ended and the next began (the host keeps\n * no per-call marker), so their card collects both. A stretch that ends the\n * transcript may still be live; the presenter shows it once the host reports\n * no voice session (`state.voice` is null), see {@link mayHaveLiveCall}.\n *\n * **Cost.** Every message row asks {@link callRecapAfter} on every transcript\n * update. A row that does not end a stretch answers after looking at the next\n * message; a row that does walks back over its own stretch only. What a message\n * and each of its parts contribute is memoized per object (statewire replaces a\n * changed message and assistant-ui keeps every unchanged part), and an equal\n * recap is the same frozen object, so a row selecting it re-renders only when\n * the card's contents change. The transcript is never re-scanned per delta.\n */\n\nimport { getExternalStoreMessages, type ThreadMessage } from '@assistant-ui/core';\nimport { isDeepAgentStateHydrated, readDeepAgentStreamValues } from './statewire-state';\nimport {\n classifyToolResult,\n resultRecord,\n stringField,\n type ToolCallStatusLike,\n} from './tool-result';\nimport {\n isSpokenTurnTextPart,\n VOICE_DELEGATION_ID_PREFIX,\n VOICE_UPDATE_TOOL_NAME,\n VOICE_SOURCE_DELEGATION,\n VOICE_SOURCE_SPOKEN,\n VOICE_SOURCE_TYPED,\n} from './voice-presentation';\n\n/** Id prefix of a message typed into a live call (`voice/user-text`). */\nexport const VOICE_TYPED_ID_PREFIX = 'voicetext_';\n\n/** An asset the call created, read or linked. */\nexport interface CallRecapAsset {\n readonly kind: 'asset';\n /** Unique within a recap. */\n readonly key: string;\n readonly assetId: string;\n /** What the transcript calls it (the link's label, or the tool result's title); may be empty. */\n readonly title: string;\n /** The asset type the link or tool named (`document`, `spreadsheet`, …), when it named one. */\n readonly assetType: string | null;\n /** The link the transcript carried for it, when it came from a link. */\n readonly href: string | null;\n}\n\n/** A web page the call linked. */\nexport interface CallRecapLink {\n readonly kind: 'link';\n readonly key: string;\n readonly url: string;\n /** The link's own label, else the page's host and path. */\n readonly label: string;\n}\n\n/** A tool call of the call's that is waiting for the person's approval. */\nexport interface CallRecapApproval {\n readonly kind: 'approval';\n readonly key: string;\n readonly toolCallId: string;\n readonly toolName: string;\n}\n\nexport type CallRecapItem = CallRecapAsset | CallRecapLink | CallRecapApproval;\n\n/** A tool call in the stretch with no result yet: an approval while the host's request names it. */\nexport interface CallRecapOpenToolCall {\n readonly toolCallId: string;\n readonly toolName: string;\n}\n\nexport interface CallRecap {\n /** The message the stretch ends at; the card renders right after it. */\n readonly endMessageId: string;\n /** The stretch ends the transcript, so the call may still be live. */\n readonly trailing: boolean;\n /** Assets, in the order the stretch first mentioned them. */\n readonly assets: readonly CallRecapAsset[];\n /** Web links, in the order the stretch first mentioned them. */\n readonly links: readonly CallRecapLink[];\n /** Result-less tool calls, in transcript order. */\n readonly openToolCalls: readonly CallRecapOpenToolCall[];\n}\n\n/** The fields of an assistant-ui thread message this reads (structural, so any runtime's message fits). */\nexport interface CallRecapMessage {\n readonly id: string;\n readonly role: string;\n readonly content: readonly CallRecapPart[];\n readonly metadata?: { readonly custom?: Record<string, unknown> } | undefined;\n}\n\n/** The fields of a message part this reads. */\nexport interface CallRecapPart {\n readonly type: string;\n readonly text?: string;\n readonly toolCallId?: string;\n readonly toolName?: string;\n readonly result?: unknown;\n readonly isError?: boolean;\n /** An interim result a running tool published (`markPreliminaryToolResults`); the real one has not arrived. */\n readonly isPreliminary?: boolean;\n readonly status?: unknown;\n}\n\nconst CALL_SOURCES: ReadonlySet<unknown> = new Set([\n VOICE_SOURCE_SPOKEN,\n VOICE_SOURCE_TYPED,\n VOICE_SOURCE_DELEGATION,\n]);\n\n/**\n * Whether a user message is one of a call's turns: spoken, typed into the call,\n * or a task the live model delegated (by source, or by id on hosts that drop\n * the command's metadata).\n */\nexport function isCallUserMessage(\n message: Pick<CallRecapMessage, 'id' | 'role' | 'metadata'>,\n): boolean {\n if (message.role !== 'user') return false;\n if (CALL_SOURCES.has(message.metadata?.custom?.source)) return true;\n return (\n message.id.startsWith(VOICE_DELEGATION_ID_PREFIX) ||\n message.id.startsWith(VOICE_TYPED_ID_PREFIX)\n );\n}\n\n/**\n * Whether a stretch that ends the transcript may still be a live call: the\n * host reports a voice session (`state.voice`), or its state has not loaded\n * yet. A primitive read of the raw statewire state, safe as a selector.\n */\nexport function mayHaveLiveCall(state: unknown): boolean {\n if (!isDeepAgentStateHydrated(state)) return true;\n const voice: unknown = readDeepAgentStreamValues(state).voice;\n return voice !== null && voice !== undefined;\n}\n\n// ── Per-part entries ─────────────────────────────────────────────────────────\n\ntype Entry =\n | { readonly kind: 'asset'; readonly asset: CallRecapAsset }\n | { readonly kind: 'link'; readonly link: CallRecapLink }\n | { readonly kind: 'open-call'; readonly call: CallRecapOpenToolCall };\n\nconst NO_ENTRIES: readonly Entry[] = Object.freeze([]);\n\n/** `[label](href)` with an optional `\"title\"`; one level of brackets in the label, one of parens in the href. */\nconst MARKDOWN_LINK =\n /\\[((?:[^[\\]\\n]|\\[[^[\\]\\n]*\\])*)\\]\\(\\s*<?((?:[^()\\s<>]|\\([^()\\s]*\\))+)>?(?:\\s+\"[^\"\\n]*\")?\\s*\\)/g;\n/** A bare web address in prose. */\nconst BARE_URL = /\\bhttps?:\\/\\/[^\\s<>()[\\]\"'`]+/g;\n/** Sentence punctuation that follows an address rather than belonging to it. */\nconst TRAILING_PUNCTUATION = /[.,;:!?'\"*_]+$/;\n/** The Spaces route an asset link opens (`/dashboard/spaces?asset_ids=…`). */\nconst SPACES_PATH = /\\/dashboard\\/spaces\\/?$/;\n/** Past this a tool result is not read for an asset: a created asset's answer is small. */\nconst MAX_TOOL_RESULT_CHARS = 20_000;\n\nfunction assetEntry(asset: Omit<CallRecapAsset, 'kind' | 'key'>): Entry {\n return {\n kind: 'asset',\n asset: { kind: 'asset', key: `asset:${asset.assetId}`, ...asset },\n };\n}\n\n/** `sheet` is the old spelling of `spreadsheet` in Spaces links. */\nfunction normalizeAssetType(value: string | null): string | null {\n if (!value) return null;\n return value === 'sheet' ? 'spreadsheet' : value;\n}\n\nfunction labelOf(raw: string): string {\n return raw.trim().replace(/^@/, '').trim();\n}\n\n/** The page's host and path, for a link that carries no label of its own. */\nfunction displayUrl(url: URL): string {\n const host = url.hostname.replace(/^www\\./, '');\n const path = url.pathname === '/' ? '' : url.pathname.replace(/\\/$/, '');\n return `${host}${path}`;\n}\n\n/** The base a relative Spaces link (`/dashboard/spaces?asset_ids=…`) resolves against; only its query is read. */\nconst RELATIVE_LINK_BASE = 'https://app.athenaintel.com';\n\n/** The assets an Athena link names, the web page a link names, or nothing. */\nfunction entriesOfHref(href: string, rawLabel: string): readonly Entry[] {\n const label = labelOf(rawLabel);\n let url: URL;\n try {\n url = new URL(href, href.startsWith('/') ? RELATIVE_LINK_BASE : undefined);\n } catch {\n return [];\n }\n if (url.protocol === 'mention:') {\n if (url.hostname !== 'asset') return [];\n const assetId = url.searchParams.get('id') ?? url.searchParams.get('asset_id');\n if (!assetId?.startsWith('asset_')) return [];\n return [\n assetEntry({\n assetId,\n title: label || (url.searchParams.get('name') ?? ''),\n assetType: normalizeAssetType(\n url.searchParams.get('asset_type') ?? url.searchParams.get('type'),\n ),\n href,\n }),\n ];\n }\n if (url.protocol !== 'https:' && url.protocol !== 'http:') return [];\n const assetIds = url.searchParams.get('asset_ids');\n if (assetIds && SPACES_PATH.test(url.pathname)) {\n const ids = [\n ...new Set(\n assetIds\n .split(',')\n .map((id) => id.trim())\n .filter((id) => id.startsWith('asset_')),\n ),\n ];\n const assetType = normalizeAssetType(\n url.searchParams.get('type') ??\n url.searchParams.get('assetType') ??\n url.searchParams.get('asset_type'),\n );\n // A link that opens several assets side by side names each of them: one\n // item per asset, each opening only its own asset.\n return ids.map((assetId) => {\n if (ids.length === 1) return assetEntry({ assetId, title: label, assetType, href });\n const single = new URL(url.toString());\n single.searchParams.set('asset_ids', assetId);\n return assetEntry({\n assetId,\n title: label,\n assetType,\n href: href.startsWith('/') ? `${single.pathname}${single.search}` : single.toString(),\n });\n });\n }\n // Any other relative link is a route inside the app, not a page the call named.\n if (href.startsWith('/')) return [];\n const shown = label && label !== href ? label : displayUrl(url);\n const key = `${url.origin}${url.pathname.replace(/\\/$/, '')}${url.search}`;\n return [\n {\n kind: 'link',\n link: { kind: 'link', key: `link:${key}`, url: url.toString(), label: shown },\n },\n ];\n}\n\n/** Fenced code blocks and inline code spans: an address shown as code is not a link the message names. */\nconst CODE = /```[\\s\\S]*?(?:```|$)|`[^`\\n]*`/g;\n\n/** The asset and web links a text part carries, in the order it carries them. */\nfunction textEntries(raw: string): readonly Entry[] {\n // An address (`https://`, `mention://`) or a relative Spaces link (`](/dashboard/…`).\n if (!raw.includes('://') && !raw.includes('](/')) return NO_ENTRIES;\n // Blanked to the same length, so the rest keeps its offsets (the order the part names things in).\n const text = raw.includes('`') ? raw.replace(CODE, (code) => ' '.repeat(code.length)) : raw;\n const found: { index: number; entry: Entry }[] = [];\n const covered: [number, number][] = [];\n for (const match of text.matchAll(MARKDOWN_LINK)) {\n const [whole, label = '', href = ''] = match;\n covered.push([match.index, match.index + whole.length]);\n // `![alt](src)` is an image the message shows, not a link it names.\n if (text[match.index - 1] === '!') continue;\n for (const entry of entriesOfHref(href, label)) found.push({ index: match.index, entry });\n }\n for (const match of text.matchAll(BARE_URL)) {\n const start = match.index;\n if (covered.some(([from, to]) => start >= from && start < to)) continue;\n const href = match[0].replace(TRAILING_PUNCTUATION, '');\n for (const entry of entriesOfHref(href, '')) found.push({ index: start, entry });\n }\n if (found.length === 0) return NO_ENTRIES;\n found.sort((a, b) => a.index - b.index);\n return Object.freeze(found.map(({ entry }) => entry));\n}\n\n/** The asset a finished tool call reports (`asset_id` with a `title`), the open call, or nothing. */\nfunction toolCallEntries(part: CallRecapPart): readonly Entry[] {\n const toolCallId = part.toolCallId;\n const toolName = part.toolName ?? 'tool';\n if (!toolCallId) return NO_ENTRIES;\n if (part.result === undefined) {\n return Object.freeze([{ kind: 'open-call', call: { toolCallId, toolName } }]);\n }\n // A running tool's interim payload is not its outcome: the asset counts once the tool finished.\n if (part.isPreliminary === true) return NO_ENTRIES;\n if (typeof part.result === 'string' && part.result.length > MAX_TOOL_RESULT_CHARS)\n return NO_ENTRIES;\n const record = resultRecord(part.result);\n if (!record) return NO_ENTRIES;\n const assetId = stringField(record, ['asset_id', 'assetId']);\n const title = stringField(record, ['title', 'asset_title', 'name']);\n if (!assetId?.startsWith('asset_') || !title) return NO_ENTRIES;\n const outcome = classifyToolResult({\n status: { type: 'complete' } satisfies ToolCallStatusLike,\n result: part.result,\n isError: part.isError,\n });\n if (outcome.state !== 'complete') return NO_ENTRIES;\n return Object.freeze([\n assetEntry({\n assetId,\n title: title.trim(),\n assetType: normalizeAssetType(stringField(record, ['asset_type', 'assetType'])),\n href: null,\n }),\n ]);\n}\n\nconst partEntriesCache = new WeakMap<object, readonly Entry[]>();\n\nfunction partEntries(part: CallRecapPart): readonly Entry[] {\n const cached = partEntriesCache.get(part);\n if (cached !== undefined) return cached;\n const entries =\n part.type === 'text' && typeof part.text === 'string'\n ? textEntries(part.text)\n : part.type === 'tool-call'\n ? toolCallEntries(part)\n : NO_ENTRIES;\n partEntriesCache.set(part, entries);\n return entries;\n}\n\n// ── Per-message facts ────────────────────────────────────────────────────────\n\ninterface MessageFacts {\n /** A user message outside any call: it ends the stretch before it. */\n readonly breaksCall: boolean;\n /** The first part that belongs to a call (a call turn's first part, a spoken assistant part, a voice update), or -1. */\n readonly callFrom: number;\n}\n\nconst factsCache = new WeakMap<object, MessageFacts>();\n\n/** Whether the converter bound a part to its wire entry (`isSpokenTurnTextPart` reads that entry). */\nfunction isBoundPart(part: CallRecapPart): boolean {\n return getExternalStoreMessages<unknown>(part as ThreadMessage['content'][number]).length > 0;\n}\n\nfunction messageFacts(message: CallRecapMessage): MessageFacts {\n const cached = factsCache.get(message);\n if (cached !== undefined) return cached;\n let facts: MessageFacts;\n if (message.role === 'user') {\n const call = isCallUserMessage(message);\n facts = { breaksCall: !call, callFrom: call ? 0 : -1 };\n } else if (message.role === 'assistant') {\n // A spoken turn, or the interim-update tool only a run the live model\n // delegated is given (`send_voice_update`): a delegated run still marks its\n // call when a windowed transcript has not loaded the turns before it.\n let callFrom = message.content.findIndex(\n (part) =>\n (part.type === 'text' && isSpokenTurnTextPart(part)) ||\n (part.type === 'tool-call' && part.toolName === VOICE_UPDATE_TOOL_NAME),\n );\n // A message no converter bound reads by its own source (one spoken turn).\n if (\n callFrom < 0 &&\n message.metadata?.custom?.source === VOICE_SOURCE_SPOKEN &&\n !message.content.some(isBoundPart)\n ) {\n callFrom = 0;\n }\n facts = { breaksCall: false, callFrom };\n } else {\n facts = { breaksCall: false, callFrom: -1 };\n }\n factsCache.set(message, facts);\n return facts;\n}\n\n// ── The recap at a row ───────────────────────────────────────────────────────\n\ninterface RecapMemo {\n /** The stretch's messages, start first, as last read. */\n readonly span: readonly CallRecapMessage[];\n readonly startPart: number;\n readonly trailing: boolean;\n readonly recap: CallRecap;\n}\n\n/** Keyed by the stretch's last message: a settled stretch answers from here without rebuilding. */\nconst recapMemo = new WeakMap<object, RecapMemo>();\n\n/** Equal recaps share one frozen object (bounded; a miss only costs a rebuild). */\nconst internedRecaps = new Map<string, CallRecap>();\nconst MAX_INTERNED_RECAPS = 200;\n\nfunction recapSignature(recap: CallRecap): string {\n return JSON.stringify([\n recap.endMessageId,\n recap.trailing,\n recap.assets.map((asset) => [asset.assetId, asset.title, asset.assetType, asset.href]),\n recap.links.map((link) => [link.url, link.label]),\n recap.openToolCalls.map((call) => [call.toolCallId, call.toolName]),\n ]);\n}\n\nfunction intern(recap: CallRecap): CallRecap {\n const signature = recapSignature(recap);\n const existing = internedRecaps.get(signature);\n if (existing !== undefined) {\n // Most recently used last, so the bound evicts the stalest.\n internedRecaps.delete(signature);\n internedRecaps.set(signature, existing);\n return existing;\n }\n const frozen: CallRecap = Object.freeze({\n ...recap,\n assets: Object.freeze([...recap.assets]),\n links: Object.freeze([...recap.links]),\n openToolCalls: Object.freeze([...recap.openToolCalls]),\n });\n internedRecaps.set(signature, frozen);\n if (internedRecaps.size > MAX_INTERNED_RECAPS) {\n const oldest = internedRecaps.keys().next().value;\n if (oldest !== undefined) internedRecaps.delete(oldest);\n }\n return frozen;\n}\n\nfunction sameSpan(a: readonly CallRecapMessage[], b: readonly CallRecapMessage[]): boolean {\n if (a.length !== b.length) return false;\n for (let index = 0; index < a.length; index += 1) {\n if (a[index] !== b[index]) return false;\n }\n return true;\n}\n\nfunction buildRecap({\n span,\n startPart,\n trailing,\n}: {\n span: readonly CallRecapMessage[];\n startPart: number;\n trailing: boolean;\n}): CallRecap {\n const assets = new Map<string, CallRecapAsset>();\n const links = new Map<string, CallRecapLink>();\n const openToolCalls = new Map<string, CallRecapOpenToolCall>();\n span.forEach((message, position) => {\n const parts = position === 0 ? message.content.slice(startPart) : message.content;\n for (const part of parts) {\n for (const entry of partEntries(part)) {\n if (entry.kind === 'asset') {\n const known = assets.get(entry.asset.assetId);\n if (!known) {\n assets.set(entry.asset.assetId, entry.asset);\n } else if (\n (!known.title && entry.asset.title) ||\n (!known.assetType && entry.asset.assetType)\n ) {\n // A later mention fills what the first one left out; the first keeps its place.\n assets.set(entry.asset.assetId, {\n ...known,\n title: known.title || entry.asset.title,\n assetType: known.assetType ?? entry.asset.assetType,\n href: known.href ?? entry.asset.href,\n });\n }\n } else if (entry.kind === 'link') {\n if (!links.has(entry.link.key)) links.set(entry.link.key, entry.link);\n } else if (!openToolCalls.has(entry.call.toolCallId)) {\n openToolCalls.set(entry.call.toolCallId, entry.call);\n }\n }\n }\n });\n const end = span[span.length - 1];\n return {\n endMessageId: end?.id ?? '',\n trailing,\n assets: [...assets.values()],\n links: [...links.values()],\n openToolCalls: [...openToolCalls.values()],\n };\n}\n\n/**\n * The recap of the call stretch that ends at `messages[index]`, or `null` when\n * no call stretch ends there. Cheap for every other row (it looks at the next\n * message only); the same frozen object while the recap is unchanged, so it is\n * safe as a `useAuiState` selector result.\n */\nexport function callRecapAfter(\n messages: readonly CallRecapMessage[],\n index: number,\n): CallRecap | null {\n const message = messages[index];\n if (message === undefined) return null;\n const next = messages[index + 1];\n // Only a user message outside a call ends a stretch; an assistant message never does.\n if (next !== undefined && (next.role !== 'user' || isCallUserMessage(next))) return null;\n\n // Walk back over this stretch only: to the user message outside a call that opened it.\n let start = -1;\n let startPart = -1;\n for (let position = index; position >= 0; position -= 1) {\n const candidate = messages[position];\n if (candidate === undefined) break;\n const facts = messageFacts(candidate);\n if (facts.breaksCall) break;\n if (facts.callFrom >= 0) {\n start = position;\n startPart = facts.callFrom;\n }\n }\n if (start < 0) return null;\n\n const span = messages.slice(start, index + 1);\n const trailing = next === undefined;\n const memo = recapMemo.get(message);\n if (\n memo !== undefined &&\n memo.startPart === startPart &&\n memo.trailing === trailing &&\n sameSpan(memo.span, span)\n ) {\n return memo.recap;\n }\n const recap = intern(buildRecap({ span, startPart, trailing }));\n recapMemo.set(message, { span, startPart, trailing, recap });\n return recap;\n}\n\n/** Whether a recap has anything to show once approvals are matched (see {@link callRecapItems}). */\nexport function callRecapHasItems(\n recap: CallRecap,\n awaitingApprovalToolCallIds: ReadonlySet<string> = EMPTY_IDS,\n): boolean {\n return (\n recap.assets.length > 0 ||\n recap.links.length > 0 ||\n recap.openToolCalls.some((call) => awaitingApprovalToolCallIds.has(call.toolCallId))\n );\n}\n\nconst EMPTY_IDS: ReadonlySet<string> = new Set();\n\n/**\n * The card's items: the call's tool calls still waiting for the person's\n * approval (the host's live request names them), then its assets, then its\n * web links. Pure; hosts memoize it on the recap and the id set.\n */\nexport function callRecapItems(\n recap: CallRecap,\n awaitingApprovalToolCallIds: ReadonlySet<string> = EMPTY_IDS,\n): CallRecapItem[] {\n const approvals: CallRecapApproval[] = recap.openToolCalls\n .filter((call) => awaitingApprovalToolCallIds.has(call.toolCallId))\n .map((call) => ({\n kind: 'approval',\n key: `approval:${call.toolCallId}`,\n toolCallId: call.toolCallId,\n toolName: call.toolName,\n }));\n return [...approvals, ...recap.assets, ...recap.links];\n}\n\n/** The card's heading, worded once for every app. */\nexport const CALL_RECAP_TITLE = 'From the call';\n\n/** An approval item's copy, worded once for every app. */\nexport const CALL_RECAP_APPROVAL_LABEL = 'Waiting for your approval';\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"call-recap.js","sources":["../../../../agent-runtime/src/call-recap.ts"],"sourcesContent":["/**\n * \"From the call\": what a voice call produced or mentioned, gathered where the\n * call's stretch of the transcript ends, so a person who talked to the agent\n * and later reads the conversation as text (on the same app or another) finds\n * the call's assets, links and open approvals in one place.\n *\n * Everything here is read from the transcript every app already renders, and\n * nothing is invented: an asset item is an asset link or `@`-mention in a\n * message, or a tool result that names the asset it made or read (`asset_id`\n * with a `title`); a link item is a web link in a message; an approval item is\n * a tool call of the call's that has no result yet, shown only while the\n * host's live approval request still names it (the presenter's\n * {@link callRecapItems}).\n *\n * **Where a call is in the transcript.** The host marks every turn a call\n * commits (`metadata.custom.source`, see `voice-presentation.ts`): spoken turns\n * (`voice`), messages typed into the call (`text`, id `voicetext_…`) and the\n * tasks the live model handed the thread (`voice_delegation`, id\n * `voicetask_…`). A typed message sent outside a call carries none of these. So\n * a call's stretch starts at its first marked turn and runs, through the\n * assistant work in between (the delegated runs), up to the next user message\n * that is not a call turn, or the end of the transcript. A spoken assistant\n * turn the converter joined onto the end of an earlier run's message starts\n * the stretch at that part, so the earlier run's artifacts stay out. A\n * delegated run's `send_voice_update` call (a tool only voice-delegated runs\n * get) marks the call too, so a windowed transcript whose loaded tail starts\n * inside a long delegated run still finds it.\n *\n * Two calls with no typed message between them read as one stretch: the\n * transcript does not say where one ended and the next began (the host keeps\n * no per-call marker), so their card collects both. A stretch that ends the\n * transcript may still be live; the presenter shows it once the host reports\n * no voice session (`state.voice` is null), see {@link mayHaveLiveCall}.\n *\n * **Cost.** Every message row asks {@link callRecapAfter} on every transcript\n * update. A row that does not end a stretch answers after looking at the next\n * message; a row that does walks back over its own stretch only. What a message\n * and each of its parts contribute is memoized per object (statewire replaces a\n * changed message and assistant-ui keeps every unchanged part), and an equal\n * recap is the same frozen object, so a row selecting it re-renders only when\n * the card's contents change. The transcript is never re-scanned per delta.\n */\n\nimport { getExternalStoreMessages, type ThreadMessage } from '@assistant-ui/core';\nimport { isDeepAgentStateHydrated, readDeepAgentStreamValues } from './statewire-state';\nimport {\n classifyToolResult,\n resultRecord,\n stringField,\n type ToolCallStatusLike,\n} from './tool-result';\nimport {\n isSpokenTurnTextPart,\n VOICE_DELEGATION_ID_PREFIX,\n VOICE_UPDATE_TOOL_NAME,\n VOICE_SOURCE_DELEGATION,\n VOICE_SOURCE_SPOKEN,\n VOICE_SOURCE_TYPED,\n VOICE_TYPED_ID_PREFIX,\n} from './voice-presentation';\n\nexport { VOICE_TYPED_ID_PREFIX } from './voice-presentation';\n\n/** An asset the call created, read or linked. */\nexport interface CallRecapAsset {\n readonly kind: 'asset';\n /** Unique within a recap. */\n readonly key: string;\n readonly assetId: string;\n /** What the transcript calls it (the link's label, or the tool result's title); may be empty. */\n readonly title: string;\n /** The asset type the link or tool named (`document`, `spreadsheet`, …), when it named one. */\n readonly assetType: string | null;\n /** The link the transcript carried for it, when it came from a link. */\n readonly href: string | null;\n}\n\n/** A web page the call linked. */\nexport interface CallRecapLink {\n readonly kind: 'link';\n readonly key: string;\n readonly url: string;\n /** The link's own label, else the page's host and path. */\n readonly label: string;\n}\n\n/** A tool call of the call's that is waiting for the person's approval. */\nexport interface CallRecapApproval {\n readonly kind: 'approval';\n readonly key: string;\n readonly toolCallId: string;\n readonly toolName: string;\n}\n\nexport type CallRecapItem = CallRecapAsset | CallRecapLink | CallRecapApproval;\n\n/** A tool call in the stretch with no result yet: an approval while the host's request names it. */\nexport interface CallRecapOpenToolCall {\n readonly toolCallId: string;\n readonly toolName: string;\n}\n\nexport interface CallRecap {\n /** The message the stretch ends at; the card renders right after it. */\n readonly endMessageId: string;\n /** The stretch ends the transcript, so the call may still be live. */\n readonly trailing: boolean;\n /** Assets, in the order the stretch first mentioned them. */\n readonly assets: readonly CallRecapAsset[];\n /** Web links, in the order the stretch first mentioned them. */\n readonly links: readonly CallRecapLink[];\n /** Result-less tool calls, in transcript order. */\n readonly openToolCalls: readonly CallRecapOpenToolCall[];\n}\n\n/** The fields of an assistant-ui thread message this reads (structural, so any runtime's message fits). */\nexport interface CallRecapMessage {\n readonly id: string;\n readonly role: string;\n readonly content: readonly CallRecapPart[];\n readonly metadata?: { readonly custom?: Record<string, unknown> } | undefined;\n}\n\n/** The fields of a message part this reads. */\nexport interface CallRecapPart {\n readonly type: string;\n readonly text?: string;\n readonly toolCallId?: string;\n readonly toolName?: string;\n readonly result?: unknown;\n readonly isError?: boolean;\n /** An interim result a running tool published (`markPreliminaryToolResults`); the real one has not arrived. */\n readonly isPreliminary?: boolean;\n readonly status?: unknown;\n}\n\nconst CALL_SOURCES: ReadonlySet<unknown> = new Set([\n VOICE_SOURCE_SPOKEN,\n VOICE_SOURCE_TYPED,\n VOICE_SOURCE_DELEGATION,\n]);\n\n/**\n * Whether a user message is one of a call's turns: spoken, typed into the call,\n * or a task the live model delegated (by source, or by id on hosts that drop\n * the command's metadata).\n */\nexport function isCallUserMessage(\n message: Pick<CallRecapMessage, 'id' | 'role' | 'metadata'>,\n): boolean {\n if (message.role !== 'user') return false;\n if (CALL_SOURCES.has(message.metadata?.custom?.source)) return true;\n return (\n message.id.startsWith(VOICE_DELEGATION_ID_PREFIX) ||\n message.id.startsWith(VOICE_TYPED_ID_PREFIX)\n );\n}\n\n/**\n * Whether a stretch that ends the transcript may still be a live call: the\n * host reports a voice session (`state.voice`), or its state has not loaded\n * yet. A primitive read of the raw statewire state, safe as a selector.\n */\nexport function mayHaveLiveCall(state: unknown): boolean {\n if (!isDeepAgentStateHydrated(state)) return true;\n const voice: unknown = readDeepAgentStreamValues(state).voice;\n return voice !== null && voice !== undefined;\n}\n\n// ── Per-part entries ─────────────────────────────────────────────────────────\n\ntype Entry =\n | { readonly kind: 'asset'; readonly asset: CallRecapAsset }\n | { readonly kind: 'link'; readonly link: CallRecapLink }\n | { readonly kind: 'open-call'; readonly call: CallRecapOpenToolCall };\n\nconst NO_ENTRIES: readonly Entry[] = Object.freeze([]);\n\n/** `[label](href)` with an optional `\"title\"`; one level of brackets in the label, one of parens in the href. */\nconst MARKDOWN_LINK =\n /\\[((?:[^[\\]\\n]|\\[[^[\\]\\n]*\\])*)\\]\\(\\s*<?((?:[^()\\s<>]|\\([^()\\s]*\\))+)>?(?:\\s+\"[^\"\\n]*\")?\\s*\\)/g;\n/** A bare web address in prose. */\nconst BARE_URL = /\\bhttps?:\\/\\/[^\\s<>()[\\]\"'`]+/g;\n/** Sentence punctuation that follows an address rather than belonging to it. */\nconst TRAILING_PUNCTUATION = /[.,;:!?'\"*_]+$/;\n/** The Spaces route an asset link opens (`/dashboard/spaces?asset_ids=…`). */\nconst SPACES_PATH = /\\/dashboard\\/spaces\\/?$/;\n/** Past this a tool result is not read for an asset: a created asset's answer is small. */\nconst MAX_TOOL_RESULT_CHARS = 20_000;\n\nfunction assetEntry(asset: Omit<CallRecapAsset, 'kind' | 'key'>): Entry {\n return {\n kind: 'asset',\n asset: { kind: 'asset', key: `asset:${asset.assetId}`, ...asset },\n };\n}\n\n/** `sheet` is the old spelling of `spreadsheet` in Spaces links. */\nfunction normalizeAssetType(value: string | null): string | null {\n if (!value) return null;\n return value === 'sheet' ? 'spreadsheet' : value;\n}\n\nfunction labelOf(raw: string): string {\n return raw.trim().replace(/^@/, '').trim();\n}\n\n/** The page's host and path, for a link that carries no label of its own. */\nfunction displayUrl(url: URL): string {\n const host = url.hostname.replace(/^www\\./, '');\n const path = url.pathname === '/' ? '' : url.pathname.replace(/\\/$/, '');\n return `${host}${path}`;\n}\n\n/** The base a relative Spaces link (`/dashboard/spaces?asset_ids=…`) resolves against; only its query is read. */\nconst RELATIVE_LINK_BASE = 'https://app.athenaintel.com';\n\n/** The assets an Athena link names, the web page a link names, or nothing. */\nfunction entriesOfHref(href: string, rawLabel: string): readonly Entry[] {\n const label = labelOf(rawLabel);\n let url: URL;\n try {\n url = new URL(href, href.startsWith('/') ? RELATIVE_LINK_BASE : undefined);\n } catch {\n return [];\n }\n if (url.protocol === 'mention:') {\n if (url.hostname !== 'asset') return [];\n const assetId = url.searchParams.get('id') ?? url.searchParams.get('asset_id');\n if (!assetId?.startsWith('asset_')) return [];\n return [\n assetEntry({\n assetId,\n title: label || (url.searchParams.get('name') ?? ''),\n assetType: normalizeAssetType(\n url.searchParams.get('asset_type') ?? url.searchParams.get('type'),\n ),\n href,\n }),\n ];\n }\n if (url.protocol !== 'https:' && url.protocol !== 'http:') return [];\n const assetIds = url.searchParams.get('asset_ids');\n if (assetIds && SPACES_PATH.test(url.pathname)) {\n const ids = [\n ...new Set(\n assetIds\n .split(',')\n .map((id) => id.trim())\n .filter((id) => id.startsWith('asset_')),\n ),\n ];\n const assetType = normalizeAssetType(\n url.searchParams.get('type') ??\n url.searchParams.get('assetType') ??\n url.searchParams.get('asset_type'),\n );\n // A link that opens several assets side by side names each of them: one\n // item per asset, each opening only its own asset.\n return ids.map((assetId) => {\n if (ids.length === 1) return assetEntry({ assetId, title: label, assetType, href });\n const single = new URL(url.toString());\n single.searchParams.set('asset_ids', assetId);\n return assetEntry({\n assetId,\n title: label,\n assetType,\n href: href.startsWith('/') ? `${single.pathname}${single.search}` : single.toString(),\n });\n });\n }\n // Any other relative link is a route inside the app, not a page the call named.\n if (href.startsWith('/')) return [];\n const shown = label && label !== href ? label : displayUrl(url);\n const key = `${url.origin}${url.pathname.replace(/\\/$/, '')}${url.search}`;\n return [\n {\n kind: 'link',\n link: { kind: 'link', key: `link:${key}`, url: url.toString(), label: shown },\n },\n ];\n}\n\n/** Fenced code blocks and inline code spans: an address shown as code is not a link the message names. */\nconst CODE = /```[\\s\\S]*?(?:```|$)|`[^`\\n]*`/g;\n\n/** The asset and web links a text part carries, in the order it carries them. */\nfunction textEntries(raw: string): readonly Entry[] {\n // An address (`https://`, `mention://`) or a relative Spaces link (`](/dashboard/…`).\n if (!raw.includes('://') && !raw.includes('](/')) return NO_ENTRIES;\n // Blanked to the same length, so the rest keeps its offsets (the order the part names things in).\n const text = raw.includes('`') ? raw.replace(CODE, (code) => ' '.repeat(code.length)) : raw;\n const found: { index: number; entry: Entry }[] = [];\n const covered: [number, number][] = [];\n for (const match of text.matchAll(MARKDOWN_LINK)) {\n const [whole, label = '', href = ''] = match;\n covered.push([match.index, match.index + whole.length]);\n // `![alt](src)` is an image the message shows, not a link it names.\n if (text[match.index - 1] === '!') continue;\n for (const entry of entriesOfHref(href, label)) found.push({ index: match.index, entry });\n }\n for (const match of text.matchAll(BARE_URL)) {\n const start = match.index;\n if (covered.some(([from, to]) => start >= from && start < to)) continue;\n const href = match[0].replace(TRAILING_PUNCTUATION, '');\n for (const entry of entriesOfHref(href, '')) found.push({ index: start, entry });\n }\n if (found.length === 0) return NO_ENTRIES;\n found.sort((a, b) => a.index - b.index);\n return Object.freeze(found.map(({ entry }) => entry));\n}\n\n/** The asset a finished tool call reports (`asset_id` with a `title`), the open call, or nothing. */\nfunction toolCallEntries(part: CallRecapPart): readonly Entry[] {\n const toolCallId = part.toolCallId;\n const toolName = part.toolName ?? 'tool';\n if (!toolCallId) return NO_ENTRIES;\n if (part.result === undefined) {\n return Object.freeze([{ kind: 'open-call', call: { toolCallId, toolName } }]);\n }\n // A running tool's interim payload is not its outcome: the asset counts once the tool finished.\n if (part.isPreliminary === true) return NO_ENTRIES;\n if (typeof part.result === 'string' && part.result.length > MAX_TOOL_RESULT_CHARS)\n return NO_ENTRIES;\n const record = resultRecord(part.result);\n if (!record) return NO_ENTRIES;\n const assetId = stringField(record, ['asset_id', 'assetId']);\n const title = stringField(record, ['title', 'asset_title', 'name']);\n if (!assetId?.startsWith('asset_') || !title) return NO_ENTRIES;\n const outcome = classifyToolResult({\n status: { type: 'complete' } satisfies ToolCallStatusLike,\n result: part.result,\n isError: part.isError,\n });\n if (outcome.state !== 'complete') return NO_ENTRIES;\n return Object.freeze([\n assetEntry({\n assetId,\n title: title.trim(),\n assetType: normalizeAssetType(stringField(record, ['asset_type', 'assetType'])),\n href: null,\n }),\n ]);\n}\n\nconst partEntriesCache = new WeakMap<object, readonly Entry[]>();\n\nfunction partEntries(part: CallRecapPart): readonly Entry[] {\n const cached = partEntriesCache.get(part);\n if (cached !== undefined) return cached;\n const entries =\n part.type === 'text' && typeof part.text === 'string'\n ? textEntries(part.text)\n : part.type === 'tool-call'\n ? toolCallEntries(part)\n : NO_ENTRIES;\n partEntriesCache.set(part, entries);\n return entries;\n}\n\n// ── Per-message facts ────────────────────────────────────────────────────────\n\ninterface MessageFacts {\n /** A user message outside any call: it ends the stretch before it. */\n readonly breaksCall: boolean;\n /** The first part that belongs to a call (a call turn's first part, a spoken assistant part, a voice update), or -1. */\n readonly callFrom: number;\n}\n\nconst factsCache = new WeakMap<object, MessageFacts>();\n\n/** Whether the converter bound a part to its wire entry (`isSpokenTurnTextPart` reads that entry). */\nfunction isBoundPart(part: CallRecapPart): boolean {\n return getExternalStoreMessages<unknown>(part as ThreadMessage['content'][number]).length > 0;\n}\n\nfunction messageFacts(message: CallRecapMessage): MessageFacts {\n const cached = factsCache.get(message);\n if (cached !== undefined) return cached;\n let facts: MessageFacts;\n if (message.role === 'user') {\n const call = isCallUserMessage(message);\n facts = { breaksCall: !call, callFrom: call ? 0 : -1 };\n } else if (message.role === 'assistant') {\n // A spoken turn, or the interim-update tool only a run the live model\n // delegated is given (`send_voice_update`): a delegated run still marks its\n // call when a windowed transcript has not loaded the turns before it.\n let callFrom = message.content.findIndex(\n (part) =>\n (part.type === 'text' && isSpokenTurnTextPart(part)) ||\n (part.type === 'tool-call' && part.toolName === VOICE_UPDATE_TOOL_NAME),\n );\n // A message no converter bound reads by its own source (one spoken turn).\n if (\n callFrom < 0 &&\n message.metadata?.custom?.source === VOICE_SOURCE_SPOKEN &&\n !message.content.some(isBoundPart)\n ) {\n callFrom = 0;\n }\n facts = { breaksCall: false, callFrom };\n } else {\n facts = { breaksCall: false, callFrom: -1 };\n }\n factsCache.set(message, facts);\n return facts;\n}\n\n// ── The recap at a row ───────────────────────────────────────────────────────\n\ninterface RecapMemo {\n /** The stretch's messages, start first, as last read. */\n readonly span: readonly CallRecapMessage[];\n readonly startPart: number;\n readonly trailing: boolean;\n readonly recap: CallRecap;\n}\n\n/** Keyed by the stretch's last message: a settled stretch answers from here without rebuilding. */\nconst recapMemo = new WeakMap<object, RecapMemo>();\n\n/** Equal recaps share one frozen object (bounded; a miss only costs a rebuild). */\nconst internedRecaps = new Map<string, CallRecap>();\nconst MAX_INTERNED_RECAPS = 200;\n\nfunction recapSignature(recap: CallRecap): string {\n return JSON.stringify([\n recap.endMessageId,\n recap.trailing,\n recap.assets.map((asset) => [asset.assetId, asset.title, asset.assetType, asset.href]),\n recap.links.map((link) => [link.url, link.label]),\n recap.openToolCalls.map((call) => [call.toolCallId, call.toolName]),\n ]);\n}\n\nfunction intern(recap: CallRecap): CallRecap {\n const signature = recapSignature(recap);\n const existing = internedRecaps.get(signature);\n if (existing !== undefined) {\n // Most recently used last, so the bound evicts the stalest.\n internedRecaps.delete(signature);\n internedRecaps.set(signature, existing);\n return existing;\n }\n const frozen: CallRecap = Object.freeze({\n ...recap,\n assets: Object.freeze([...recap.assets]),\n links: Object.freeze([...recap.links]),\n openToolCalls: Object.freeze([...recap.openToolCalls]),\n });\n internedRecaps.set(signature, frozen);\n if (internedRecaps.size > MAX_INTERNED_RECAPS) {\n const oldest = internedRecaps.keys().next().value;\n if (oldest !== undefined) internedRecaps.delete(oldest);\n }\n return frozen;\n}\n\nfunction sameSpan(a: readonly CallRecapMessage[], b: readonly CallRecapMessage[]): boolean {\n if (a.length !== b.length) return false;\n for (let index = 0; index < a.length; index += 1) {\n if (a[index] !== b[index]) return false;\n }\n return true;\n}\n\nfunction buildRecap({\n span,\n startPart,\n trailing,\n}: {\n span: readonly CallRecapMessage[];\n startPart: number;\n trailing: boolean;\n}): CallRecap {\n const assets = new Map<string, CallRecapAsset>();\n const links = new Map<string, CallRecapLink>();\n const openToolCalls = new Map<string, CallRecapOpenToolCall>();\n span.forEach((message, position) => {\n const parts = position === 0 ? message.content.slice(startPart) : message.content;\n for (const part of parts) {\n for (const entry of partEntries(part)) {\n if (entry.kind === 'asset') {\n const known = assets.get(entry.asset.assetId);\n if (!known) {\n assets.set(entry.asset.assetId, entry.asset);\n } else if (\n (!known.title && entry.asset.title) ||\n (!known.assetType && entry.asset.assetType)\n ) {\n // A later mention fills what the first one left out; the first keeps its place.\n assets.set(entry.asset.assetId, {\n ...known,\n title: known.title || entry.asset.title,\n assetType: known.assetType ?? entry.asset.assetType,\n href: known.href ?? entry.asset.href,\n });\n }\n } else if (entry.kind === 'link') {\n if (!links.has(entry.link.key)) links.set(entry.link.key, entry.link);\n } else if (!openToolCalls.has(entry.call.toolCallId)) {\n openToolCalls.set(entry.call.toolCallId, entry.call);\n }\n }\n }\n });\n const end = span[span.length - 1];\n return {\n endMessageId: end?.id ?? '',\n trailing,\n assets: [...assets.values()],\n links: [...links.values()],\n openToolCalls: [...openToolCalls.values()],\n };\n}\n\n/**\n * The recap of the call stretch that ends at `messages[index]`, or `null` when\n * no call stretch ends there. Cheap for every other row (it looks at the next\n * message only); the same frozen object while the recap is unchanged, so it is\n * safe as a `useAuiState` selector result.\n */\nexport function callRecapAfter(\n messages: readonly CallRecapMessage[],\n index: number,\n): CallRecap | null {\n const message = messages[index];\n if (message === undefined) return null;\n const next = messages[index + 1];\n // Only a user message outside a call ends a stretch; an assistant message never does.\n if (next !== undefined && (next.role !== 'user' || isCallUserMessage(next))) return null;\n\n // Walk back over this stretch only: to the user message outside a call that opened it.\n let start = -1;\n let startPart = -1;\n for (let position = index; position >= 0; position -= 1) {\n const candidate = messages[position];\n if (candidate === undefined) break;\n const facts = messageFacts(candidate);\n if (facts.breaksCall) break;\n if (facts.callFrom >= 0) {\n start = position;\n startPart = facts.callFrom;\n }\n }\n if (start < 0) return null;\n\n const span = messages.slice(start, index + 1);\n const trailing = next === undefined;\n const memo = recapMemo.get(message);\n if (\n memo !== undefined &&\n memo.startPart === startPart &&\n memo.trailing === trailing &&\n sameSpan(memo.span, span)\n ) {\n return memo.recap;\n }\n const recap = intern(buildRecap({ span, startPart, trailing }));\n recapMemo.set(message, { span, startPart, trailing, recap });\n return recap;\n}\n\n/** Whether a recap has anything to show once approvals are matched (see {@link callRecapItems}). */\nexport function callRecapHasItems(\n recap: CallRecap,\n awaitingApprovalToolCallIds: ReadonlySet<string> = EMPTY_IDS,\n): boolean {\n return (\n recap.assets.length > 0 ||\n recap.links.length > 0 ||\n recap.openToolCalls.some((call) => awaitingApprovalToolCallIds.has(call.toolCallId))\n );\n}\n\nconst EMPTY_IDS: ReadonlySet<string> = new Set();\n\n/**\n * The card's items: the call's tool calls still waiting for the person's\n * approval (the host's live request names them), then its assets, then its\n * web links. Pure; hosts memoize it on the recap and the id set.\n */\nexport function callRecapItems(\n recap: CallRecap,\n awaitingApprovalToolCallIds: ReadonlySet<string> = EMPTY_IDS,\n): CallRecapItem[] {\n const approvals: CallRecapApproval[] = recap.openToolCalls\n .filter((call) => awaitingApprovalToolCallIds.has(call.toolCallId))\n .map((call) => ({\n kind: 'approval',\n key: `approval:${call.toolCallId}`,\n toolCallId: call.toolCallId,\n toolName: call.toolName,\n }));\n return [...approvals, ...recap.assets, ...recap.links];\n}\n\n/** The card's heading, worded once for every app. */\nexport const CALL_RECAP_TITLE = 'From the call';\n\n/** An approval item's copy, worded once for every app. */\nexport const CALL_RECAP_APPROVAL_LABEL = 'Waiting for your approval';\n"],"names":[],"mappings":";;;;AAwIA,MAAM,mCAAyC,IAAI;AAAA,EACjD;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAOM,SAAS,kBACd,SACS;;AACT,MAAI,QAAQ,SAAS,OAAQ,QAAO;AACpC,MAAI,aAAa,KAAI,mBAAQ,aAAR,mBAAkB,WAAlB,mBAA0B,MAAM,EAAG,QAAO;AAC/D,SACE,QAAQ,GAAG,WAAW,0BAA0B,KAChD,QAAQ,GAAG,WAAW,qBAAqB;AAE/C;AAOO,SAAS,gBAAgB,OAAyB;AACvD,MAAI,CAAC,yBAAyB,KAAK,EAAG,QAAO;AAC7C,QAAM,QAAiB,0BAA0B,KAAK,EAAE;AACxD,SAAO,UAAU,QAAQ,UAAU;AACrC;AASA,MAAM,aAA+B,OAAO,OAAO,EAAE;AAGrD,MAAM,gBACJ;AAEF,MAAM,WAAW;AAEjB,MAAM,uBAAuB;AAE7B,MAAM,cAAc;AAEpB,MAAM,wBAAwB;AAE9B,SAAS,WAAW,OAAoD;AACtE,SAAO;AAAA,IACL,MAAM;AAAA,IACN,OAAO,EAAE,MAAM,SAAS,KAAK,SAAS,MAAM,OAAO,IAAI,GAAG,MAAA;AAAA,EAAM;AAEpE;AAGA,SAAS,mBAAmB,OAAqC;AAC/D,MAAI,CAAC,MAAO,QAAO;AACnB,SAAO,UAAU,UAAU,gBAAgB;AAC7C;AAEA,SAAS,QAAQ,KAAqB;AACpC,SAAO,IAAI,OAAO,QAAQ,MAAM,EAAE,EAAE,KAAA;AACtC;AAGA,SAAS,WAAW,KAAkB;AACpC,QAAM,OAAO,IAAI,SAAS,QAAQ,UAAU,EAAE;AAC9C,QAAM,OAAO,IAAI,aAAa,MAAM,KAAK,IAAI,SAAS,QAAQ,OAAO,EAAE;AACvE,SAAO,GAAG,IAAI,GAAG,IAAI;AACvB;AAGA,MAAM,qBAAqB;AAG3B,SAAS,cAAc,MAAc,UAAoC;AACvE,QAAM,QAAQ,QAAQ,QAAQ;AAC9B,MAAI;AACJ,MAAI;AACF,UAAM,IAAI,IAAI,MAAM,KAAK,WAAW,GAAG,IAAI,qBAAqB,MAAS;AAAA,EAC3E,QAAQ;AACN,WAAO,CAAA;AAAA,EACT;AACA,MAAI,IAAI,aAAa,YAAY;AAC/B,QAAI,IAAI,aAAa,QAAS,QAAO,CAAA;AACrC,UAAM,UAAU,IAAI,aAAa,IAAI,IAAI,KAAK,IAAI,aAAa,IAAI,UAAU;AAC7E,QAAI,EAAC,mCAAS,WAAW,mBAAkB,CAAA;AAC3C,WAAO;AAAA,MACL,WAAW;AAAA,QACT;AAAA,QACA,OAAO,UAAU,IAAI,aAAa,IAAI,MAAM,KAAK;AAAA,QACjD,WAAW;AAAA,UACT,IAAI,aAAa,IAAI,YAAY,KAAK,IAAI,aAAa,IAAI,MAAM;AAAA,QAAA;AAAA,QAEnE;AAAA,MAAA,CACD;AAAA,IAAA;AAAA,EAEL;AACA,MAAI,IAAI,aAAa,YAAY,IAAI,aAAa,gBAAgB,CAAA;AAClE,QAAM,WAAW,IAAI,aAAa,IAAI,WAAW;AACjD,MAAI,YAAY,YAAY,KAAK,IAAI,QAAQ,GAAG;AAC9C,UAAM,MAAM;AAAA,MACV,GAAG,IAAI;AAAA,QACL,SACG,MAAM,GAAG,EACT,IAAI,CAAC,OAAO,GAAG,KAAA,CAAM,EACrB,OAAO,CAAC,OAAO,GAAG,WAAW,QAAQ,CAAC;AAAA,MAAA;AAAA,IAC3C;AAEF,UAAM,YAAY;AAAA,MAChB,IAAI,aAAa,IAAI,MAAM,KACzB,IAAI,aAAa,IAAI,WAAW,KAChC,IAAI,aAAa,IAAI,YAAY;AAAA,IAAA;AAIrC,WAAO,IAAI,IAAI,CAAC,YAAY;AAC1B,UAAI,IAAI,WAAW,EAAG,QAAO,WAAW,EAAE,SAAS,OAAO,OAAO,WAAW,KAAA,CAAM;AAClF,YAAM,SAAS,IAAI,IAAI,IAAI,UAAU;AACrC,aAAO,aAAa,IAAI,aAAa,OAAO;AAC5C,aAAO,WAAW;AAAA,QAChB;AAAA,QACA,OAAO;AAAA,QACP;AAAA,QACA,MAAM,KAAK,WAAW,GAAG,IAAI,GAAG,OAAO,QAAQ,GAAG,OAAO,MAAM,KAAK,OAAO,SAAA;AAAA,MAAS,CACrF;AAAA,IACH,CAAC;AAAA,EACH;AAEA,MAAI,KAAK,WAAW,GAAG,UAAU,CAAA;AACjC,QAAM,QAAQ,SAAS,UAAU,OAAO,QAAQ,WAAW,GAAG;AAC9D,QAAM,MAAM,GAAG,IAAI,MAAM,GAAG,IAAI,SAAS,QAAQ,OAAO,EAAE,CAAC,GAAG,IAAI,MAAM;AACxE,SAAO;AAAA,IACL;AAAA,MACE,MAAM;AAAA,MACN,MAAM,EAAE,MAAM,QAAQ,KAAK,QAAQ,GAAG,IAAI,KAAK,IAAI,SAAA,GAAY,OAAO,MAAA;AAAA,IAAM;AAAA,EAC9E;AAEJ;AAGA,MAAM,OAAO;AAGb,SAAS,YAAY,KAA+B;AAElD,MAAI,CAAC,IAAI,SAAS,KAAK,KAAK,CAAC,IAAI,SAAS,KAAK,EAAG,QAAO;AAEzD,QAAM,OAAO,IAAI,SAAS,GAAG,IAAI,IAAI,QAAQ,MAAM,CAAC,SAAS,IAAI,OAAO,KAAK,MAAM,CAAC,IAAI;AACxF,QAAM,QAA2C,CAAA;AACjD,QAAM,UAA8B,CAAA;AACpC,aAAW,SAAS,KAAK,SAAS,aAAa,GAAG;AAChD,UAAM,CAAC,OAAO,QAAQ,IAAI,OAAO,EAAE,IAAI;AACvC,YAAQ,KAAK,CAAC,MAAM,OAAO,MAAM,QAAQ,MAAM,MAAM,CAAC;AAEtD,QAAI,KAAK,MAAM,QAAQ,CAAC,MAAM,IAAK;AACnC,eAAW,SAAS,cAAc,MAAM,KAAK,EAAG,OAAM,KAAK,EAAE,OAAO,MAAM,OAAO,MAAA,CAAO;AAAA,EAC1F;AACA,aAAW,SAAS,KAAK,SAAS,QAAQ,GAAG;AAC3C,UAAM,QAAQ,MAAM;AACpB,QAAI,QAAQ,KAAK,CAAC,CAAC,MAAM,EAAE,MAAM,SAAS,QAAQ,QAAQ,EAAE,EAAG;AAC/D,UAAM,OAAO,MAAM,CAAC,EAAE,QAAQ,sBAAsB,EAAE;AACtD,eAAW,SAAS,cAAc,MAAM,EAAE,EAAG,OAAM,KAAK,EAAE,OAAO,OAAO,MAAA,CAAO;AAAA,EACjF;AACA,MAAI,MAAM,WAAW,EAAG,QAAO;AAC/B,QAAM,KAAK,CAAC,GAAG,MAAM,EAAE,QAAQ,EAAE,KAAK;AACtC,SAAO,OAAO,OAAO,MAAM,IAAI,CAAC,EAAE,MAAA,MAAY,KAAK,CAAC;AACtD;AAGA,SAAS,gBAAgB,MAAuC;AAC9D,QAAM,aAAa,KAAK;AACxB,QAAM,WAAW,KAAK,YAAY;AAClC,MAAI,CAAC,WAAY,QAAO;AACxB,MAAI,KAAK,WAAW,QAAW;AAC7B,WAAO,OAAO,OAAO,CAAC,EAAE,MAAM,aAAa,MAAM,EAAE,YAAY,SAAA,EAAS,CAAG,CAAC;AAAA,EAC9E;AAEA,MAAI,KAAK,kBAAkB,KAAM,QAAO;AACxC,MAAI,OAAO,KAAK,WAAW,YAAY,KAAK,OAAO,SAAS;AAC1D,WAAO;AACT,QAAM,SAAS,aAAa,KAAK,MAAM;AACvC,MAAI,CAAC,OAAQ,QAAO;AACpB,QAAM,UAAU,YAAY,QAAQ,CAAC,YAAY,SAAS,CAAC;AAC3D,QAAM,QAAQ,YAAY,QAAQ,CAAC,SAAS,eAAe,MAAM,CAAC;AAClE,MAAI,EAAC,mCAAS,WAAW,cAAa,CAAC,MAAO,QAAO;AACrD,QAAM,UAAU,mBAAmB;AAAA,IACjC,QAAQ,EAAE,MAAM,WAAA;AAAA,IAChB,QAAQ,KAAK;AAAA,IACb,SAAS,KAAK;AAAA,EAAA,CACf;AACD,MAAI,QAAQ,UAAU,WAAY,QAAO;AACzC,SAAO,OAAO,OAAO;AAAA,IACnB,WAAW;AAAA,MACT;AAAA,MACA,OAAO,MAAM,KAAA;AAAA,MACb,WAAW,mBAAmB,YAAY,QAAQ,CAAC,cAAc,WAAW,CAAC,CAAC;AAAA,MAC9E,MAAM;AAAA,IAAA,CACP;AAAA,EAAA,CACF;AACH;AAEA,MAAM,uCAAuB,QAAA;AAE7B,SAAS,YAAY,MAAuC;AAC1D,QAAM,SAAS,iBAAiB,IAAI,IAAI;AACxC,MAAI,WAAW,OAAW,QAAO;AACjC,QAAM,UACJ,KAAK,SAAS,UAAU,OAAO,KAAK,SAAS,WACzC,YAAY,KAAK,IAAI,IACrB,KAAK,SAAS,cACZ,gBAAgB,IAAI,IACpB;AACR,mBAAiB,IAAI,MAAM,OAAO;AAClC,SAAO;AACT;AAWA,MAAM,iCAAiB,QAAA;AAGvB,SAAS,YAAY,MAA8B;AACjD,SAAO,yBAAkC,IAAwC,EAAE,SAAS;AAC9F;AAEA,SAAS,aAAa,SAAyC;;AAC7D,QAAM,SAAS,WAAW,IAAI,OAAO;AACrC,MAAI,WAAW,OAAW,QAAO;AACjC,MAAI;AACJ,MAAI,QAAQ,SAAS,QAAQ;AAC3B,UAAM,OAAO,kBAAkB,OAAO;AACtC,YAAQ,EAAE,YAAY,CAAC,MAAM,UAAU,OAAO,IAAI,GAAA;AAAA,EACpD,WAAW,QAAQ,SAAS,aAAa;AAIvC,QAAI,WAAW,QAAQ,QAAQ;AAAA,MAC7B,CAAC,SACE,KAAK,SAAS,UAAU,qBAAqB,IAAI,KACjD,KAAK,SAAS,eAAe,KAAK,aAAa;AAAA,IAAA;AAGpD,QACE,WAAW,OACX,mBAAQ,aAAR,mBAAkB,WAAlB,mBAA0B,YAAW,uBACrC,CAAC,QAAQ,QAAQ,KAAK,WAAW,GACjC;AACA,iBAAW;AAAA,IACb;AACA,YAAQ,EAAE,YAAY,OAAO,SAAA;AAAA,EAC/B,OAAO;AACL,YAAQ,EAAE,YAAY,OAAO,UAAU,GAAA;AAAA,EACzC;AACA,aAAW,IAAI,SAAS,KAAK;AAC7B,SAAO;AACT;AAaA,MAAM,gCAAgB,QAAA;AAGtB,MAAM,qCAAqB,IAAA;AAC3B,MAAM,sBAAsB;AAE5B,SAAS,eAAe,OAA0B;AAChD,SAAO,KAAK,UAAU;AAAA,IACpB,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM,OAAO,IAAI,CAAC,UAAU,CAAC,MAAM,SAAS,MAAM,OAAO,MAAM,WAAW,MAAM,IAAI,CAAC;AAAA,IACrF,MAAM,MAAM,IAAI,CAAC,SAAS,CAAC,KAAK,KAAK,KAAK,KAAK,CAAC;AAAA,IAChD,MAAM,cAAc,IAAI,CAAC,SAAS,CAAC,KAAK,YAAY,KAAK,QAAQ,CAAC;AAAA,EAAA,CACnE;AACH;AAEA,SAAS,OAAO,OAA6B;AAC3C,QAAM,YAAY,eAAe,KAAK;AACtC,QAAM,WAAW,eAAe,IAAI,SAAS;AAC7C,MAAI,aAAa,QAAW;AAE1B,mBAAe,OAAO,SAAS;AAC/B,mBAAe,IAAI,WAAW,QAAQ;AACtC,WAAO;AAAA,EACT;AACA,QAAM,SAAoB,OAAO,OAAO;AAAA,IACtC,GAAG;AAAA,IACH,QAAQ,OAAO,OAAO,CAAC,GAAG,MAAM,MAAM,CAAC;AAAA,IACvC,OAAO,OAAO,OAAO,CAAC,GAAG,MAAM,KAAK,CAAC;AAAA,IACrC,eAAe,OAAO,OAAO,CAAC,GAAG,MAAM,aAAa,CAAC;AAAA,EAAA,CACtD;AACD,iBAAe,IAAI,WAAW,MAAM;AACpC,MAAI,eAAe,OAAO,qBAAqB;AAC7C,UAAM,SAAS,eAAe,KAAA,EAAO,OAAO;AAC5C,QAAI,WAAW,OAAW,gBAAe,OAAO,MAAM;AAAA,EACxD;AACA,SAAO;AACT;AAEA,SAAS,SAAS,GAAgC,GAAyC;AACzF,MAAI,EAAE,WAAW,EAAE,OAAQ,QAAO;AAClC,WAAS,QAAQ,GAAG,QAAQ,EAAE,QAAQ,SAAS,GAAG;AAChD,QAAI,EAAE,KAAK,MAAM,EAAE,KAAK,EAAG,QAAO;AAAA,EACpC;AACA,SAAO;AACT;AAEA,SAAS,WAAW;AAAA,EAClB;AAAA,EACA;AAAA,EACA;AACF,GAIc;AACZ,QAAM,6BAAa,IAAA;AACnB,QAAM,4BAAY,IAAA;AAClB,QAAM,oCAAoB,IAAA;AAC1B,OAAK,QAAQ,CAAC,SAAS,aAAa;AAClC,UAAM,QAAQ,aAAa,IAAI,QAAQ,QAAQ,MAAM,SAAS,IAAI,QAAQ;AAC1E,eAAW,QAAQ,OAAO;AACxB,iBAAW,SAAS,YAAY,IAAI,GAAG;AACrC,YAAI,MAAM,SAAS,SAAS;AAC1B,gBAAM,QAAQ,OAAO,IAAI,MAAM,MAAM,OAAO;AAC5C,cAAI,CAAC,OAAO;AACV,mBAAO,IAAI,MAAM,MAAM,SAAS,MAAM,KAAK;AAAA,UAC7C,WACG,CAAC,MAAM,SAAS,MAAM,MAAM,SAC5B,CAAC,MAAM,aAAa,MAAM,MAAM,WACjC;AAEA,mBAAO,IAAI,MAAM,MAAM,SAAS;AAAA,cAC9B,GAAG;AAAA,cACH,OAAO,MAAM,SAAS,MAAM,MAAM;AAAA,cAClC,WAAW,MAAM,aAAa,MAAM,MAAM;AAAA,cAC1C,MAAM,MAAM,QAAQ,MAAM,MAAM;AAAA,YAAA,CACjC;AAAA,UACH;AAAA,QACF,WAAW,MAAM,SAAS,QAAQ;AAChC,cAAI,CAAC,MAAM,IAAI,MAAM,KAAK,GAAG,EAAG,OAAM,IAAI,MAAM,KAAK,KAAK,MAAM,IAAI;AAAA,QACtE,WAAW,CAAC,cAAc,IAAI,MAAM,KAAK,UAAU,GAAG;AACpD,wBAAc,IAAI,MAAM,KAAK,YAAY,MAAM,IAAI;AAAA,QACrD;AAAA,MACF;AAAA,IACF;AAAA,EACF,CAAC;AACD,QAAM,MAAM,KAAK,KAAK,SAAS,CAAC;AAChC,SAAO;AAAA,IACL,eAAc,2BAAK,OAAM;AAAA,IACzB;AAAA,IACA,QAAQ,CAAC,GAAG,OAAO,QAAQ;AAAA,IAC3B,OAAO,CAAC,GAAG,MAAM,QAAQ;AAAA,IACzB,eAAe,CAAC,GAAG,cAAc,QAAQ;AAAA,EAAA;AAE7C;AAQO,SAAS,eACd,UACA,OACkB;AAClB,QAAM,UAAU,SAAS,KAAK;AAC9B,MAAI,YAAY,OAAW,QAAO;AAClC,QAAM,OAAO,SAAS,QAAQ,CAAC;AAE/B,MAAI,SAAS,WAAc,KAAK,SAAS,UAAU,kBAAkB,IAAI,GAAI,QAAO;AAGpF,MAAI,QAAQ;AACZ,MAAI,YAAY;AAChB,WAAS,WAAW,OAAO,YAAY,GAAG,YAAY,GAAG;AACvD,UAAM,YAAY,SAAS,QAAQ;AACnC,QAAI,cAAc,OAAW;AAC7B,UAAM,QAAQ,aAAa,SAAS;AACpC,QAAI,MAAM,WAAY;AACtB,QAAI,MAAM,YAAY,GAAG;AACvB,cAAQ;AACR,kBAAY,MAAM;AAAA,IACpB;AAAA,EACF;AACA,MAAI,QAAQ,EAAG,QAAO;AAEtB,QAAM,OAAO,SAAS,MAAM,OAAO,QAAQ,CAAC;AAC5C,QAAM,WAAW,SAAS;AAC1B,QAAM,OAAO,UAAU,IAAI,OAAO;AAClC,MACE,SAAS,UACT,KAAK,cAAc,aACnB,KAAK,aAAa,YAClB,SAAS,KAAK,MAAM,IAAI,GACxB;AACA,WAAO,KAAK;AAAA,EACd;AACA,QAAM,QAAQ,OAAO,WAAW,EAAE,MAAM,WAAW,SAAA,CAAU,CAAC;AAC9D,YAAU,IAAI,SAAS,EAAE,MAAM,WAAW,UAAU,OAAO;AAC3D,SAAO;AACT;AAcA,MAAM,gCAAqC,IAAA;AAOpC,SAAS,eACd,OACA,8BAAmD,WAClC;AACjB,QAAM,YAAiC,MAAM,cAC1C,OAAO,CAAC,SAAS,4BAA4B,IAAI,KAAK,UAAU,CAAC,EACjE,IAAI,CAAC,UAAU;AAAA,IACd,MAAM;AAAA,IACN,KAAK,YAAY,KAAK,UAAU;AAAA,IAChC,YAAY,KAAK;AAAA,IACjB,UAAU,KAAK;AAAA,EAAA,EACf;AACJ,SAAO,CAAC,GAAG,WAAW,GAAG,MAAM,QAAQ,GAAG,MAAM,KAAK;AACvD;AAGO,MAAM,mBAAmB;AAGzB,MAAM,4BAA4B;"}
@@ -21,6 +21,7 @@ function createDeepAgentAttachTransport({
21
21
  onAttachTiming,
22
22
  retryGate = statewireAttachObserver.attachRetryGate,
23
23
  messageWindow: messageWindow$1,
24
+ declaredInterests,
24
25
  laneHistory = false
25
26
  }) {
26
27
  const url = deepAgentThreadUrl({ baseUrl, threadId });
@@ -37,9 +38,12 @@ function createDeepAgentAttachTransport({
37
38
  headers: async (ctx) => {
38
39
  await retryGate.waitForDial(url);
39
40
  onAuthResolve == null ? void 0 : onAuthResolve(ctx);
40
- return messageWindow.withDeclaredMessageWindow(
41
- protocol.withDeclaredProtocolVersion(await headers(ctx), { laneHistory }),
42
- messageWindow$1
41
+ return messageWindow.withDeclaredInterests(
42
+ messageWindow.withDeclaredMessageWindow(
43
+ protocol.withDeclaredProtocolVersion(await headers(ctx), { laneHistory }),
44
+ messageWindow$1
45
+ ),
46
+ (declaredInterests == null ? void 0 : declaredInterests()) ?? null
43
47
  );
44
48
  },
45
49
  ...sessionStore && { sessionStore },
@@ -1 +1 @@
1
- {"version":3,"file":"deep-agent-transport-recipe.cjs","sources":["../../../../agent-runtime/src/deep-agent-transport-recipe.ts"],"sourcesContent":["import type { ResourceElement } from \"@assistant-ui/tap\";\nimport type { Statewire, StatewireClient } from \"statewire\";\nimport {\n type DeepAgentMessageWindowDeclaration,\n withDeclaredMessageWindow,\n} from \"./message-window\";\nimport { withDeclaredProtocolVersion } from \"./protocol\";\nimport {\n attachRetryGate,\n type AttachRetryGate,\n createAttachInstrumentation,\n type DeepAgentAttachTimingEvent,\n type DeepAgentWakeRefusal,\n parseRetryAfterHeaderMs,\n} from \"./statewire-attach-observer\";\nimport {\n resolveStatewireTransportKind,\n statewireTransport,\n type StatewireTransportOptions,\n} from \"./statewire-transport\";\n\n/** Auth-header resolver for the transport; `staleAuth` is true right after an\n * auth rejection, so resolvers that can force-renew should honor it. */\nexport type DeepAgentHeadersResolver = (ctx: {\n staleAuth: boolean;\n}) => Record<string, string> | Promise<Record<string, string>>;\n\n/** The statewire attach URL for a thread: the host's `…/v2/threads` mount plus the id. */\nexport function deepAgentThreadUrl({\n baseUrl,\n threadId,\n}: {\n baseUrl: string;\n threadId: string;\n}): string {\n return `${baseUrl.replace(/\\/+$/, \"\")}/${encodeURIComponent(threadId)}`;\n}\n\nexport interface DeepAgentAttachTransportOptions {\n /** Base URL of the deep-agent statewire host (the `…/v2/threads` mount). */\n baseUrl: string;\n threadId: string;\n headers: DeepAgentHeadersResolver;\n /** SSE when true, WebSocket otherwise (`resolveStatewireTransportKind`). */\n sse?: boolean;\n /** A never-started chat: the transport makes no network calls until the first command send. */\n isNew?: boolean;\n /** Host session store (mobile); browser hosts mount `storage.session` on the thread instead. */\n sessionStore?: StatewireClient.SessionStorage | undefined;\n /** Observes every headers resolve (the diagnostics recorder's stale-auth seam). */\n onAuthResolve?: ((ctx: { staleAuth: boolean }) => void) | undefined;\n /** Attach timing + retry hints (see `statewire-attach-observer`). */\n onAttachTiming?: ((event: DeepAgentAttachTimingEvent) => void) | undefined;\n /** Dial floors from server retry hints; the process-wide gate by default. */\n retryGate?: AttachRetryGate;\n /**\n * The head window declared on the attach (`x-athena-deep-agent-window`,\n * see `message-window.ts`): the hello of a client the host does not\n * remember carries the newest `size` messages. Part of the recipe so a\n * pooled dial asks for the same shape as the thread's own attach — a pooled\n * hello then seeds the very state the attach will reconcile. Omitted, the\n * shared default is declared.\n */\n messageWindow?: DeepAgentMessageWindowDeclaration | undefined;\n /**\n * The surface pages cut lanes (see `DeepAgentProtocolDeclaration`), so its\n * attach declares the lane-cut contract version. Part of the recipe so a\n * pooled dial declares what the thread's own attach would; omitted, the\n * default version is declared.\n */\n laneHistory?: boolean | undefined;\n}\n\n/**\n * The ONE recipe for a deep-agent thread transport — shared by the mounted\n * thread (`useDeepAgentThread`) and the prewarm pool, so a pre-dialed\n * connection is byte-for-byte the connection the thread would have opened\n * itself: same URL, same auth + wire-contract headers, same session lane, same\n * instrumentation.\n *\n * Every attach declares the surface's wire-contract version alongside the\n * host's auth headers (first WS frame / SSE request headers) — the server\n * half of version negotiation reads it in `authorize`. The headers resolve\n * also waits out any server `Retry-After` floor for the thread: both statewire\n * attach paths resolve headers before they dial, so this is the seam where a\n * hint can hold the next dial without touching the library's backoff.\n */\nexport function createDeepAgentAttachTransport<TState = unknown>({\n baseUrl,\n threadId,\n headers,\n sse = false,\n isNew = false,\n sessionStore,\n onAuthResolve,\n onAttachTiming,\n retryGate = attachRetryGate,\n messageWindow,\n laneHistory = false,\n}: DeepAgentAttachTransportOptions): ResourceElement<\n Statewire<TState | undefined>\n> {\n const url = deepAgentThreadUrl({ baseUrl, threadId });\n // The instrumentation wraps the transport actually built: SSE needs the\n // fetch wrapper that reads a refused attach's `Retry-After`.\n const useSse = resolveStatewireTransportKind({ sse }) === \"sse\";\n const instrumentation = createAttachInstrumentation({\n url,\n sse: useSse,\n onEvent: onAttachTiming,\n retryGate,\n });\n const options: StatewireTransportOptions = {\n url,\n ...(isNew && { isNew: true }),\n headers: async (ctx: { staleAuth: boolean }) => {\n await retryGate.waitForDial(url);\n onAuthResolve?.(ctx);\n return withDeclaredMessageWindow(\n withDeclaredProtocolVersion(await headers(ctx), { laneHistory }),\n messageWindow,\n );\n },\n ...(sessionStore && { sessionStore }),\n // Structural types: this package compiles without the DOM lib, while\n // statewire's option types name the browser `WebSocket`/`fetch`.\n ...(instrumentation.webSocket && {\n webSocket:\n instrumentation.webSocket as unknown as StatewireTransportOptions[\"webSocket\"],\n }),\n ...(instrumentation.fetch && {\n fetch:\n instrumentation.fetch as unknown as StatewireTransportOptions[\"fetch\"],\n }),\n };\n return statewireTransport<TState>(options, { sse: useSse });\n}\n\n/** What `POST …/prewarm` answered. */\nexport interface DeepAgentWakeResult {\n status: number;\n /** The host activated the thread and its history is resident. */\n warm: boolean;\n /**\n * `'warm'` when the history was already memoized, `'fresh_load'` when this\n * call loaded it; `null` when the host did not say (a refusal, or a host\n * without the route).\n */\n history: \"fresh_load\" | \"warm\" | null;\n /**\n * The dial floor recorded for the thread off a 503/429 `Retry-After` — the\n * header's value, except a 503 `\"worker\"` refusal, capped at\n * {@link WAKE_WORKER_REFUSAL_FLOOR_MS}.\n */\n retryAfterMs: number | null;\n /** Who refused a 503/429 (see {@link DeepAgentWakeRefusal}); `null` for any other answer. */\n refusal: DeepAgentWakeRefusal;\n durationMs: number;\n}\n\n/**\n * Dial floor for a 503 the pinned worker produced about one instance's\n * lifecycle. Pinned answers every retriable 503 with a blanket\n * `Retry-After: 1`, but what those name — an instance record mid-teardown\n * (\"instance closing; retry\": fence release + placement release, ≈100 ms), a\n * full activation queue — clears far sooner than that, and the thread's own\n * attach is the request that carries the next activation. Holding it a full\n * second traded a ≈100 ms gap for a ≈1 s stall on every refused wake. A 429\n * is rate limiting and keeps its hint whatever the body says.\n */\nexport const WAKE_WORKER_REFUSAL_FLOOR_MS = 250;\n\n/**\n * The pinboard's own placement-refusal vocabulary — the `error` strings of\n * `@assistant-ui/pinboard` (`src/refusals.ts` plus the two inline refusals in\n * `src/server.ts`). Mirrors `PINBOARD_PLACEMENT_REFUSAL_ERRORS` in\n * `iris/scaler/src/v2AccessLog.ts`, whose test pins it to the installed\n * library; keep the two lists identical. A body with any other `error`\n * string is `\"unknown\"`, never `\"placement\"`.\n */\nexport const PINBOARD_PLACEMENT_REFUSAL_ERRORS: ReadonlySet<string> = new Set([\n \"no live backends for namespace\",\n \"registry cold start, retry\",\n \"not the serving fleet\",\n \"no live workers for hostname\",\n \"hostname binding displaced, retry\",\n \"hostname bindings inconsistent, retry\",\n \"takeover in progress, retry\",\n \"placement held by another hostname\",\n \"worker inflight cap exceeded, retry\",\n \"worker refused activation, retry\",\n]);\n\n/**\n * Pinned's instance-lifecycle refusals (`pinned/hosting/directory.py`,\n * `pinned/hosting/_records.py`): each names one instance on the worker the\n * thread is already placed on and clears in the time a teardown or an\n * activation slot takes. Anything else the worker answers 503 with — \"Thread\n * history load failed\", \"The regional conversation store is temporarily\n * unavailable\", a lease that expired — is not about one instance and keeps\n * the full hint.\n */\nexport const PINNED_TRANSIENT_WORKER_REFUSALS: ReadonlySet<string> = new Set([\n \"instance closing; retry\",\n \"instance aborted\",\n \"activation queue full; retry\",\n \"host draining; retry\",\n \"instance capacity reached; retry\",\n \"instance past its wind-down deadline; retry\",\n]);\n\n/**\n * Classify a 503/429 body (see {@link DeepAgentWakeRefusal}): a known pinboard\n * `error` is `\"placement\"`, a known pinned `detail` is `\"worker\"`, everything\n * else — including unfamiliar strings in either field — is `\"unknown\"`.\n */\nexport function classifyWakeRefusal(\n body: unknown,\n): Exclude<DeepAgentWakeRefusal, null> {\n if (typeof body === \"object\" && body !== null) {\n const record = body as Record<string, unknown>;\n if (\n typeof record.error === \"string\" &&\n PINBOARD_PLACEMENT_REFUSAL_ERRORS.has(record.error)\n ) {\n return \"placement\";\n }\n if (\n typeof record.detail === \"string\" &&\n PINNED_TRANSIENT_WORKER_REFUSALS.has(record.detail)\n ) {\n return \"worker\";\n }\n }\n return \"unknown\";\n}\n\n/** Structural `fetch` for the wake call — this package compiles without the DOM lib. */\nexport type DeepAgentWakeFetch = (\n input: string,\n init?: {\n method?: string;\n headers?: Record<string, string>;\n keepalive?: boolean;\n },\n) => Promise<{\n ok: boolean;\n status: number;\n headers: { get(name: string): string | null };\n json(): Promise<unknown>;\n}>;\n\nexport interface DeepAgentWakeOptions {\n /** Base URL of the deep-agent statewire host (the `…/v2/threads` mount). */\n baseUrl: string;\n threadId: string;\n headers: DeepAgentHeadersResolver;\n fetch?: DeepAgentWakeFetch | undefined;\n /** Dial floors from server retry hints; the process-wide gate by default. */\n retryGate?: AttachRetryGate;\n onAttachTiming?: ((event: DeepAgentAttachTimingEvent) => void) | undefined;\n now?: () => number;\n /** The surface pages cut lanes: declare what its attach declares (see `DeepAgentAttachTransportOptions`). */\n laneHistory?: boolean | undefined;\n}\n\n/**\n * Wake a thread's server instance ahead of its attach. `POST …/prewarm` runs\n * the host's `authorize` and history load — the fence adopt, graph compile and\n * checkpoint read that make a cold attach's hello take ≈1 s — and answers with\n * no thread content, only whether the history was already resident. Fire it\n * on hover intent, before a pooled attach or the click's own dial, so the\n * activation overlaps the handshake and the attach that follows finds the\n * instance warm.\n *\n * Fire-and-forget by contract: every failure resolves `null`, and a host\n * without the route (404) is reported like any other non-OK answer. A 503/429\n * `Retry-After` is recorded as a dial floor for the thread, so the attach that\n * follows waits it out instead of bouncing — in full, except a 503 that names\n * one of pinned's instance-lifecycle refusals (one instance mid-teardown on\n * the worker the thread already has; see {@link DeepAgentWakeRefusal}), whose\n * floor is capped at {@link WAKE_WORKER_REFUSAL_FLOOR_MS}.\n */\nexport async function wakeDeepAgentThread({\n baseUrl,\n threadId,\n headers,\n fetch: fetchOverride,\n retryGate = attachRetryGate,\n onAttachTiming,\n now = () => Date.now(),\n laneHistory = false,\n}: DeepAgentWakeOptions): Promise<DeepAgentWakeResult | null> {\n const fetchImpl =\n fetchOverride ?? (globalThis as { fetch?: DeepAgentWakeFetch }).fetch;\n if (typeof fetchImpl !== \"function\") return null;\n const threadUrl = deepAgentThreadUrl({ baseUrl, threadId });\n const emit = (event: DeepAgentAttachTimingEvent): void => {\n try {\n onAttachTiming?.(event);\n } catch {\n // Observers are best-effort.\n }\n };\n const startedAt = now();\n try {\n const resolved = withDeclaredProtocolVersion(\n await headers({ staleAuth: false }),\n { laneHistory },\n );\n const response = await fetchImpl(`${threadUrl}/prewarm`, {\n method: \"POST\",\n headers: resolved,\n keepalive: true,\n });\n const durationMs = now() - startedAt;\n let retryAfterMs: number | null = null;\n let refusal: DeepAgentWakeRefusal = null;\n if (response.status === 503 || response.status === 429) {\n let body: unknown = null;\n try {\n body = await response.json();\n } catch {\n // A refusal without a readable body is classified by nothing.\n }\n refusal = classifyWakeRefusal(body);\n const hinted = parseRetryAfterHeaderMs(\n response.headers.get(\"retry-after\"),\n now,\n );\n if (hinted !== null) {\n retryAfterMs =\n response.status === 503 && refusal === \"worker\"\n ? Math.min(hinted, WAKE_WORKER_REFUSAL_FLOOR_MS)\n : hinted;\n if (retryAfterMs > 0) {\n retryGate.noteRetryAfter(threadUrl, retryAfterMs);\n emit({\n type: \"retry-after\",\n url: threadUrl,\n delayMs: retryAfterMs,\n source: \"http-header\",\n });\n }\n }\n }\n let warm = false;\n let history: DeepAgentWakeResult[\"history\"] = null;\n if (response.ok) {\n try {\n const body = (await response.json()) as {\n warm?: unknown;\n history?: unknown;\n } | null;\n warm = body?.warm === true;\n history =\n body?.history === \"warm\" || body?.history === \"fresh_load\"\n ? body.history\n : null;\n } catch {\n // A 200 without a JSON body still means the instance is up.\n warm = true;\n }\n }\n emit({\n type: \"wake\",\n url: threadUrl,\n status: response.status,\n durationMs,\n history,\n refusal,\n });\n return {\n status: response.status,\n warm,\n history,\n retryAfterMs,\n refusal,\n durationMs,\n };\n } catch {\n return null;\n }\n}\n"],"names":["attachRetryGate","messageWindow","resolveStatewireTransportKind","createAttachInstrumentation","withDeclaredMessageWindow","withDeclaredProtocolVersion","statewireTransport"],"mappings":";;;;;;AA4BO,SAAS,mBAAmB;AAAA,EACjC;AAAA,EACA;AACF,GAGW;AACT,SAAO,GAAG,QAAQ,QAAQ,QAAQ,EAAE,CAAC,IAAI,mBAAmB,QAAQ,CAAC;AACvE;AAmDO,SAAS,+BAAiD;AAAA,EAC/D;AAAA,EACA;AAAA,EACA;AAAA,EACA,MAAM;AAAA,EACN,QAAQ;AAAA,EACR;AAAA,EACA;AAAA,EACA;AAAA,EACA,YAAYA,wBAAAA;AAAAA,EAAA,eACZC;AAAAA,EACA,cAAc;AAChB,GAEE;AACA,QAAM,MAAM,mBAAmB,EAAE,SAAS,UAAU;AAGpD,QAAM,SAASC,mBAAAA,8BAA8B,EAAE,IAAA,CAAK,MAAM;AAC1D,QAAM,kBAAkBC,wBAAAA,4BAA4B;AAAA,IAClD;AAAA,IACA,KAAK;AAAA,IACL,SAAS;AAAA,IACT;AAAA,EAAA,CACD;AACD,QAAM,UAAqC;AAAA,IACzC;AAAA,IACA,GAAI,SAAS,EAAE,OAAO,KAAA;AAAA,IACtB,SAAS,OAAO,QAAgC;AAC9C,YAAM,UAAU,YAAY,GAAG;AAC/B,qDAAgB;AAChB,aAAOC,cAAAA;AAAAA,QACLC,SAAAA,4BAA4B,MAAM,QAAQ,GAAG,GAAG,EAAE,aAAa;AAAA,QAC/DJ;AAAAA,MAAA;AAAA,IAEJ;AAAA,IACA,GAAI,gBAAgB,EAAE,aAAA;AAAA;AAAA;AAAA,IAGtB,GAAI,gBAAgB,aAAa;AAAA,MAC/B,WACE,gBAAgB;AAAA,IAAA;AAAA,IAEpB,GAAI,gBAAgB,SAAS;AAAA,MAC3B,OACE,gBAAgB;AAAA,IAAA;AAAA,EACpB;AAEF,SAAOK,mBAAAA,mBAA2B,SAAS,EAAE,KAAK,QAAQ;AAC5D;;;"}
1
+ {"version":3,"file":"deep-agent-transport-recipe.cjs","sources":["../../../../agent-runtime/src/deep-agent-transport-recipe.ts"],"sourcesContent":["import type { ResourceElement } from \"@assistant-ui/tap\";\nimport type { Statewire, StatewireClient } from \"statewire\";\nimport {\n type DeepAgentInterestDeclaration,\n type DeepAgentMessageWindowDeclaration,\n withDeclaredInterests,\n withDeclaredMessageWindow,\n} from \"./message-window\";\nimport { withDeclaredProtocolVersion } from \"./protocol\";\nimport {\n attachRetryGate,\n type AttachRetryGate,\n createAttachInstrumentation,\n type DeepAgentAttachTimingEvent,\n type DeepAgentWakeRefusal,\n parseRetryAfterHeaderMs,\n} from \"./statewire-attach-observer\";\nimport {\n resolveStatewireTransportKind,\n statewireTransport,\n type StatewireTransportOptions,\n} from \"./statewire-transport\";\n\n/** Auth-header resolver for the transport; `staleAuth` is true right after an\n * auth rejection, so resolvers that can force-renew should honor it. */\nexport type DeepAgentHeadersResolver = (ctx: {\n staleAuth: boolean;\n}) => Record<string, string> | Promise<Record<string, string>>;\n\n/** The statewire attach URL for a thread: the host's `…/v2/threads` mount plus the id. */\nexport function deepAgentThreadUrl({\n baseUrl,\n threadId,\n}: {\n baseUrl: string;\n threadId: string;\n}): string {\n return `${baseUrl.replace(/\\/+$/, \"\")}/${encodeURIComponent(threadId)}`;\n}\n\nexport interface DeepAgentAttachTransportOptions {\n /** Base URL of the deep-agent statewire host (the `…/v2/threads` mount). */\n baseUrl: string;\n threadId: string;\n headers: DeepAgentHeadersResolver;\n /** SSE when true, WebSocket otherwise (`resolveStatewireTransportKind`). */\n sse?: boolean;\n /** A never-started chat: the transport makes no network calls until the first command send. */\n isNew?: boolean;\n /** Host session store (mobile); browser hosts mount `storage.session` on the thread instead. */\n sessionStore?: StatewireClient.SessionStorage | undefined;\n /** Observes every headers resolve (the diagnostics recorder's stale-auth seam). */\n onAuthResolve?: ((ctx: { staleAuth: boolean }) => void) | undefined;\n /** Attach timing + retry hints (see `statewire-attach-observer`). */\n onAttachTiming?: ((event: DeepAgentAttachTimingEvent) => void) | undefined;\n /** Dial floors from server retry hints; the process-wide gate by default. */\n retryGate?: AttachRetryGate;\n /**\n * The head window declared on the attach (`x-athena-deep-agent-window`,\n * see `message-window.ts`): the hello of a client the host does not\n * remember carries the newest `size` messages. Part of the recipe so a\n * pooled dial asks for the same shape as the thread's own attach — a pooled\n * hello then seeds the very state the attach will reconcile. Omitted, the\n * shared default is declared.\n */\n messageWindow?: DeepAgentMessageWindowDeclaration | undefined;\n /**\n * The view this thread's attach holds, read at every attach's header\n * resolve (`x-athena-deep-agent-interests`, see `message-window.ts`): a\n * re-attach's hello then carries the pages it had loaded on whichever host\n * instance it lands on. `null` (a first attach) declares nothing. Not part\n * of a pooled dial, which never held a view.\n */\n declaredInterests?: (() => DeepAgentInterestDeclaration | null) | undefined;\n /**\n * The surface pages cut lanes (see `DeepAgentProtocolDeclaration`), so its\n * attach declares the lane-cut contract version. Part of the recipe so a\n * pooled dial declares what the thread's own attach would; omitted, the\n * default version is declared.\n */\n laneHistory?: boolean | undefined;\n}\n\n/**\n * The ONE recipe for a deep-agent thread transport — shared by the mounted\n * thread (`useDeepAgentThread`) and the prewarm pool, so a pre-dialed\n * connection is byte-for-byte the connection the thread would have opened\n * itself: same URL, same auth + wire-contract headers, same session lane, same\n * instrumentation.\n *\n * Every attach declares the surface's wire-contract version alongside the\n * host's auth headers (first WS frame / SSE request headers) — the server\n * half of version negotiation reads it in `authorize`. The headers resolve\n * also waits out any server `Retry-After` floor for the thread: both statewire\n * attach paths resolve headers before they dial, so this is the seam where a\n * hint can hold the next dial without touching the library's backoff.\n */\nexport function createDeepAgentAttachTransport<TState = unknown>({\n baseUrl,\n threadId,\n headers,\n sse = false,\n isNew = false,\n sessionStore,\n onAuthResolve,\n onAttachTiming,\n retryGate = attachRetryGate,\n messageWindow,\n declaredInterests,\n laneHistory = false,\n}: DeepAgentAttachTransportOptions): ResourceElement<\n Statewire<TState | undefined>\n> {\n const url = deepAgentThreadUrl({ baseUrl, threadId });\n // The instrumentation wraps the transport actually built: SSE needs the\n // fetch wrapper that reads a refused attach's `Retry-After`.\n const useSse = resolveStatewireTransportKind({ sse }) === \"sse\";\n const instrumentation = createAttachInstrumentation({\n url,\n sse: useSse,\n onEvent: onAttachTiming,\n retryGate,\n });\n const options: StatewireTransportOptions = {\n url,\n ...(isNew && { isNew: true }),\n headers: async (ctx: { staleAuth: boolean }) => {\n await retryGate.waitForDial(url);\n onAuthResolve?.(ctx);\n return withDeclaredInterests(\n withDeclaredMessageWindow(\n withDeclaredProtocolVersion(await headers(ctx), { laneHistory }),\n messageWindow,\n ),\n declaredInterests?.() ?? null,\n );\n },\n ...(sessionStore && { sessionStore }),\n // Structural types: this package compiles without the DOM lib, while\n // statewire's option types name the browser `WebSocket`/`fetch`.\n ...(instrumentation.webSocket && {\n webSocket:\n instrumentation.webSocket as unknown as StatewireTransportOptions[\"webSocket\"],\n }),\n ...(instrumentation.fetch && {\n fetch:\n instrumentation.fetch as unknown as StatewireTransportOptions[\"fetch\"],\n }),\n };\n return statewireTransport<TState>(options, { sse: useSse });\n}\n\n/** What `POST …/prewarm` answered. */\nexport interface DeepAgentWakeResult {\n status: number;\n /** The host activated the thread and its history is resident. */\n warm: boolean;\n /**\n * `'warm'` when the history was already memoized, `'fresh_load'` when this\n * call loaded it; `null` when the host did not say (a refusal, or a host\n * without the route).\n */\n history: \"fresh_load\" | \"warm\" | null;\n /**\n * The dial floor recorded for the thread off a 503/429 `Retry-After` — the\n * header's value, except a 503 `\"worker\"` refusal, capped at\n * {@link WAKE_WORKER_REFUSAL_FLOOR_MS}.\n */\n retryAfterMs: number | null;\n /** Who refused a 503/429 (see {@link DeepAgentWakeRefusal}); `null` for any other answer. */\n refusal: DeepAgentWakeRefusal;\n durationMs: number;\n}\n\n/**\n * Dial floor for a 503 the pinned worker produced about one instance's\n * lifecycle. Pinned answers every retriable 503 with a blanket\n * `Retry-After: 1`, but what those name — an instance record mid-teardown\n * (\"instance closing; retry\": fence release + placement release, ≈100 ms), a\n * full activation queue — clears far sooner than that, and the thread's own\n * attach is the request that carries the next activation. Holding it a full\n * second traded a ≈100 ms gap for a ≈1 s stall on every refused wake. A 429\n * is rate limiting and keeps its hint whatever the body says.\n */\nexport const WAKE_WORKER_REFUSAL_FLOOR_MS = 250;\n\n/**\n * The pinboard's own placement-refusal vocabulary — the `error` strings of\n * `@assistant-ui/pinboard` (`src/refusals.ts` plus the two inline refusals in\n * `src/server.ts`). Mirrors `PINBOARD_PLACEMENT_REFUSAL_ERRORS` in\n * `iris/scaler/src/v2AccessLog.ts`, whose test pins it to the installed\n * library; keep the two lists identical. A body with any other `error`\n * string is `\"unknown\"`, never `\"placement\"`.\n */\nexport const PINBOARD_PLACEMENT_REFUSAL_ERRORS: ReadonlySet<string> = new Set([\n \"no live backends for namespace\",\n \"registry cold start, retry\",\n \"not the serving fleet\",\n \"no live workers for hostname\",\n \"hostname binding displaced, retry\",\n \"hostname bindings inconsistent, retry\",\n \"takeover in progress, retry\",\n \"placement held by another hostname\",\n \"worker inflight cap exceeded, retry\",\n \"worker refused activation, retry\",\n]);\n\n/**\n * Pinned's instance-lifecycle refusals (`pinned/hosting/directory.py`,\n * `pinned/hosting/_records.py`): each names one instance on the worker the\n * thread is already placed on and clears in the time a teardown or an\n * activation slot takes. Anything else the worker answers 503 with — \"Thread\n * history load failed\", \"The regional conversation store is temporarily\n * unavailable\", a lease that expired — is not about one instance and keeps\n * the full hint.\n */\nexport const PINNED_TRANSIENT_WORKER_REFUSALS: ReadonlySet<string> = new Set([\n \"instance closing; retry\",\n \"instance aborted\",\n \"activation queue full; retry\",\n \"host draining; retry\",\n \"instance capacity reached; retry\",\n \"instance past its wind-down deadline; retry\",\n]);\n\n/**\n * Classify a 503/429 body (see {@link DeepAgentWakeRefusal}): a known pinboard\n * `error` is `\"placement\"`, a known pinned `detail` is `\"worker\"`, everything\n * else — including unfamiliar strings in either field — is `\"unknown\"`.\n */\nexport function classifyWakeRefusal(\n body: unknown,\n): Exclude<DeepAgentWakeRefusal, null> {\n if (typeof body === \"object\" && body !== null) {\n const record = body as Record<string, unknown>;\n if (\n typeof record.error === \"string\" &&\n PINBOARD_PLACEMENT_REFUSAL_ERRORS.has(record.error)\n ) {\n return \"placement\";\n }\n if (\n typeof record.detail === \"string\" &&\n PINNED_TRANSIENT_WORKER_REFUSALS.has(record.detail)\n ) {\n return \"worker\";\n }\n }\n return \"unknown\";\n}\n\n/** Structural `fetch` for the wake call — this package compiles without the DOM lib. */\nexport type DeepAgentWakeFetch = (\n input: string,\n init?: {\n method?: string;\n headers?: Record<string, string>;\n keepalive?: boolean;\n },\n) => Promise<{\n ok: boolean;\n status: number;\n headers: { get(name: string): string | null };\n json(): Promise<unknown>;\n}>;\n\nexport interface DeepAgentWakeOptions {\n /** Base URL of the deep-agent statewire host (the `…/v2/threads` mount). */\n baseUrl: string;\n threadId: string;\n headers: DeepAgentHeadersResolver;\n fetch?: DeepAgentWakeFetch | undefined;\n /** Dial floors from server retry hints; the process-wide gate by default. */\n retryGate?: AttachRetryGate;\n onAttachTiming?: ((event: DeepAgentAttachTimingEvent) => void) | undefined;\n now?: () => number;\n /** The surface pages cut lanes: declare what its attach declares (see `DeepAgentAttachTransportOptions`). */\n laneHistory?: boolean | undefined;\n}\n\n/**\n * Wake a thread's server instance ahead of its attach. `POST …/prewarm` runs\n * the host's `authorize` and history load — the fence adopt, graph compile and\n * checkpoint read that make a cold attach's hello take ≈1 s — and answers with\n * no thread content, only whether the history was already resident. Fire it\n * on hover intent, before a pooled attach or the click's own dial, so the\n * activation overlaps the handshake and the attach that follows finds the\n * instance warm.\n *\n * Fire-and-forget by contract: every failure resolves `null`, and a host\n * without the route (404) is reported like any other non-OK answer. A 503/429\n * `Retry-After` is recorded as a dial floor for the thread, so the attach that\n * follows waits it out instead of bouncing — in full, except a 503 that names\n * one of pinned's instance-lifecycle refusals (one instance mid-teardown on\n * the worker the thread already has; see {@link DeepAgentWakeRefusal}), whose\n * floor is capped at {@link WAKE_WORKER_REFUSAL_FLOOR_MS}.\n */\nexport async function wakeDeepAgentThread({\n baseUrl,\n threadId,\n headers,\n fetch: fetchOverride,\n retryGate = attachRetryGate,\n onAttachTiming,\n now = () => Date.now(),\n laneHistory = false,\n}: DeepAgentWakeOptions): Promise<DeepAgentWakeResult | null> {\n const fetchImpl =\n fetchOverride ?? (globalThis as { fetch?: DeepAgentWakeFetch }).fetch;\n if (typeof fetchImpl !== \"function\") return null;\n const threadUrl = deepAgentThreadUrl({ baseUrl, threadId });\n const emit = (event: DeepAgentAttachTimingEvent): void => {\n try {\n onAttachTiming?.(event);\n } catch {\n // Observers are best-effort.\n }\n };\n const startedAt = now();\n try {\n const resolved = withDeclaredProtocolVersion(\n await headers({ staleAuth: false }),\n { laneHistory },\n );\n const response = await fetchImpl(`${threadUrl}/prewarm`, {\n method: \"POST\",\n headers: resolved,\n keepalive: true,\n });\n const durationMs = now() - startedAt;\n let retryAfterMs: number | null = null;\n let refusal: DeepAgentWakeRefusal = null;\n if (response.status === 503 || response.status === 429) {\n let body: unknown = null;\n try {\n body = await response.json();\n } catch {\n // A refusal without a readable body is classified by nothing.\n }\n refusal = classifyWakeRefusal(body);\n const hinted = parseRetryAfterHeaderMs(\n response.headers.get(\"retry-after\"),\n now,\n );\n if (hinted !== null) {\n retryAfterMs =\n response.status === 503 && refusal === \"worker\"\n ? Math.min(hinted, WAKE_WORKER_REFUSAL_FLOOR_MS)\n : hinted;\n if (retryAfterMs > 0) {\n retryGate.noteRetryAfter(threadUrl, retryAfterMs);\n emit({\n type: \"retry-after\",\n url: threadUrl,\n delayMs: retryAfterMs,\n source: \"http-header\",\n });\n }\n }\n }\n let warm = false;\n let history: DeepAgentWakeResult[\"history\"] = null;\n if (response.ok) {\n try {\n const body = (await response.json()) as {\n warm?: unknown;\n history?: unknown;\n } | null;\n warm = body?.warm === true;\n history =\n body?.history === \"warm\" || body?.history === \"fresh_load\"\n ? body.history\n : null;\n } catch {\n // A 200 without a JSON body still means the instance is up.\n warm = true;\n }\n }\n emit({\n type: \"wake\",\n url: threadUrl,\n status: response.status,\n durationMs,\n history,\n refusal,\n });\n return {\n status: response.status,\n warm,\n history,\n retryAfterMs,\n refusal,\n durationMs,\n };\n } catch {\n return null;\n }\n}\n"],"names":["attachRetryGate","messageWindow","resolveStatewireTransportKind","createAttachInstrumentation","withDeclaredInterests","withDeclaredMessageWindow","withDeclaredProtocolVersion","statewireTransport"],"mappings":";;;;;;AA8BO,SAAS,mBAAmB;AAAA,EACjC;AAAA,EACA;AACF,GAGW;AACT,SAAO,GAAG,QAAQ,QAAQ,QAAQ,EAAE,CAAC,IAAI,mBAAmB,QAAQ,CAAC;AACvE;AA2DO,SAAS,+BAAiD;AAAA,EAC/D;AAAA,EACA;AAAA,EACA;AAAA,EACA,MAAM;AAAA,EACN,QAAQ;AAAA,EACR;AAAA,EACA;AAAA,EACA;AAAA,EACA,YAAYA,wBAAAA;AAAAA,EAAA,eACZC;AAAAA,EACA;AAAA,EACA,cAAc;AAChB,GAEE;AACA,QAAM,MAAM,mBAAmB,EAAE,SAAS,UAAU;AAGpD,QAAM,SAASC,mBAAAA,8BAA8B,EAAE,IAAA,CAAK,MAAM;AAC1D,QAAM,kBAAkBC,wBAAAA,4BAA4B;AAAA,IAClD;AAAA,IACA,KAAK;AAAA,IACL,SAAS;AAAA,IACT;AAAA,EAAA,CACD;AACD,QAAM,UAAqC;AAAA,IACzC;AAAA,IACA,GAAI,SAAS,EAAE,OAAO,KAAA;AAAA,IACtB,SAAS,OAAO,QAAgC;AAC9C,YAAM,UAAU,YAAY,GAAG;AAC/B,qDAAgB;AAChB,aAAOC,cAAAA;AAAAA,QACLC,cAAAA;AAAAA,UACEC,SAAAA,4BAA4B,MAAM,QAAQ,GAAG,GAAG,EAAE,aAAa;AAAA,UAC/DL;AAAAA,QAAA;AAAA,SAEF,6DAAyB;AAAA,MAAA;AAAA,IAE7B;AAAA,IACA,GAAI,gBAAgB,EAAE,aAAA;AAAA;AAAA;AAAA,IAGtB,GAAI,gBAAgB,aAAa;AAAA,MAC/B,WACE,gBAAgB;AAAA,IAAA;AAAA,IAEpB,GAAI,gBAAgB,SAAS;AAAA,MAC3B,OACE,gBAAgB;AAAA,IAAA;AAAA,EACpB;AAEF,SAAOM,mBAAAA,mBAA2B,SAAS,EAAE,KAAK,QAAQ;AAC5D;;;"}
@@ -1,6 +1,6 @@
1
1
  import type { ResourceElement } from "@assistant-ui/tap";
2
2
  import type { Statewire, StatewireClient } from "statewire";
3
- import { type DeepAgentMessageWindowDeclaration } from "./message-window";
3
+ import { type DeepAgentInterestDeclaration, type DeepAgentMessageWindowDeclaration } from "./message-window";
4
4
  import { type AttachRetryGate, type DeepAgentAttachTimingEvent, type DeepAgentWakeRefusal } from "./statewire-attach-observer";
5
5
  /** Auth-header resolver for the transport; `staleAuth` is true right after an
6
6
  * auth rejection, so resolvers that can force-renew should honor it. */
@@ -40,6 +40,14 @@ export interface DeepAgentAttachTransportOptions {
40
40
  * shared default is declared.
41
41
  */
42
42
  messageWindow?: DeepAgentMessageWindowDeclaration | undefined;
43
+ /**
44
+ * The view this thread's attach holds, read at every attach's header
45
+ * resolve (`x-athena-deep-agent-interests`, see `message-window.ts`): a
46
+ * re-attach's hello then carries the pages it had loaded on whichever host
47
+ * instance it lands on. `null` (a first attach) declares nothing. Not part
48
+ * of a pooled dial, which never held a view.
49
+ */
50
+ declaredInterests?: (() => DeepAgentInterestDeclaration | null) | undefined;
43
51
  /**
44
52
  * The surface pages cut lanes (see `DeepAgentProtocolDeclaration`), so its
45
53
  * attach declares the lane-cut contract version. Part of the recipe so a
@@ -62,7 +70,7 @@ export interface DeepAgentAttachTransportOptions {
62
70
  * attach paths resolve headers before they dial, so this is the seam where a
63
71
  * hint can hold the next dial without touching the library's backoff.
64
72
  */
65
- export declare function createDeepAgentAttachTransport<TState = unknown>({ baseUrl, threadId, headers, sse, isNew, sessionStore, onAuthResolve, onAttachTiming, retryGate, messageWindow, laneHistory, }: DeepAgentAttachTransportOptions): ResourceElement<Statewire<TState | undefined>>;
73
+ export declare function createDeepAgentAttachTransport<TState = unknown>({ baseUrl, threadId, headers, sse, isNew, sessionStore, onAuthResolve, onAttachTiming, retryGate, messageWindow, declaredInterests, laneHistory, }: DeepAgentAttachTransportOptions): ResourceElement<Statewire<TState | undefined>>;
66
74
  /** What `POST …/prewarm` answered. */
67
75
  export interface DeepAgentWakeResult {
68
76
  status: number;
@@ -1,4 +1,4 @@
1
- import { withDeclaredMessageWindow } from "./message-window.js";
1
+ import { withDeclaredInterests, withDeclaredMessageWindow } from "./message-window.js";
2
2
  import { withDeclaredProtocolVersion } from "./protocol.js";
3
3
  import { createAttachInstrumentation, attachRetryGate } from "./statewire-attach-observer.js";
4
4
  import { statewireTransport, resolveStatewireTransportKind } from "./statewire-transport.js";
@@ -19,6 +19,7 @@ function createDeepAgentAttachTransport({
19
19
  onAttachTiming,
20
20
  retryGate = attachRetryGate,
21
21
  messageWindow,
22
+ declaredInterests,
22
23
  laneHistory = false
23
24
  }) {
24
25
  const url = deepAgentThreadUrl({ baseUrl, threadId });
@@ -35,9 +36,12 @@ function createDeepAgentAttachTransport({
35
36
  headers: async (ctx) => {
36
37
  await retryGate.waitForDial(url);
37
38
  onAuthResolve == null ? void 0 : onAuthResolve(ctx);
38
- return withDeclaredMessageWindow(
39
- withDeclaredProtocolVersion(await headers(ctx), { laneHistory }),
40
- messageWindow
39
+ return withDeclaredInterests(
40
+ withDeclaredMessageWindow(
41
+ withDeclaredProtocolVersion(await headers(ctx), { laneHistory }),
42
+ messageWindow
43
+ ),
44
+ (declaredInterests == null ? void 0 : declaredInterests()) ?? null
41
45
  );
42
46
  },
43
47
  ...sessionStore && { sessionStore },