@athenaintel/react 0.24.0 → 0.25.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 (197) hide show
  1. package/README.md +29 -4
  2. package/dist/assets/AssetPanel.cjs +3 -2
  3. package/dist/assets/AssetPanel.cjs.map +1 -1
  4. package/dist/assets/AssetPanel.js +3 -2
  5. package/dist/assets/AssetPanel.js.map +1 -1
  6. package/dist/assets/AthenaAssetEmbed.cjs +1 -0
  7. package/dist/assets/AthenaAssetEmbed.cjs.map +1 -1
  8. package/dist/assets/AthenaAssetEmbed.js +1 -0
  9. package/dist/assets/AthenaAssetEmbed.js.map +1 -1
  10. package/dist/assets/use-asset-embed.cjs +22 -23
  11. package/dist/assets/use-asset-embed.cjs.map +1 -1
  12. package/dist/assets/use-asset-embed.d.ts +9 -0
  13. package/dist/assets/use-asset-embed.js +22 -23
  14. package/dist/assets/use-asset-embed.js.map +1 -1
  15. package/dist/auth/AthenaAuthProvider.cjs +32 -4
  16. package/dist/auth/AthenaAuthProvider.cjs.map +1 -1
  17. package/dist/auth/AthenaAuthProvider.d.ts +21 -0
  18. package/dist/auth/AthenaAuthProvider.js +33 -5
  19. package/dist/auth/AthenaAuthProvider.js.map +1 -1
  20. package/dist/auth/AthenaCredentialSourceContext.cjs +21 -0
  21. package/dist/auth/AthenaCredentialSourceContext.cjs.map +1 -0
  22. package/dist/auth/AthenaCredentialSourceContext.d.ts +14 -0
  23. package/dist/auth/AthenaCredentialSourceContext.js +21 -0
  24. package/dist/auth/AthenaCredentialSourceContext.js.map +1 -0
  25. package/dist/auth/AthenaSSOProvider.cjs +71 -47
  26. package/dist/auth/AthenaSSOProvider.cjs.map +1 -1
  27. package/dist/auth/AthenaSSOProvider.js +71 -47
  28. package/dist/auth/AthenaSSOProvider.js.map +1 -1
  29. package/dist/auth/credential-source.cjs +120 -0
  30. package/dist/auth/credential-source.cjs.map +1 -0
  31. package/dist/auth/credential-source.d.ts +68 -0
  32. package/dist/auth/credential-source.js +120 -0
  33. package/dist/auth/credential-source.js.map +1 -0
  34. package/dist/auth/types.d.ts +9 -0
  35. package/dist/chat/AthenaChat.cjs +8 -0
  36. package/dist/chat/AthenaChat.cjs.map +1 -1
  37. package/dist/chat/AthenaChat.js +8 -0
  38. package/dist/chat/AthenaChat.js.map +1 -1
  39. package/dist/chat/StatewireApprovalCard.cjs +31 -6
  40. package/dist/chat/StatewireApprovalCard.cjs.map +1 -1
  41. package/dist/chat/StatewireApprovalCard.d.ts +34 -4
  42. package/dist/chat/StatewireApprovalCard.js +33 -8
  43. package/dist/chat/StatewireApprovalCard.js.map +1 -1
  44. package/dist/chat-ui/index.d.ts +1 -1
  45. package/dist/chat-ui/mentions/suggestions/data/api-client.cjs +20 -11
  46. package/dist/chat-ui/mentions/suggestions/data/api-client.cjs.map +1 -1
  47. package/dist/chat-ui/mentions/suggestions/data/api-client.d.ts +11 -2
  48. package/dist/chat-ui/mentions/suggestions/data/api-client.js +20 -11
  49. package/dist/chat-ui/mentions/suggestions/data/api-client.js.map +1 -1
  50. package/dist/chat-ui/statewire.d.ts +1 -1
  51. package/dist/collab/mint.cjs +6 -15
  52. package/dist/collab/mint.cjs.map +1 -1
  53. package/dist/collab/mint.d.ts +8 -0
  54. package/dist/collab/mint.js +6 -15
  55. package/dist/collab/mint.js.map +1 -1
  56. package/dist/collab/presence-mint.cjs +20 -16
  57. package/dist/collab/presence-mint.cjs.map +1 -1
  58. package/dist/collab/presence-mint.js +21 -17
  59. package/dist/collab/presence-mint.js.map +1 -1
  60. package/dist/collab/presence-react.cjs +12 -5
  61. package/dist/collab/presence-react.cjs.map +1 -1
  62. package/dist/collab/presence-react.d.ts +3 -1
  63. package/dist/collab/presence-react.js +12 -5
  64. package/dist/collab/presence-react.js.map +1 -1
  65. package/dist/collab/react.cjs +5 -2
  66. package/dist/collab/react.cjs.map +1 -1
  67. package/dist/collab/react.js +5 -2
  68. package/dist/collab/react.js.map +1 -1
  69. package/dist/dictation/use-athena-dictation-engine.cjs +12 -11
  70. package/dist/dictation/use-athena-dictation-engine.cjs.map +1 -1
  71. package/dist/dictation/use-athena-dictation-engine.d.ts +4 -3
  72. package/dist/dictation/use-athena-dictation-engine.js +12 -11
  73. package/dist/dictation/use-athena-dictation-engine.js.map +1 -1
  74. package/dist/index.cjs +1 -0
  75. package/dist/index.cjs.map +1 -1
  76. package/dist/index.d.ts +3 -3
  77. package/dist/index.js +2 -1
  78. package/dist/packages/agent-runtime/src/conversation-list.cjs.map +1 -1
  79. package/dist/packages/agent-runtime/src/conversation-list.js.map +1 -1
  80. package/dist/packages/agent-runtime/src/generated/runconfig-contract.cjs +1 -0
  81. package/dist/packages/agent-runtime/src/generated/runconfig-contract.cjs.map +1 -1
  82. package/dist/packages/agent-runtime/src/generated/runconfig-contract.js +1 -0
  83. package/dist/packages/agent-runtime/src/generated/runconfig-contract.js.map +1 -1
  84. package/dist/packages/agent-runtime/src/interrupts.cjs +22 -2
  85. package/dist/packages/agent-runtime/src/interrupts.cjs.map +1 -1
  86. package/dist/packages/agent-runtime/src/interrupts.js +22 -2
  87. package/dist/packages/agent-runtime/src/interrupts.js.map +1 -1
  88. package/dist/packages/agent-runtime/src/messages.cjs +2 -1
  89. package/dist/packages/agent-runtime/src/messages.cjs.map +1 -1
  90. package/dist/packages/agent-runtime/src/messages.js +2 -1
  91. package/dist/packages/agent-runtime/src/messages.js.map +1 -1
  92. package/dist/packages/agent-runtime/src/voice-presentation.cjs +6 -0
  93. package/dist/packages/agent-runtime/src/voice-presentation.cjs.map +1 -1
  94. package/dist/packages/agent-runtime/src/voice-presentation.js +6 -0
  95. package/dist/packages/agent-runtime/src/voice-presentation.js.map +1 -1
  96. package/dist/packages/agent-runtime/src/voice-turn-grouping.cjs +68 -0
  97. package/dist/packages/agent-runtime/src/voice-turn-grouping.cjs.map +1 -0
  98. package/dist/packages/agent-runtime/src/voice-turn-grouping.js +68 -0
  99. package/dist/packages/agent-runtime/src/voice-turn-grouping.js.map +1 -0
  100. package/dist/packages/react/package.json.cjs +1 -1
  101. package/dist/packages/react/package.json.js +1 -1
  102. package/dist/provider/AthenaContext.cjs.map +1 -1
  103. package/dist/provider/AthenaContext.d.ts +7 -1
  104. package/dist/provider/AthenaContext.js.map +1 -1
  105. package/dist/provider/AthenaProvider.cjs +24 -2
  106. package/dist/provider/AthenaProvider.cjs.map +1 -1
  107. package/dist/provider/AthenaProvider.js +24 -2
  108. package/dist/provider/AthenaProvider.js.map +1 -1
  109. package/dist/provider/theme-style-context.cjs +12 -0
  110. package/dist/provider/theme-style-context.cjs.map +1 -0
  111. package/dist/provider/theme-style-context.d.ts +8 -0
  112. package/dist/provider/theme-style-context.js +12 -0
  113. package/dist/provider/theme-style-context.js.map +1 -0
  114. package/dist/runtime/auth-refusal-restart.cjs +66 -0
  115. package/dist/runtime/auth-refusal-restart.cjs.map +1 -0
  116. package/dist/runtime/auth-refusal-restart.d.ts +35 -0
  117. package/dist/runtime/auth-refusal-restart.js +66 -0
  118. package/dist/runtime/auth-refusal-restart.js.map +1 -0
  119. package/dist/runtime/auth.cjs +58 -21
  120. package/dist/runtime/auth.cjs.map +1 -1
  121. package/dist/runtime/auth.d.ts +11 -1
  122. package/dist/runtime/auth.js +59 -22
  123. package/dist/runtime/auth.js.map +1 -1
  124. package/dist/runtime/credential-renewal.cjs +29 -0
  125. package/dist/runtime/credential-renewal.cjs.map +1 -0
  126. package/dist/runtime/credential-renewal.d.ts +35 -0
  127. package/dist/runtime/credential-renewal.js +29 -0
  128. package/dist/runtime/credential-renewal.js.map +1 -0
  129. package/dist/search/useAthenaRepoSearch.cjs +3 -5
  130. package/dist/search/useAthenaRepoSearch.cjs.map +1 -1
  131. package/dist/search/useAthenaRepoSearch.d.ts +7 -7
  132. package/dist/search/useAthenaRepoSearch.js +3 -5
  133. package/dist/search/useAthenaRepoSearch.js.map +1 -1
  134. package/dist/styles.css +1 -1
  135. package/dist/threads/SessionList.cjs +87 -16
  136. package/dist/threads/SessionList.cjs.map +1 -1
  137. package/dist/threads/SessionList.js +88 -17
  138. package/dist/threads/SessionList.js.map +1 -1
  139. package/dist/threads/api.cjs +5 -1
  140. package/dist/threads/api.cjs.map +1 -1
  141. package/dist/threads/api.js +5 -1
  142. package/dist/threads/api.js.map +1 -1
  143. package/dist/threads/conversation-list-fetcher.cjs +5 -5
  144. package/dist/threads/conversation-list-fetcher.cjs.map +1 -1
  145. package/dist/threads/conversation-list-fetcher.d.ts +6 -5
  146. package/dist/threads/conversation-list-fetcher.js +5 -5
  147. package/dist/threads/conversation-list-fetcher.js.map +1 -1
  148. package/dist/threads/session-list-model.cjs +13 -5
  149. package/dist/threads/session-list-model.cjs.map +1 -1
  150. package/dist/threads/session-list-model.d.ts +10 -3
  151. package/dist/threads/session-list-model.js +13 -5
  152. package/dist/threads/session-list-model.js.map +1 -1
  153. package/dist/threads/statewire-thread-list.cjs +13 -14
  154. package/dist/threads/statewire-thread-list.cjs.map +1 -1
  155. package/dist/threads/statewire-thread-list.js +13 -14
  156. package/dist/threads/statewire-thread-list.js.map +1 -1
  157. package/dist/threads/use-rest-conversation-history.cjs +6 -2
  158. package/dist/threads/use-rest-conversation-history.cjs.map +1 -1
  159. package/dist/threads/use-rest-conversation-history.d.ts +3 -1
  160. package/dist/threads/use-rest-conversation-history.js +7 -3
  161. package/dist/threads/use-rest-conversation-history.js.map +1 -1
  162. package/dist/threads/use-statewire-conversation-history.cjs +9 -2
  163. package/dist/threads/use-statewire-conversation-history.cjs.map +1 -1
  164. package/dist/threads/use-statewire-conversation-history.d.ts +3 -1
  165. package/dist/threads/use-statewire-conversation-history.js +10 -3
  166. package/dist/threads/use-statewire-conversation-history.js.map +1 -1
  167. package/dist/threads/use-thread-archive.cjs +2 -1
  168. package/dist/threads/use-thread-archive.cjs.map +1 -1
  169. package/dist/threads/use-thread-archive.d.ts +3 -1
  170. package/dist/threads/use-thread-archive.js +2 -1
  171. package/dist/threads/use-thread-archive.js.map +1 -1
  172. package/dist/threads/use-thread-favorite.cjs.map +1 -1
  173. package/dist/threads/use-thread-favorite.d.ts +3 -1
  174. package/dist/threads/use-thread-favorite.js.map +1 -1
  175. package/dist/threads/use-thread-field-override.cjs +2 -1
  176. package/dist/threads/use-thread-field-override.cjs.map +1 -1
  177. package/dist/threads/use-thread-field-override.d.ts +3 -1
  178. package/dist/threads/use-thread-field-override.js +2 -1
  179. package/dist/threads/use-thread-field-override.js.map +1 -1
  180. package/dist/threads/use-thread-rename.cjs.map +1 -1
  181. package/dist/threads/use-thread-rename.d.ts +3 -1
  182. package/dist/threads/use-thread-rename.js.map +1 -1
  183. package/dist/tools/tool-uis.cjs +11 -15
  184. package/dist/tools/tool-uis.cjs.map +1 -1
  185. package/dist/tools/tool-uis.js +11 -15
  186. package/dist/tools/tool-uis.js.map +1 -1
  187. package/dist/ui/popover.cjs +46 -0
  188. package/dist/ui/popover.cjs.map +1 -0
  189. package/dist/ui/popover.d.ts +6 -0
  190. package/dist/ui/popover.js +46 -0
  191. package/dist/ui/popover.js.map +1 -0
  192. package/dist/voice/use-athena-voice-session.cjs +17 -5
  193. package/dist/voice/use-athena-voice-session.cjs.map +1 -1
  194. package/dist/voice/use-athena-voice-session.d.ts +2 -2
  195. package/dist/voice/use-athena-voice-session.js +17 -5
  196. package/dist/voice/use-athena-voice-session.js.map +1 -1
  197. package/package.json +2 -2
@@ -33,13 +33,14 @@ function readInterruptMessage(value, fallback = DEFAULT_INTERRUPT_PAUSED_MESSAGE
33
33
  function pendingActionRequests(hitl) {
34
34
  const context = hitl == null ? void 0 : hitl.context;
35
35
  if (!context) return [];
36
- const requests = Array.isArray(context.action_requests) ? context.action_requests.filter(isRecord) : context.tool_name && context.tool_args ? [{ name: context.tool_name, args: context.tool_args }] : [];
36
+ const requests = Array.isArray(context.action_requests) ? context.action_requests.filter(isRecord) : context.tool_name && context.tool_args ? [{ name: context.tool_name, args: context.tool_args, tool_call_id: context.tool_call_id }] : [];
37
37
  return requests.map((request) => ({
38
38
  name: typeof request.name === "string" ? request.name : "tool",
39
39
  args: isRecord(request.args) ? request.args : {},
40
40
  ...typeof request.tool_call_id === "string" && { toolCallId: request.tool_call_id },
41
41
  ...typeof request.interrupt_id === "string" && { interruptId: request.interrupt_id },
42
- ...request.session_approval_allowed === true && { sessionApprovalAllowed: true }
42
+ ...request.session_approval_allowed === true && { sessionApprovalAllowed: true },
43
+ ...Array.isArray(request.allowed_decisions) && request.allowed_decisions.includes("edit") && { editAllowed: true }
43
44
  }));
44
45
  }
45
46
  function canApproveForSession(actionRequests) {
@@ -95,6 +96,23 @@ function hitlResumeApproveForSession() {
95
96
  function hitlResumeReject(reason = HITL_REJECT_REASON) {
96
97
  return { action: "reject", reason };
97
98
  }
99
+ function actionRequestToolCallIds(actionRequests) {
100
+ if (actionRequests.length === 0) return null;
101
+ const ids = [];
102
+ for (const request of actionRequests) {
103
+ if (request.toolCallId === void 0 || ids.includes(request.toolCallId)) return null;
104
+ ids.push(request.toolCallId);
105
+ }
106
+ return ids;
107
+ }
108
+ function hitlResumeActionDecisions(decisions) {
109
+ const [anchorId, anchor] = Object.entries(decisions)[0] ?? [];
110
+ return {
111
+ action: (anchor == null ? void 0 : anchor.action) === "reject" ? "reject" : "approve",
112
+ ...anchorId !== void 0 && { tool_call_id: anchorId },
113
+ action_decisions: decisions
114
+ };
115
+ }
98
116
  const INTERRUPT_DECLINE_REASON = "User declined the request.";
99
117
  function interruptResumeDecline(reason = INTERRUPT_DECLINE_REASON) {
100
118
  return { action: "reject", reason };
@@ -205,10 +223,12 @@ export {
205
223
  HITL_REJECT_REASON,
206
224
  HITL_RESUME_TRANSPORT_RETRIES,
207
225
  INTERRUPT_DECLINE_REASON,
226
+ actionRequestToolCallIds,
208
227
  asHitlApproval,
209
228
  canApproveForSession,
210
229
  fanLaneDecision,
211
230
  findAbandonedInterrupt,
231
+ hitlResumeActionDecisions,
212
232
  hitlResumeApprove,
213
233
  hitlResumeApproveForSession,
214
234
  hitlResumeReject,
@@ -1 +1 @@
1
- {"version":3,"file":"interrupts.js","sources":["../../../../../agent-runtime/src/interrupts.ts"],"sourcesContent":["/**\n * Human-in-the-loop interrupt primitives shared by every statewire chat\n * surface (Olympus, the React SDK, the Office add-in, the Chrome extension,\n * mobile).\n *\n * The agora deep-agent runtime parks a gated run on an `interrupt` input\n * request whose payload carries the Athena `hitl_approval` shape\n * (`athena_deep_agent/hitl_bridge.py`). A surface answers it with\n * `{ type: 'run/input', requestId, response: { type: 'resume', value } }` —\n * the exact command the backend's resume path consumes. This module holds the\n * payload contract (narrowing, resume values) and the shared resume-lock state\n * machine; rendering stays with each surface.\n *\n * Deliberately parameterized rather than self-wiring: these helpers never call\n * `useStatewireRuns`/`useAuiState` themselves. Each surface reads its own\n * statewire hooks (which its tests already mock) and passes the values in, so\n * the shared semantics cannot drift while per-surface wiring stays testable.\n */\n\nimport type { StatewireThread } from '@assistant-ui/react-statewire';\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport { hasClientToolInterruptSource } from './client-tools';\nimport { statewireTransportOutcome } from './errors';\nimport { readDeepAgentStreamValues } from './statewire-state';\n\n/** Human-in-the-loop approval payload the agora deep-agent runtime parks a run\n * on (shaped by `athena_deep_agent/hitl_bridge.py`). */\nexport interface HitlApprovalPayload {\n source: string;\n type: string;\n message: string;\n /** Risk headline/facts/warnings, when the host includes them. */\n approval_context?: unknown;\n context?: {\n tool_name?: string;\n tool_id?: string;\n tool_call_id?: string;\n tool_args?: Record<string, unknown>;\n pending_action_count?: number;\n approval_context?: unknown;\n /**\n * Every pending gated call (name + args), in decision order. Absent on\n * older backends — fall back to the single `tool_name`/`tool_args` pair.\n * Per-dispatch payloads also carry each lane's `tool_call_id` (the lane\n * card key) and `interrupt_id` (the lane resume key).\n */\n action_requests?: {\n name?: string;\n args?: Record<string, unknown>;\n tool_call_id?: string;\n interrupt_id?: string;\n /**\n * True when \"approve all for this session\" would take effect for this\n * tool (the workspace allows overriding its approval). Absent on older\n * backends and on non-overridable tools: the option is not offered.\n */\n session_approval_allowed?: boolean;\n }[];\n /**\n * True when each dispatch parked on its own interrupt: decisions submit\n * per lane via `lane_decisions` and each approved lane starts immediately,\n * instead of the legacy batch where one resume must answer every lane.\n */\n per_dispatch?: boolean;\n };\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/** A pending `interrupt` input request; a present `response` key marks it answered. */\nexport function isPendingInterrupt(\n request: StatewireThread.InputRequestState,\n): request is StatewireThread.CustomInputRequest {\n return request.type === 'interrupt' && !('response' in request);\n}\n\n/**\n * Interrupt sources that were system pauses before the host stamped\n * `requires_user_action: false` on them (a background deep-research wait). Kept\n * only for the deploy skew between an older agora and a newer client; delete\n * once every deployment's raiser stamps the flag.\n */\nconst LEGACY_SYSTEM_PAUSE_INTERRUPT_SOURCES: ReadonlySet<string> = new Set(['deep_research_pending']);\n\n/**\n * A park nobody answers: the agent is still working (a background research\n * wait) and the backend resumes it itself. The raiser stamps\n * `requires_user_action: false`; older raisers are recognised by source.\n */\nexport function isSystemPauseInterrupt(payload: unknown): boolean {\n if (!isRecord(payload)) return false;\n if (payload.requires_user_action === false) return true;\n return (\n typeof payload.source === 'string' && LEGACY_SYSTEM_PAUSE_INTERRUPT_SOURCES.has(payload.source)\n );\n}\n\n/**\n * A pending interrupt an approval card may answer. Two kinds are excluded —\n * client-tool parks (the client-tool bridge owns those, and a generic resume\n * would corrupt the parked tool call) and system pauses (nothing to answer; the\n * backend resumes them). The source checks are deliberately loose so a skewed\n * deploy can never route one here.\n */\nexport function isApprovalCardInterrupt(\n request: StatewireThread.InputRequestState,\n): request is StatewireThread.CustomInputRequest {\n if (!isPendingInterrupt(request)) return false;\n const payload = (request as { payload?: unknown }).payload;\n return !hasClientToolInterruptSource(payload) && !isSystemPauseInterrupt(payload);\n}\n\n/** Narrow an interrupt payload to the Athena HITL approval shape. */\nexport function asHitlApproval(value: unknown): HitlApprovalPayload | null {\n if (!isRecord(value)) return null;\n return value.type === 'hitl' &&\n value.source === 'hitl_approval' &&\n typeof value.message === 'string'\n ? (value as unknown as HitlApprovalPayload)\n : null;\n}\n\n/** Default paused-run copy when an interrupt payload carries no message. */\nexport const DEFAULT_INTERRUPT_PAUSED_MESSAGE =\n 'The agent is paused and waiting for input.';\n\n/** Best-effort human-readable message from any interrupt payload. */\nexport function readInterruptMessage(\n value: unknown,\n fallback: string = DEFAULT_INTERRUPT_PAUSED_MESSAGE,\n): string {\n if (isRecord(value) && typeof value.message === 'string' && value.message.length > 0) {\n return value.message;\n }\n return fallback;\n}\n\nexport interface PendingActionRequest {\n name: string;\n args: Record<string, unknown>;\n /** The lane's `task` tool-call id (present on per-dispatch payloads). */\n toolCallId?: string;\n /** The lane's langgraph interrupt id — the `lane_decisions` resume key. */\n interruptId?: string;\n /** Present (true) when a session-wide approval would cover later calls of this tool. */\n sessionApprovalAllowed?: true;\n}\n\n/**\n * Every gated call this approval covers, in decision order. One resume answers\n * ALL of them, so every entry must stay reviewable somewhere. Falls back to\n * the single first-call fields on older backends without `action_requests`.\n */\nexport function pendingActionRequests(hitl: HitlApprovalPayload | null): PendingActionRequest[] {\n const context = hitl?.context;\n if (!context) return [];\n // The payload crosses the wire with only its top-level shape narrowed\n // (`asHitlApproval`), so the nested collection must be validated before any\n // array/property access: a malformed host value must degrade to \"no\n // reviewable entries\", never crash the approval surface.\n const requests: Record<string, unknown>[] = Array.isArray(context.action_requests)\n ? context.action_requests.filter(isRecord)\n : context.tool_name && context.tool_args\n ? [{ name: context.tool_name, args: context.tool_args }]\n : [];\n return requests.map((request) => ({\n name: typeof request.name === 'string' ? request.name : 'tool',\n args: isRecord(request.args) ? request.args : {},\n ...(typeof request.tool_call_id === 'string' && { toolCallId: request.tool_call_id }),\n ...(typeof request.interrupt_id === 'string' && { interruptId: request.interrupt_id }),\n ...(request.session_approval_allowed === true && { sessionApprovalAllowed: true }),\n }));\n}\n\n/**\n * Whether the card may offer \"approve all for this session\": every pending\n * call must be one the backend stamped as session-approvable, so a batch that\n * mixes in a tool the workspace pinned as always-prompt only offers the\n * one-time answer.\n */\nexport function canApproveForSession(actionRequests: readonly PendingActionRequest[]): boolean {\n return (\n actionRequests.length > 0 &&\n actionRequests.every((request) => request.sessionApprovalAllowed === true)\n );\n}\n\n/** An interrupt a non-resume dispatch dropped, published by the host. */\nexport interface AbandonedInputRequest {\n id: string;\n type: string;\n}\n\n/**\n * `useStatewireState` selector body for the host-published abandoned input\n * requests. Reads through {@link readDeepAgentStreamValues} so the selector is\n * safe on the pre-attach (`undefined`) wire state, and returns the raw array\n * reference so the subscription stays shallow-stable — derive (`.find`) on the\n * result, never inside the selector.\n */\nexport function readAbandonedInputRequests(\n state: unknown,\n): readonly AbandonedInputRequest[] | undefined {\n const abandoned = readDeepAgentStreamValues(state).abandonedInputRequests;\n // Wire value: anything non-array (including a malformed host publication)\n // reads as \"none\" rather than crashing the consuming surface. Identity-\n // preserving for the valid case, so it stays selector-safe.\n return Array.isArray(abandoned)\n ? (abandoned as readonly AbandonedInputRequest[])\n : undefined;\n}\n\n/**\n * `useStatewireState` selector body for the host's mirrored interrupt\n * (`__interrupt__`, the LangGraph `{value, resumable}` wrapper). The mirror\n * is published the moment a lane parks on its gate — while sibling lanes are\n * still streaming and the run manager has NOT yet published an answerable\n * input request. Raw reference, selector-safe; unwrap with\n * {@link interruptMirrorPayload}.\n */\nexport function readInterruptMirror(state: unknown): unknown {\n return readDeepAgentStreamValues(state).__interrupt__;\n}\n\n/** The payload of a mirrored interrupt wrapper, or `undefined` for anything else. */\nexport function interruptMirrorPayload(mirror: unknown): unknown {\n return isRecord(mirror) ? mirror.value : undefined;\n}\n\n/**\n * `useStatewireState` selector body for the per-dispatch lane decisions the\n * host accepted through `run/lane-input` while the run was still streaming\n * (`queuedLaneDecisions`, keyed by langgraph interrupt id). Held until the\n * run parks, then answered as a native `lane_decisions` resume; every attached\n * client reads a queued lane as decided. Raw reference, selector-safe.\n */\nexport function readQueuedLaneDecisions(\n state: unknown,\n): Readonly<Record<string, unknown>> | undefined {\n const queued = readDeepAgentStreamValues(state).queuedLaneDecisions;\n return isRecord(queued) ? queued : undefined;\n}\n\n/**\n * The distinct langgraph interrupt ids a `lane_decisions` resume answers for\n * these gated calls, in first-seen order — or `null` when any call lacks its\n * stamp (a legacy batch payload only a positional resume can answer). A\n * nested HITL request gating several calls contributes one id: the lane is\n * the interrupt, and one decision fans across its calls server-side.\n */\nexport function laneInterruptIds(actionRequests: readonly PendingActionRequest[]): string[] | null {\n if (actionRequests.length === 0) return null;\n const ids: string[] = [];\n for (const request of actionRequests) {\n if (request.interruptId === undefined) return null;\n if (!ids.includes(request.interruptId)) ids.push(request.interruptId);\n }\n return ids;\n}\n\n/** One decision for every lane — the `lane_decisions` map a run-wide approve or reject sends. */\nexport function fanLaneDecision<T>({\n interruptIds,\n decision,\n}: {\n interruptIds: readonly string[];\n decision: T;\n}): Record<string, T> {\n return Object.fromEntries(interruptIds.map((id) => [id, decision]));\n}\n\n/** The abandoned `interrupt` entry, if any — the \"approval was cancelled\" notice. */\nexport function findAbandonedInterrupt(\n abandoned: readonly AbandonedInputRequest[] | undefined,\n): AbandonedInputRequest | undefined {\n return abandoned?.find(\n (request) => isRecord(request) && request.type === 'interrupt' && typeof request.id === 'string',\n );\n}\n\n/** Canonical reason string carried by a user rejection. */\nexport const HITL_REJECT_REASON = 'User rejected the action.';\n\n/**\n * Default {@link UseApprovalResumeLockOptions.pendingResumeGraceMs} for every\n * approval surface: how long a clicked decision waits for its resume run to\n * start before the actions unlock again. Covers a `run/input` the host\n * rejected or a disconnected transport swallowed before any run began — the\n * one failure neither identity-change nor observed-run recovery can detect.\n */\nexport const HITL_PENDING_RESUME_GRACE_MS = 15_000;\n\n/**\n * Default {@link UseApprovalResumeLockOptions.maxTransportRetries}: how many\n * times a decision the transport lost (the websocket dropped or the statewire\n * session reset while the `run/input` was in flight) is re-sent on its own\n * before the user is asked to click again.\n */\nexport const HITL_RESUME_TRANSPORT_RETRIES = 3;\n\n/**\n * The dispatch died client-side without the host ever seeing it — a proven\n * session loss or a gone stream. Safe to re-send the same decision for the\n * same still-parked request. The complement (a server verdict, or the surface\n * unmounting) is not.\n */\nexport function isResumeTransportLoss(error: unknown): boolean {\n const outcome = statewireTransportOutcome(error);\n return outcome === 'session-lost' || outcome === 'stream-gone';\n}\n\n/** `run/input` resume value approving the gated call(s). */\nexport function hitlResumeApprove(): Record<string, string> {\n return { action: 'approve' };\n}\n\n/**\n * `run/input` resume value approving the gated call(s) AND every later call of\n * the same tool(s) on this thread. The backend records the tools from its own\n * parked interrupt (never from this payload), so the value carries no names.\n */\nexport function hitlResumeApproveForSession(): Record<string, string> {\n return { action: 'approve', scope: 'session' };\n}\n\n/** `run/input` resume value rejecting the gated call(s). */\nexport function hitlResumeReject(reason: string = HITL_REJECT_REASON): Record<string, string> {\n return { action: 'reject', reason };\n}\n\n/** Canonical reason string carried by a user declining a non-HITL park. */\nexport const INTERRUPT_DECLINE_REASON = 'User declined the request.';\n\n/**\n * `run/input` resume value declining a non-HITL park. `{ action: 'reject' }`\n * is the one value every typed raiser treats as a clean user decline\n * (`FormCancelledError` / `ToolRejectedError`); a generic `continue` would be\n * rejected by their resume schemas and surface as a tool error the agent then\n * improvises around.\n */\nexport function interruptResumeDecline(\n reason: string = INTERRUPT_DECLINE_REASON,\n): Record<string, string> {\n return { action: 'reject', reason };\n}\n\nexport interface UseApprovalResumeLockOptions<TValue> {\n /** The pending interrupt's request id (`undefined` when nothing is parked). */\n requestId: string | undefined;\n /** The surface's thread `isRunning` read (mockable per surface). */\n isRunning: boolean;\n /**\n * Dispatch the `run/input` resume for `requestId` on the surface's transport.\n * Return the dispatch promise so a refused or lost command releases the lock\n * at once and reaches `onResumeRejected`; a `void` return leaves the\n * recovery paths below as the only release.\n */\n sendResume: (requestId: string, value: TValue) => void | Promise<unknown>;\n /**\n * The dispatch `sendResume` returned rejected — the host refused the\n * `run/input` or the transport lost it. The lock is already released when\n * this fires; the surface tells the user, since the request is back on\n * screen with nothing else saying why.\n */\n onResumeRejected?: (error: unknown, requestId: string) => void;\n /**\n * A dispatch the transport lost is being re-sent for the still-parked\n * request; `attempt` counts the retries so far (1-based). Diagnostics only —\n * the lock stays held across the retry.\n */\n onResumeRetry?: (error: unknown, requestId: string, attempt: number) => void;\n /**\n * Automatic re-sends of a decision the transport lost\n * ({@link isResumeTransportLoss}) while the request is still parked. `0`\n * disables retries. Defaults to {@link HITL_RESUME_TRANSPORT_RETRIES}.\n */\n maxTransportRetries?: number;\n /**\n * Re-enable the actions when no resume run started within this window —\n * covers a `run/input` that was rejected or lost before any run began, so\n * neither reset path below can fire. Omit to keep the pre-existing surface\n * behavior of staying locked until a resume run is observed.\n */\n pendingResumeGraceMs?: number;\n}\n\nexport interface ApprovalResumeLock<TValue> {\n /** True after a resume until the request identity changes (or recovery fires). */\n pending: boolean;\n /**\n * Send the resume value, locking the actions. No-op (returns `undefined`)\n * while pending or unparked. Returns the dispatch promise when `sendResume`\n * produced one, so a surface can hold its own state until the host settles.\n */\n resume: (value: TValue) => Promise<unknown> | undefined;\n}\n\n/**\n * Optimistic action lock shared by every approval surface: disable the\n * decision buttons the moment one is clicked, until the request identity\n * changes (it clears after the resume, or a fresh interrupt arrives). This\n * closes the double-click → duplicate-resume window.\n *\n * Recovery paths, in order of specificity:\n * 0. The dispatch itself rejected (a `sendResume` that returned a promise) —\n * released at once, and reported through `onResumeRejected`. A rejection\n * that is a transport loss (the socket dropped mid-flight) is re-sent up to\n * `maxTransportRetries` times first, the lock held throughout, so a flaky\n * connection does not hand the card back to the user for another click.\n * The returned promise settles with the final attempt.\n * 1. Identity change — the resume landed (or a new interrupt replaced the\n * request). Reset during render via React's \"previous value\" pattern.\n * 2. A resume run was observed starting and then settling with the request\n * still pending (failed/cancelled resume) — clear the lock so the user can\n * answer again.\n * 3. Optional grace window ({@link UseApprovalResumeLockOptions.pendingResumeGraceMs})\n * for a `run/input` that never started a run at all.\n */\nexport function useApprovalResumeLock<TValue = Record<string, string>>(\n options: UseApprovalResumeLockOptions<TValue>,\n): ApprovalResumeLock<TValue> {\n const {\n requestId,\n isRunning,\n sendResume,\n pendingResumeGraceMs,\n onResumeRejected,\n onResumeRetry,\n maxTransportRetries = HITL_RESUME_TRANSPORT_RETRIES,\n } = options;\n\n const [pending, setPending] = useState(false);\n // Bumped per re-send so the grace window restarts with each attempt.\n const [attempt, setAttempt] = useState(0);\n const prevRequestIdRef = useRef(requestId);\n if (prevRequestIdRef.current !== requestId) {\n prevRequestIdRef.current = requestId;\n if (pending) setPending(false);\n }\n\n // Track the resume run we kicked off (isRunning observed true→false while\n // pending) and clear `pending` when it ends with the request unresolved.\n const sawResumeRunRef = useRef(false);\n useEffect(() => {\n if (!pending) {\n sawResumeRunRef.current = false;\n return;\n }\n if (isRunning) {\n sawResumeRunRef.current = true;\n return;\n }\n if (sawResumeRunRef.current) {\n sawResumeRunRef.current = false;\n setPending(false);\n }\n }, [pending, isRunning]);\n\n // Pre-start failure recovery: re-enable after the grace window when no\n // resume run began; answering the still-parked request again is exactly\n // what the user needs to be able to do.\n useEffect(() => {\n if (!pending || pendingResumeGraceMs === undefined) return;\n const timer = setTimeout(() => {\n if (!sawResumeRunRef.current) setPending(false);\n }, pendingResumeGraceMs);\n return () => clearTimeout(timer);\n }, [pending, requestId, pendingResumeGraceMs, attempt]);\n\n const sendResumeRef = useRef(sendResume);\n sendResumeRef.current = sendResume;\n const onResumeRejectedRef = useRef(onResumeRejected);\n onResumeRejectedRef.current = onResumeRejected;\n const onResumeRetryRef = useRef(onResumeRetry);\n onResumeRetryRef.current = onResumeRetry;\n const maxTransportRetriesRef = useRef(maxTransportRetries);\n maxTransportRetriesRef.current = maxTransportRetries;\n const currentRequestIdRef = useRef(requestId);\n currentRequestIdRef.current = requestId;\n const pendingRef = useRef(pending);\n pendingRef.current = pending;\n // One token per `resume` call: a settle from an earlier submission must not\n // retry over, or release, a newer one for the same request.\n const submissionRef = useRef(0);\n\n const resume = useCallback(\n (value: TValue): Promise<unknown> | undefined => {\n if (requestId === undefined || pendingRef.current) return undefined;\n const submission = ++submissionRef.current;\n setPending(true);\n pendingRef.current = true;\n setAttempt(0);\n const first: unknown = sendResumeRef.current(requestId, value);\n if (!(first instanceof Promise)) return undefined;\n // The final failure is already reported through `onResumeRejected`;\n // callers that drop the promise must not get an unhandled rejection.\n const outcome = new Promise<unknown>((resolve, reject) => {\n const settle = (dispatched: Promise<unknown>, retries: number) => {\n dispatched.then(resolve, (error: unknown) => {\n // A late refusal for a request the lock has since moved past (the\n // park was re-keyed, or a newer answer is in flight) must neither\n // release that answer's lock nor be reported as a failure of it.\n // The optimistic fold hides the answered request until the fold\n // rolls back, so `undefined` here is not a departure.\n const current = currentRequestIdRef.current;\n if (\n submission !== submissionRef.current ||\n (current !== undefined && current !== requestId)\n ) {\n reject(error);\n return;\n }\n if (retries < maxTransportRetriesRef.current && isResumeTransportLoss(error)) {\n const next = retries + 1;\n onResumeRetryRef.current?.(error, requestId, next);\n setPending(true);\n pendingRef.current = true;\n setAttempt(next);\n const resent: unknown = sendResumeRef.current(requestId, value);\n if (resent instanceof Promise) {\n settle(resent, next);\n return;\n }\n resolve(undefined);\n return;\n }\n setPending(false);\n pendingRef.current = false;\n onResumeRejectedRef.current?.(error, requestId);\n reject(error);\n });\n };\n settle(first, 0);\n });\n outcome.catch(() => {});\n return outcome;\n },\n [requestId],\n );\n\n return { pending, resume };\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"interrupts.js","sources":["../../../../../agent-runtime/src/interrupts.ts"],"sourcesContent":["/**\n * Human-in-the-loop interrupt primitives shared by every statewire chat\n * surface (Olympus, the React SDK, the Office add-in, the Chrome extension,\n * mobile).\n *\n * The agora deep-agent runtime parks a gated run on an `interrupt` input\n * request whose payload carries the Athena `hitl_approval` shape\n * (`athena_deep_agent/hitl_bridge.py`). A surface answers it with\n * `{ type: 'run/input', requestId, response: { type: 'resume', value } }` —\n * the exact command the backend's resume path consumes. This module holds the\n * payload contract (narrowing, resume values) and the shared resume-lock state\n * machine; rendering stays with each surface.\n *\n * Deliberately parameterized rather than self-wiring: these helpers never call\n * `useStatewireRuns`/`useAuiState` themselves. Each surface reads its own\n * statewire hooks (which its tests already mock) and passes the values in, so\n * the shared semantics cannot drift while per-surface wiring stays testable.\n */\n\nimport type { StatewireThread } from '@assistant-ui/react-statewire';\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport { hasClientToolInterruptSource } from './client-tools';\nimport { statewireTransportOutcome } from './errors';\nimport { readDeepAgentStreamValues } from './statewire-state';\n\n/** Human-in-the-loop approval payload the agora deep-agent runtime parks a run\n * on (shaped by `athena_deep_agent/hitl_bridge.py`). */\nexport interface HitlApprovalPayload {\n source: string;\n type: string;\n message: string;\n /** Risk headline/facts/warnings, when the host includes them. */\n approval_context?: unknown;\n context?: {\n tool_name?: string;\n tool_id?: string;\n tool_call_id?: string;\n tool_args?: Record<string, unknown>;\n pending_action_count?: number;\n approval_context?: unknown;\n /**\n * Every pending gated call (name + args), in decision order. Absent on\n * older backends — fall back to the single `tool_name`/`tool_args` pair.\n * Per-dispatch payloads also carry each lane's `tool_call_id` (the lane\n * card key) and `interrupt_id` (the lane resume key).\n */\n action_requests?: {\n name?: string;\n args?: Record<string, unknown>;\n tool_call_id?: string;\n interrupt_id?: string;\n /**\n * True when \"approve all for this session\" would take effect for this\n * tool (the workspace allows overriding its approval). Absent on older\n * backends and on non-overridable tools: the option is not offered.\n */\n session_approval_allowed?: boolean;\n /**\n * The decision types the parked gate accepts for this call (`approve`,\n * `reject`, `edit`). Absent on older backends: only approve / reject\n * are known to be accepted.\n */\n allowed_decisions?: string[];\n }[];\n /**\n * True when each dispatch parked on its own interrupt: decisions submit\n * per lane via `lane_decisions` and each approved lane starts immediately,\n * instead of the legacy batch where one resume must answer every lane.\n */\n per_dispatch?: boolean;\n };\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\n/** A pending `interrupt` input request; a present `response` key marks it answered. */\nexport function isPendingInterrupt(\n request: StatewireThread.InputRequestState,\n): request is StatewireThread.CustomInputRequest {\n return request.type === 'interrupt' && !('response' in request);\n}\n\n/**\n * Interrupt sources that were system pauses before the host stamped\n * `requires_user_action: false` on them (a background deep-research wait). Kept\n * only for the deploy skew between an older agora and a newer client; delete\n * once every deployment's raiser stamps the flag.\n */\nconst LEGACY_SYSTEM_PAUSE_INTERRUPT_SOURCES: ReadonlySet<string> = new Set(['deep_research_pending']);\n\n/**\n * A park nobody answers: the agent is still working (a background research\n * wait) and the backend resumes it itself. The raiser stamps\n * `requires_user_action: false`; older raisers are recognised by source.\n */\nexport function isSystemPauseInterrupt(payload: unknown): boolean {\n if (!isRecord(payload)) return false;\n if (payload.requires_user_action === false) return true;\n return (\n typeof payload.source === 'string' && LEGACY_SYSTEM_PAUSE_INTERRUPT_SOURCES.has(payload.source)\n );\n}\n\n/**\n * A pending interrupt an approval card may answer. Two kinds are excluded —\n * client-tool parks (the client-tool bridge owns those, and a generic resume\n * would corrupt the parked tool call) and system pauses (nothing to answer; the\n * backend resumes them). The source checks are deliberately loose so a skewed\n * deploy can never route one here.\n */\nexport function isApprovalCardInterrupt(\n request: StatewireThread.InputRequestState,\n): request is StatewireThread.CustomInputRequest {\n if (!isPendingInterrupt(request)) return false;\n const payload = (request as { payload?: unknown }).payload;\n return !hasClientToolInterruptSource(payload) && !isSystemPauseInterrupt(payload);\n}\n\n/** Narrow an interrupt payload to the Athena HITL approval shape. */\nexport function asHitlApproval(value: unknown): HitlApprovalPayload | null {\n if (!isRecord(value)) return null;\n return value.type === 'hitl' &&\n value.source === 'hitl_approval' &&\n typeof value.message === 'string'\n ? (value as unknown as HitlApprovalPayload)\n : null;\n}\n\n/** Default paused-run copy when an interrupt payload carries no message. */\nexport const DEFAULT_INTERRUPT_PAUSED_MESSAGE =\n 'The agent is paused and waiting for input.';\n\n/** Best-effort human-readable message from any interrupt payload. */\nexport function readInterruptMessage(\n value: unknown,\n fallback: string = DEFAULT_INTERRUPT_PAUSED_MESSAGE,\n): string {\n if (isRecord(value) && typeof value.message === 'string' && value.message.length > 0) {\n return value.message;\n }\n return fallback;\n}\n\nexport interface PendingActionRequest {\n name: string;\n args: Record<string, unknown>;\n /** The lane's `task` tool-call id (present on per-dispatch payloads). */\n toolCallId?: string;\n /** The lane's langgraph interrupt id — the `lane_decisions` resume key. */\n interruptId?: string;\n /** Present (true) when a session-wide approval would cover later calls of this tool. */\n sessionApprovalAllowed?: true;\n /** Present (true) when the gate accepts an `edit` decision for this call. */\n editAllowed?: true;\n}\n\n/**\n * Every gated call this approval covers, in decision order. One resume answers\n * ALL of them, so every entry must stay reviewable somewhere. Falls back to\n * the single first-call fields on older backends without `action_requests`.\n */\nexport function pendingActionRequests(hitl: HitlApprovalPayload | null): PendingActionRequest[] {\n const context = hitl?.context;\n if (!context) return [];\n // The payload crosses the wire with only its top-level shape narrowed\n // (`asHitlApproval`), so the nested collection must be validated before any\n // array/property access: a malformed host value must degrade to \"no\n // reviewable entries\", never crash the approval surface.\n const requests: Record<string, unknown>[] = Array.isArray(context.action_requests)\n ? context.action_requests.filter(isRecord)\n : context.tool_name && context.tool_args\n ? [{ name: context.tool_name, args: context.tool_args, tool_call_id: context.tool_call_id }]\n : [];\n return requests.map((request) => ({\n name: typeof request.name === 'string' ? request.name : 'tool',\n args: isRecord(request.args) ? request.args : {},\n ...(typeof request.tool_call_id === 'string' && { toolCallId: request.tool_call_id }),\n ...(typeof request.interrupt_id === 'string' && { interruptId: request.interrupt_id }),\n ...(request.session_approval_allowed === true && { sessionApprovalAllowed: true }),\n ...(Array.isArray(request.allowed_decisions) &&\n request.allowed_decisions.includes('edit') && { editAllowed: true }),\n }));\n}\n\n/**\n * Whether the card may offer \"approve all for this session\": every pending\n * call must be one the backend stamped as session-approvable, so a batch that\n * mixes in a tool the workspace pinned as always-prompt only offers the\n * one-time answer.\n */\nexport function canApproveForSession(actionRequests: readonly PendingActionRequest[]): boolean {\n return (\n actionRequests.length > 0 &&\n actionRequests.every((request) => request.sessionApprovalAllowed === true)\n );\n}\n\n/** An interrupt a non-resume dispatch dropped, published by the host. */\nexport interface AbandonedInputRequest {\n id: string;\n type: string;\n}\n\n/**\n * `useStatewireState` selector body for the host-published abandoned input\n * requests. Reads through {@link readDeepAgentStreamValues} so the selector is\n * safe on the pre-attach (`undefined`) wire state, and returns the raw array\n * reference so the subscription stays shallow-stable — derive (`.find`) on the\n * result, never inside the selector.\n */\nexport function readAbandonedInputRequests(\n state: unknown,\n): readonly AbandonedInputRequest[] | undefined {\n const abandoned = readDeepAgentStreamValues(state).abandonedInputRequests;\n // Wire value: anything non-array (including a malformed host publication)\n // reads as \"none\" rather than crashing the consuming surface. Identity-\n // preserving for the valid case, so it stays selector-safe.\n return Array.isArray(abandoned)\n ? (abandoned as readonly AbandonedInputRequest[])\n : undefined;\n}\n\n/**\n * `useStatewireState` selector body for the host's mirrored interrupt\n * (`__interrupt__`, the LangGraph `{value, resumable}` wrapper). The mirror\n * is published the moment a lane parks on its gate — while sibling lanes are\n * still streaming and the run manager has NOT yet published an answerable\n * input request. Raw reference, selector-safe; unwrap with\n * {@link interruptMirrorPayload}.\n */\nexport function readInterruptMirror(state: unknown): unknown {\n return readDeepAgentStreamValues(state).__interrupt__;\n}\n\n/** The payload of a mirrored interrupt wrapper, or `undefined` for anything else. */\nexport function interruptMirrorPayload(mirror: unknown): unknown {\n return isRecord(mirror) ? mirror.value : undefined;\n}\n\n/**\n * `useStatewireState` selector body for the per-dispatch lane decisions the\n * host accepted through `run/lane-input` while the run was still streaming\n * (`queuedLaneDecisions`, keyed by langgraph interrupt id). Held until the\n * run parks, then answered as a native `lane_decisions` resume; every attached\n * client reads a queued lane as decided. Raw reference, selector-safe.\n */\nexport function readQueuedLaneDecisions(\n state: unknown,\n): Readonly<Record<string, unknown>> | undefined {\n const queued = readDeepAgentStreamValues(state).queuedLaneDecisions;\n return isRecord(queued) ? queued : undefined;\n}\n\n/**\n * The distinct langgraph interrupt ids a `lane_decisions` resume answers for\n * these gated calls, in first-seen order — or `null` when any call lacks its\n * stamp (a legacy batch payload only a positional resume can answer). A\n * nested HITL request gating several calls contributes one id: the lane is\n * the interrupt, and one decision fans across its calls server-side.\n */\nexport function laneInterruptIds(actionRequests: readonly PendingActionRequest[]): string[] | null {\n if (actionRequests.length === 0) return null;\n const ids: string[] = [];\n for (const request of actionRequests) {\n if (request.interruptId === undefined) return null;\n if (!ids.includes(request.interruptId)) ids.push(request.interruptId);\n }\n return ids;\n}\n\n/** One decision for every lane — the `lane_decisions` map a run-wide approve or reject sends. */\nexport function fanLaneDecision<T>({\n interruptIds,\n decision,\n}: {\n interruptIds: readonly string[];\n decision: T;\n}): Record<string, T> {\n return Object.fromEntries(interruptIds.map((id) => [id, decision]));\n}\n\n/** The abandoned `interrupt` entry, if any — the \"approval was cancelled\" notice. */\nexport function findAbandonedInterrupt(\n abandoned: readonly AbandonedInputRequest[] | undefined,\n): AbandonedInputRequest | undefined {\n return abandoned?.find(\n (request) => isRecord(request) && request.type === 'interrupt' && typeof request.id === 'string',\n );\n}\n\n/** Canonical reason string carried by a user rejection. */\nexport const HITL_REJECT_REASON = 'User rejected the action.';\n\n/**\n * Default {@link UseApprovalResumeLockOptions.pendingResumeGraceMs} for every\n * approval surface: how long a clicked decision waits for its resume run to\n * start before the actions unlock again. Covers a `run/input` the host\n * rejected or a disconnected transport swallowed before any run began — the\n * one failure neither identity-change nor observed-run recovery can detect.\n */\nexport const HITL_PENDING_RESUME_GRACE_MS = 15_000;\n\n/**\n * Default {@link UseApprovalResumeLockOptions.maxTransportRetries}: how many\n * times a decision the transport lost (the websocket dropped or the statewire\n * session reset while the `run/input` was in flight) is re-sent on its own\n * before the user is asked to click again.\n */\nexport const HITL_RESUME_TRANSPORT_RETRIES = 3;\n\n/**\n * The dispatch died client-side without the host ever seeing it — a proven\n * session loss or a gone stream. Safe to re-send the same decision for the\n * same still-parked request. The complement (a server verdict, or the surface\n * unmounting) is not.\n */\nexport function isResumeTransportLoss(error: unknown): boolean {\n const outcome = statewireTransportOutcome(error);\n return outcome === 'session-lost' || outcome === 'stream-gone';\n}\n\n/** `run/input` resume value approving the gated call(s). */\nexport function hitlResumeApprove(): Record<string, string> {\n return { action: 'approve' };\n}\n\n/**\n * `run/input` resume value approving the gated call(s) AND every later call of\n * the same tool(s) on this thread. The backend records the tools from its own\n * parked interrupt (never from this payload), so the value carries no names.\n */\nexport function hitlResumeApproveForSession(): Record<string, string> {\n return { action: 'approve', scope: 'session' };\n}\n\n/** `run/input` resume value rejecting the gated call(s). */\nexport function hitlResumeReject(reason: string = HITL_REJECT_REASON): Record<string, string> {\n return { action: 'reject', reason };\n}\n\n/** One gated call's own answer inside a per-action resume ({@link hitlResumeActionDecisions}). */\nexport type HitlActionDecision =\n | { action: 'approve' }\n | { action: 'reject'; reason: string }\n | { action: 'edit'; edited_args: Record<string, unknown> };\n\n/**\n * The `tool_call_id` of every gated call, in decision order — or `null` when\n * any call lacks one or two calls share one, in which case no per-action\n * resume can be bound to the batch and only the batch answers above apply.\n */\nexport function actionRequestToolCallIds(\n actionRequests: readonly PendingActionRequest[],\n): string[] | null {\n if (actionRequests.length === 0) return null;\n const ids: string[] = [];\n for (const request of actionRequests) {\n if (request.toolCallId === undefined || ids.includes(request.toolCallId)) return null;\n ids.push(request.toolCallId);\n }\n return ids;\n}\n\n/**\n * `run/input` resume value deciding each gated call by its `tool_call_id` —\n * the only channel that carries an edited call. The backend binds every entry\n * to the parked request with that id, takes the tool NAME from its own\n * request (an edit can change a call's arguments, never which tool runs), and\n * refuses the whole answer — nothing runs — unless the map names exactly the\n * parked calls. `edited_args` is the complete argument set the call runs\n * with, not a patch. The envelope `action` mirrors the first entry for hosts\n * that only read the envelope; `action_decisions` is authoritative.\n */\nexport function hitlResumeActionDecisions(\n decisions: Readonly<Record<string, HitlActionDecision>>,\n): Record<string, unknown> {\n const [anchorId, anchor] = Object.entries(decisions)[0] ?? [];\n return {\n action: anchor?.action === 'reject' ? 'reject' : 'approve',\n ...(anchorId !== undefined && { tool_call_id: anchorId }),\n action_decisions: decisions,\n };\n}\n\n/** Canonical reason string carried by a user declining a non-HITL park. */\nexport const INTERRUPT_DECLINE_REASON = 'User declined the request.';\n\n/**\n * `run/input` resume value declining a non-HITL park. `{ action: 'reject' }`\n * is the one value every typed raiser treats as a clean user decline\n * (`FormCancelledError` / `ToolRejectedError`); a generic `continue` would be\n * rejected by their resume schemas and surface as a tool error the agent then\n * improvises around.\n */\nexport function interruptResumeDecline(\n reason: string = INTERRUPT_DECLINE_REASON,\n): Record<string, string> {\n return { action: 'reject', reason };\n}\n\nexport interface UseApprovalResumeLockOptions<TValue> {\n /** The pending interrupt's request id (`undefined` when nothing is parked). */\n requestId: string | undefined;\n /** The surface's thread `isRunning` read (mockable per surface). */\n isRunning: boolean;\n /**\n * Dispatch the `run/input` resume for `requestId` on the surface's transport.\n * Return the dispatch promise so a refused or lost command releases the lock\n * at once and reaches `onResumeRejected`; a `void` return leaves the\n * recovery paths below as the only release.\n */\n sendResume: (requestId: string, value: TValue) => void | Promise<unknown>;\n /**\n * The dispatch `sendResume` returned rejected — the host refused the\n * `run/input` or the transport lost it. The lock is already released when\n * this fires; the surface tells the user, since the request is back on\n * screen with nothing else saying why.\n */\n onResumeRejected?: (error: unknown, requestId: string) => void;\n /**\n * A dispatch the transport lost is being re-sent for the still-parked\n * request; `attempt` counts the retries so far (1-based). Diagnostics only —\n * the lock stays held across the retry.\n */\n onResumeRetry?: (error: unknown, requestId: string, attempt: number) => void;\n /**\n * Automatic re-sends of a decision the transport lost\n * ({@link isResumeTransportLoss}) while the request is still parked. `0`\n * disables retries. Defaults to {@link HITL_RESUME_TRANSPORT_RETRIES}.\n */\n maxTransportRetries?: number;\n /**\n * Re-enable the actions when no resume run started within this window —\n * covers a `run/input` that was rejected or lost before any run began, so\n * neither reset path below can fire. Omit to keep the pre-existing surface\n * behavior of staying locked until a resume run is observed.\n */\n pendingResumeGraceMs?: number;\n}\n\nexport interface ApprovalResumeLock<TValue> {\n /** True after a resume until the request identity changes (or recovery fires). */\n pending: boolean;\n /**\n * Send the resume value, locking the actions. No-op (returns `undefined`)\n * while pending or unparked. Returns the dispatch promise when `sendResume`\n * produced one, so a surface can hold its own state until the host settles.\n */\n resume: (value: TValue) => Promise<unknown> | undefined;\n}\n\n/**\n * Optimistic action lock shared by every approval surface: disable the\n * decision buttons the moment one is clicked, until the request identity\n * changes (it clears after the resume, or a fresh interrupt arrives). This\n * closes the double-click → duplicate-resume window.\n *\n * Recovery paths, in order of specificity:\n * 0. The dispatch itself rejected (a `sendResume` that returned a promise) —\n * released at once, and reported through `onResumeRejected`. A rejection\n * that is a transport loss (the socket dropped mid-flight) is re-sent up to\n * `maxTransportRetries` times first, the lock held throughout, so a flaky\n * connection does not hand the card back to the user for another click.\n * The returned promise settles with the final attempt.\n * 1. Identity change — the resume landed (or a new interrupt replaced the\n * request). Reset during render via React's \"previous value\" pattern.\n * 2. A resume run was observed starting and then settling with the request\n * still pending (failed/cancelled resume) — clear the lock so the user can\n * answer again.\n * 3. Optional grace window ({@link UseApprovalResumeLockOptions.pendingResumeGraceMs})\n * for a `run/input` that never started a run at all.\n */\nexport function useApprovalResumeLock<TValue = Record<string, string>>(\n options: UseApprovalResumeLockOptions<TValue>,\n): ApprovalResumeLock<TValue> {\n const {\n requestId,\n isRunning,\n sendResume,\n pendingResumeGraceMs,\n onResumeRejected,\n onResumeRetry,\n maxTransportRetries = HITL_RESUME_TRANSPORT_RETRIES,\n } = options;\n\n const [pending, setPending] = useState(false);\n // Bumped per re-send so the grace window restarts with each attempt.\n const [attempt, setAttempt] = useState(0);\n const prevRequestIdRef = useRef(requestId);\n if (prevRequestIdRef.current !== requestId) {\n prevRequestIdRef.current = requestId;\n if (pending) setPending(false);\n }\n\n // Track the resume run we kicked off (isRunning observed true→false while\n // pending) and clear `pending` when it ends with the request unresolved.\n const sawResumeRunRef = useRef(false);\n useEffect(() => {\n if (!pending) {\n sawResumeRunRef.current = false;\n return;\n }\n if (isRunning) {\n sawResumeRunRef.current = true;\n return;\n }\n if (sawResumeRunRef.current) {\n sawResumeRunRef.current = false;\n setPending(false);\n }\n }, [pending, isRunning]);\n\n // Pre-start failure recovery: re-enable after the grace window when no\n // resume run began; answering the still-parked request again is exactly\n // what the user needs to be able to do.\n useEffect(() => {\n if (!pending || pendingResumeGraceMs === undefined) return;\n const timer = setTimeout(() => {\n if (!sawResumeRunRef.current) setPending(false);\n }, pendingResumeGraceMs);\n return () => clearTimeout(timer);\n }, [pending, requestId, pendingResumeGraceMs, attempt]);\n\n const sendResumeRef = useRef(sendResume);\n sendResumeRef.current = sendResume;\n const onResumeRejectedRef = useRef(onResumeRejected);\n onResumeRejectedRef.current = onResumeRejected;\n const onResumeRetryRef = useRef(onResumeRetry);\n onResumeRetryRef.current = onResumeRetry;\n const maxTransportRetriesRef = useRef(maxTransportRetries);\n maxTransportRetriesRef.current = maxTransportRetries;\n const currentRequestIdRef = useRef(requestId);\n currentRequestIdRef.current = requestId;\n const pendingRef = useRef(pending);\n pendingRef.current = pending;\n // One token per `resume` call: a settle from an earlier submission must not\n // retry over, or release, a newer one for the same request.\n const submissionRef = useRef(0);\n\n const resume = useCallback(\n (value: TValue): Promise<unknown> | undefined => {\n if (requestId === undefined || pendingRef.current) return undefined;\n const submission = ++submissionRef.current;\n setPending(true);\n pendingRef.current = true;\n setAttempt(0);\n const first: unknown = sendResumeRef.current(requestId, value);\n if (!(first instanceof Promise)) return undefined;\n // The final failure is already reported through `onResumeRejected`;\n // callers that drop the promise must not get an unhandled rejection.\n const outcome = new Promise<unknown>((resolve, reject) => {\n const settle = (dispatched: Promise<unknown>, retries: number) => {\n dispatched.then(resolve, (error: unknown) => {\n // A late refusal for a request the lock has since moved past (the\n // park was re-keyed, or a newer answer is in flight) must neither\n // release that answer's lock nor be reported as a failure of it.\n // The optimistic fold hides the answered request until the fold\n // rolls back, so `undefined` here is not a departure.\n const current = currentRequestIdRef.current;\n if (\n submission !== submissionRef.current ||\n (current !== undefined && current !== requestId)\n ) {\n reject(error);\n return;\n }\n if (retries < maxTransportRetriesRef.current && isResumeTransportLoss(error)) {\n const next = retries + 1;\n onResumeRetryRef.current?.(error, requestId, next);\n setPending(true);\n pendingRef.current = true;\n setAttempt(next);\n const resent: unknown = sendResumeRef.current(requestId, value);\n if (resent instanceof Promise) {\n settle(resent, next);\n return;\n }\n resolve(undefined);\n return;\n }\n setPending(false);\n pendingRef.current = false;\n onResumeRejectedRef.current?.(error, requestId);\n reject(error);\n });\n };\n settle(first, 0);\n });\n outcome.catch(() => {});\n return outcome;\n },\n [requestId],\n );\n\n return { pending, resume };\n}\n"],"names":[],"mappings":"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"}
@@ -9,6 +9,7 @@ const runconfigContract = require("./generated/runconfig-contract.cjs");
9
9
  const messageWindow = require("./message-window.cjs");
10
10
  const optimisticContent = require("./optimistic-content.cjs");
11
11
  const statewireState = require("./statewire-state.cjs");
12
+ const voiceTurnGrouping = require("./voice-turn-grouping.cjs");
12
13
  const messageConverter = react.createMessageConverter(convertLangChainMessages.convertLangChainMessages);
13
14
  const wirePartsToOptimisticParts = (parts) => parts.flatMap((part) => {
14
15
  if (part.type === "text") return [part];
@@ -416,7 +417,7 @@ function convertDeepAgentState(state, {
416
417
  const sanitized = isLangGraphState(values) ? sanitizeLangChainMessages(values.messages, { toolOutputs }) : [];
417
418
  const tail = sanitized[sanitized.length - 1];
418
419
  const tailWithdrawn = tail !== void 0 && isWithdrawn(tail);
419
- const stateMessages = sanitized.filter((message) => !isWithdrawn(message));
420
+ const stateMessages = voiceTurnGrouping.groupSpokenUserTurns(sanitized.filter((message) => !isWithdrawn(message)));
420
421
  return {
421
422
  messages: messageConverter.toThreadMessages(
422
423
  [...withPendingAssistantMessage(stateMessages, isRunning && !tailWithdrawn), ...laneTail],
@@ -1 +1 @@
1
- {"version":3,"file":"messages.cjs","sources":["../../../../../agent-runtime/src/messages.ts"],"sourcesContent":["import { getExternalStoreMessages } from '@assistant-ui/core';\nimport { createMessageConverter } from '@assistant-ui/core/react';\nimport { convertLangChainMessages, type LangChainMessage } from '@assistant-ui/react-langgraph';\nimport { runsMaterialize, type StatewireThread } from '@assistant-ui/react-statewire';\nimport {\n ATHENA_MESSAGE_AUTHOR_KEY,\n type DeepAgentMessageAuthor,\n laneEntryAuthor,\n} from './message-author';\nimport { DEEP_AGENT_TRUNCATED_CONTENT_KEY } from './generated/runconfig-contract';\nimport {\n ATHENA_TRUNCATED_TOOL_OUTPUT_ARTIFACT_KEY,\n readTruncatedToolContent,\n} from './message-window';\nimport { buildOptimisticHumanContent, type OptimisticContentPart } from './optimistic-content';\nimport { readDeepAgentStreamValues } from './statewire-state';\nimport type { DeepAgentToolOutputStore } from './tool-outputs';\nimport type { DeepAgentWithdrawnTurns } from './withdrawn-turns';\nimport type {\n DeepAgentLaneProjection,\n DeepAgentStreamValues,\n LangGraphState,\n} from './types';\n\n/**\n * Native message converter for the deep-agent runtime.\n *\n * This is the entire \"conversion\" layer: assistant-ui's shipped\n * `convertLangChainMessages` (from `@assistant-ui/react-langgraph`) wired\n * through `createMessageConverter`. No bespoke per-message mapping — the agora\n * `athena_deep_agent` graph streams standard LangChain messages via\n * `append_langgraph_event`, and the shipped converter renders them.\n */\nexport const messageConverter = createMessageConverter(convertLangChainMessages);\n\nconst wirePartsToOptimisticParts = (\n parts: StatewireThread.UserMessage['parts']\n): OptimisticContentPart[] =>\n parts.flatMap((part): OptimisticContentPart[] => {\n if (part.type === 'text') return [part];\n // Image attachments travel as `file` parts on the run/* commands; map\n // them back to `image` parts so the optimistic bubble renders them.\n if (part.mediaType.startsWith('image/')) return [{ type: 'image', image: part.url }];\n return [];\n });\n\n/**\n * `metadata.custom` flag marking a bubble that is a projection of a run-lane\n * entry — a message the server holds and has NOT dispatched yet.\n *\n * The lane is server-owned and replicated to every client on the thread, so\n * an item queued on one surface projects onto all of them. Surfaces without a\n * queue panel must therefore be able to tell a queued projection apart from a\n * submitted turn, or a message another device queued renders as if this device\n * had already sent it.\n *\n * `convertLangChainMessages` exposes `additional_kwargs.metadata` (and nothing\n * else) as the thread message's `metadata.custom`.\n */\nexport const ATHENA_QUEUED_LANE_KEY = 'athena_queued_lane';\n\n/**\n * Whether a thread message's `metadata.custom` marks it as an undispatched\n * lane projection. Returns a primitive, so it is safe to use directly as a\n * `useAuiState` selector.\n */\nexport function isQueuedLaneMessage(custom: Record<string, unknown> | undefined): boolean {\n return custom?.[ATHENA_QUEUED_LANE_KEY] === true;\n}\n\n/**\n * Project one run-lane entry into the optimistic human message shape.\n *\n * `queued` marks entries still waiting in a lane; a dispatched entry becomes an\n * ordinary submitted turn and carries no marker. Either way the bubble carries\n * its author (see {@link laneEntryAuthor}) under the same `metadata.custom`\n * key the backend stamps on the persisted turn, so the optimistic and\n * authoritative copies of one send render identically — and a second poster's\n * avatar shows on their own send without waiting for the acknowledgment.\n */\nconst laneEntryMessage = ({\n entry,\n queued,\n selfAuthor,\n}: {\n entry: StatewireThread.RunsQueueEntry;\n queued: boolean;\n selfAuthor: DeepAgentMessageAuthor | null;\n}): LangChainMessage => {\n const author = laneEntryAuthor(entry, selfAuthor);\n const metadata: Record<string, unknown> = {\n ...(queued ? { [ATHENA_QUEUED_LANE_KEY]: true } : {}),\n ...(author ? { [ATHENA_MESSAGE_AUTHOR_KEY]: author } : {}),\n };\n return {\n id: entry.id,\n type: 'human',\n content: buildOptimisticHumanContent(wirePartsToOptimisticParts(entry.parts)),\n ...(Object.keys(metadata).length > 0 ? { additional_kwargs: { metadata } } : {}),\n };\n};\n\nexport interface DeepAgentMaterializeOptions {\n /** The viewer, stamped onto this client's own not-yet-acknowledged sends. */\n selfAuthor?: DeepAgentMessageAuthor | null;\n}\n\n/**\n * Materialize fold for the statewire path: makes the server's dispatch record\n * concrete in the LangGraph state itself, so direct state reads see dispatched\n * messages and the lib's post-converter dispatch merge stays off.\n */\nexport const createDeepAgentMaterialize =\n ({ selfAuthor = null }: DeepAgentMaterializeOptions = {}) =>\n (draft: DeepAgentStreamValues): void => {\n runsMaterialize(draft, {\n append: (message) => {\n (draft.messages ??= []).push(\n laneEntryMessage({ entry: message, queued: false, selfAuthor })\n );\n },\n rollback: (to) => {\n if (to === null) {\n draft.messages = [];\n return;\n }\n const messages = draft.messages ?? [];\n const keepIndex = messages.findIndex((message) => message.id === to);\n // Unresolved rollback anchor: fail OPEN with the full history (mirrors the edit fold).\n if (keepIndex !== -1) messages.splice(keepIndex + 1);\n },\n });\n };\n\n/** The materialize fold without a viewer identity (surfaces that pass none). */\nexport const deepAgentMaterialize = createDeepAgentMaterialize();\n\n/**\n * Project run lanes into human messages for the converter tail, so a queued or\n * steered item stays visible below the pending-assistant placeholder until its\n * dispatch. Surfaces with a queue panel project only the steer lane\n * (`'steer-only'`); surfaces without one project both lanes (`'both'`).\n *\n * Every projected entry carries {@link ATHENA_QUEUED_LANE_KEY} so a surface\n * without a queue panel can render it as queued rather than as a submitted\n * turn.\n */\nexport const laneTailMessages = (\n state: unknown,\n laneProjection: DeepAgentLaneProjection = 'both',\n selfAuthor: DeepAgentMessageAuthor | null = null\n): LangChainMessage[] => {\n const run = (state as DeepAgentStreamValues | null)?.runs?.[0];\n if (!run) return [];\n const entries =\n laneProjection === 'steer-only' ? run.steerQueue : [...run.queue, ...run.steerQueue];\n return entries.map((entry) => laneEntryMessage({ entry, queued: true, selfAuthor }));\n};\n\n/** Stable id for the derived running-assistant placeholder. */\nexport const PENDING_ASSISTANT_MESSAGE_ID = '__athena_deep_agent_pending_assistant__';\n\n/**\n * Append an empty assistant turn while a run is in flight and the visible\n * history does not already end in one, so the thread shows its \"Thinking…\"\n * status line from the moment a message is submitted.\n *\n * Mirrors `hasUpcomingMessage` in assistant-ui's legacy external-store runtime,\n * which synthesized this placeholder itself. `StatewireThread` renders the\n * converter's message list verbatim, so the placeholder has to come from here —\n * derived from the replicated run status on every render (never written into\n * transport state) so it cannot outlive the run, and carrying a stable id so\n * the streaming bubble's identity does not churn when the real assistant\n * message arrives.\n *\n * `tool` messages convert into assistant turns too, so a mid-run tool result\n * must not produce a second placeholder row.\n */\nexport function withPendingAssistantMessage(\n messages: LangChainMessage[],\n isRunning: boolean\n): LangChainMessage[] {\n if (!isRunning) return messages;\n const lastType = messages.at(-1)?.type;\n if (lastType === 'ai' || lastType === 'tool') return messages;\n // Empty-array content plus an explicit running status — the thinking shimmer\n // keys on `status running && zero content parts`, and the explicit status\n // keeps it pulsing when lane entries render below (the last-message auto\n // status would demote a mid-list placeholder to an empty complete bubble).\n // Never persisted, so it cannot outlive the run.\n return [\n ...messages,\n {\n type: 'ai',\n content: [],\n id: PENDING_ASSISTANT_MESSAGE_ID,\n status: { type: 'running' },\n } satisfies LangChainMessage,\n ];\n}\n\n/** Stable id for the synthetic assistant turn that carries a failure when the run produced none. */\nconst RUN_ERROR_SYNTHETIC_AI_ID = '__athena_deep_agent_run_error__';\n\n/**\n * Surface a run failure the base-correct way: stamp `status: { type:'incomplete',\n * reason:'error', error }` onto the assistant turn so `<Thread/>` renders it via\n * `ErrorPrimitive`. This is the core runtime's model (see `useLanggraphRuntime`\n * `onError`) — NOT a fabricated `⚠️ The run failed` assistant text message, and\n * NOT a magic-string contract shared with the agora backend.\n *\n * Stamps a trailing assistant message; if the visible history does not already\n * end with an assistant turn, appends a synthetic empty assistant turn with a\n * STABLE id so it never churns across renders. A `null` error is a no-op. The\n * shipped `convertLangChainMessages` converter passes `message.status` through.\n */\nexport function applyRunErrorStatus(\n messages: LangChainMessage[],\n errorMessage: string | null\n): LangChainMessage[] {\n if (errorMessage === null) return messages;\n const status = { type: 'incomplete', reason: 'error', error: errorMessage } as const;\n const lastMessage = messages.at(-1);\n if (lastMessage?.type !== 'ai') {\n return [\n ...messages,\n { type: 'ai', content: '', id: RUN_ERROR_SYNTHETIC_AI_ID, status } satisfies LangChainMessage,\n ];\n }\n return [...messages.slice(0, -1), { ...lastMessage, status }];\n}\n\n/**\n * `additional_kwargs` flag the backend stamps on run-lifecycle marker messages\n * (agora `athena_deep_agent/run_markers.py`) — e.g. the \"⏹️ Run stopped by\n * user.\" message persisted when a run is cancelled.\n */\nexport const ATHENA_RUN_MARKER_KEY = 'athena_run_marker';\n\n/** The {@link ATHENA_RUN_MARKER_KEY} value for a user-cancelled run. */\nexport const ATHENA_RUN_MARKER_CANCELLED = 'cancelled';\n\n/**\n * `additional_kwargs` key carrying the stop source behind a cancellation\n * marker (agora `stop_signals.py`: `user_stop`, `send_now`, `transport_cancel`,\n * `unattributed`).\n */\nexport const ATHENA_RUN_MARKER_SOURCE_KEY = 'athena_run_marker_source';\n\n/** The stop source for a \"Send now\" promotion pre-empting the live run. */\nexport const ATHENA_STOP_SOURCE_SEND_NOW = 'send_now';\n\n/**\n * The stop source for a spoken follow-up that redirected a voice-delegated\n * run through the steer lane: the user spoke, they never pressed Stop, so it\n * renders as a redirect rather than as \"Conversation stopped\".\n */\nexport const ATHENA_STOP_SOURCE_VOICE_STEER = 'voice_steer';\n\n/** The stop source for a pod-drain pause (server update), not a user stop. */\nexport const ATHENA_STOP_SOURCE_POD_DRAIN = 'pod_drain';\n\n/**\n * The {@link ATHENA_RUN_MARKER_SOURCE_KEY} value for a run whose hosting\n * instance closed under it (a server update that reached its eviction\n * deadline mid-step, or a placement lost to a lease lapse). Same family as a\n * pod drain: nobody stopped the run, the backend resumes it automatically.\n */\nexport const ATHENA_STOP_SOURCE_INSTANCE_CLOSED = 'instance_closed';\n\n/**\n * Whether a cancellation marker's source names the infrastructure — a pod\n * drain or a closed instance — rather than a person or a client action. These\n * endings read as a pause while the server resumes the run.\n */\nexport function isServerUpdateStopSource(source: string | undefined): boolean {\n return source === ATHENA_STOP_SOURCE_POD_DRAIN || source === ATHENA_STOP_SOURCE_INSTANCE_CLOSED;\n}\n\n/**\n * The {@link ATHENA_RUN_MARKER_KEY} value for a failed run (graph error,\n * including the `GraphRecursionError` \"tool call limit was reached\" case).\n * The marker message's text content carries the user-facing failure copy.\n */\nexport const ATHENA_RUN_MARKER_FAILED = 'failed';\n\n/**\n * `additional_kwargs` key carrying the canonical reason a run ended (agora\n * `athena_deep_agent/termination.py`): `user_stop`, `recursion_limit`,\n * `duplicate_call_guard`, `step_budget`, `ended_after_tools`, `model_refusal`,\n * `completed`, …\n * Stamped on run-lifecycle markers and on handoff messages.\n */\nexport const ATHENA_TERMINATION_REASON_KEY = 'athena_termination_reason';\n\n/**\n * `additional_kwargs` flag on the closing message the runtime's **handoff\n * turn** wrote (one tools-disabled model call made when a guard or the step\n * budget ends the run). The message text is the agent's closing summary; the\n * flag lets clients frame it as a runtime ending rather than a normal reply.\n */\nexport const ATHENA_HANDOFF_KEY = 'athena_handoff';\n\n/**\n * Read a run-lifecycle marker out of a thread message's **bound source\n * messages**, bypassing the converter's metadata join entirely.\n *\n * Why this exists: assistant-ui chunks consecutive assistant/tool messages\n * into ONE `ThreadMessage`, and the upstream join copies `metadata.custom`\n * only from the FIRST assistant message of the chunk\n * (`external-message-converter.ts`: the merge sits inside\n * `if (assistantMessage.content.length === 0)`). A failure marker appended\n * after the assistant's own turn is therefore never the first message in its\n * chunk, so `message.metadata.custom.athena_run_marker` is silently dropped\n * and the whole-message gate can't fire.\n *\n * `getExternalStoreMessages` returns the raw LangChain messages bound to the\n * joined thread message, so the marker is found wherever it sits in the chunk.\n * Returns the marker value plus its text content (the user-facing copy).\n */\nexport interface RunMarkerInfo {\n marker: string;\n /** The stop source stamped by the backend, when the marker carries one. */\n source: string | undefined;\n /** The marker message's text — the user-facing copy. */\n text: string | undefined;\n /**\n * True when the marker is the ONLY source message behind this thread message,\n * i.e. it owns the whole row (a cancelled run drops its partial turn, so its\n * marker stands alone).\n *\n * False when the chunk also holds the assistant's own work. Replacing the row\n * in that case would hide completed prose and tool cards — the marker must be\n * styled in place instead.\n */\n isSoleSourceMessage: boolean;\n}\n\nexport function readRunMarkerFromSourceMessages(\n message: Parameters<typeof getExternalStoreMessages>[0] & {\n metadata?: { custom?: Record<string, unknown> | undefined } | undefined;\n content?: unknown;\n }\n): RunMarkerInfo | undefined {\n const sources = getExternalStoreMessages<LangChainMessage>(message);\n for (const source of sources) {\n if (source?.type !== 'ai') continue;\n const kwargs = source.additional_kwargs as Record<string, unknown> | undefined;\n const marker = kwargs?.[ATHENA_RUN_MARKER_KEY];\n if (typeof marker !== 'string') continue;\n const markerSource = kwargs?.[ATHENA_RUN_MARKER_SOURCE_KEY];\n return {\n marker,\n source: typeof markerSource === 'string' ? markerSource : undefined,\n text: typeof source.content === 'string' ? source.content : undefined,\n isSoleSourceMessage: sources.length === 1,\n };\n }\n // Fallback for surfaces whose messages carry no bound source messages (or a\n // converter that already merged the flag): read the joined metadata. Without\n // sources to count, a single text part is the best available \"marker owns the\n // row\" signal.\n const joined = message.metadata?.custom?.[ATHENA_RUN_MARKER_KEY];\n if (typeof joined !== 'string') return undefined;\n const joinedSource = message.metadata?.custom?.[ATHENA_RUN_MARKER_SOURCE_KEY];\n const parts = message.content;\n return {\n marker: joined,\n source: typeof joinedSource === 'string' ? joinedSource : undefined,\n text: undefined,\n isSoleSourceMessage: Array.isArray(parts) ? parts.length <= 1 : true,\n };\n}\n\n/**\n * The marker value when the marker is the SOLE source message behind a thread\n * message (it owns the whole row), else `undefined`.\n *\n * Returns a primitive so it is safe to use directly as a `useAuiState`\n * selector: `readRunMarkerFromSourceMessages` builds a fresh object on every\n * call, and selector snapshots are compared with `Object.is` — returning that\n * object from a selector makes `getSnapshot` unstable and loops the render.\n */\nexport function readSoleRunMarker(\n message: Parameters<typeof readRunMarkerFromSourceMessages>[0]\n): string | undefined {\n const found = readRunMarkerFromSourceMessages(message);\n return found?.isSoleSourceMessage ? found.marker : undefined;\n}\n\n/**\n * Whether a run marker renders nothing: a `send_now` promotion cancels the\n * live run as plumbing, not as a user-visible stop, so its cancellation\n * marker stays in the data for provenance but paints no divider.\n */\nexport function isSilentRunMarker(found: RunMarkerInfo): boolean {\n return (\n found.marker === ATHENA_RUN_MARKER_CANCELLED && found.source === ATHENA_STOP_SOURCE_SEND_NOW\n );\n}\n\n/**\n * Whether a cancellation marker records a server-update pause — a cooperative\n * pod drain, or the hosting instance closing under the run — rather than a\n * stop the user asked for. Rendered with distinct \"paused by a server update\"\n * copy instead of the \"Conversation stopped\" pill; the backend resumes the run.\n */\nexport function isDrainRunMarker(found: RunMarkerInfo): boolean {\n return found.marker === ATHENA_RUN_MARKER_CANCELLED && isServerUpdateStopSource(found.source);\n}\n\n/**\n * Surface the backend's top-level `additional_kwargs.athena_run_marker` flag\n * where the shipped converter can see it.\n *\n * `convertLangChainMessages` exposes ONLY `additional_kwargs.metadata` as the\n * thread message's `metadata.custom`; every other `additional_kwargs` key is\n * dropped. Marker messages persisted before the backend mirrored the flag into\n * `metadata` carry it top-level only, so this lift keeps those threads styling\n * correctly. Non-marker messages pass through with their identity unchanged,\n * and already-mirrored markers are left as-is (idempotent).\n */\nexport function liftRunMarkerIntoMetadata(message: LangChainMessage): LangChainMessage {\n if (message.type !== 'ai') return message;\n const kwargs = message.additional_kwargs as Record<string, unknown> | undefined;\n const marker = kwargs?.[ATHENA_RUN_MARKER_KEY];\n if (typeof marker !== 'string') return message;\n const existingMetadata = kwargs?.metadata;\n const metadata =\n typeof existingMetadata === 'object' && existingMetadata !== null\n ? (existingMetadata as Record<string, unknown>)\n : {};\n if (metadata[ATHENA_RUN_MARKER_KEY] === marker) return message;\n return {\n ...message,\n additional_kwargs: {\n ...kwargs,\n metadata: { ...metadata, [ATHENA_RUN_MARKER_KEY]: marker },\n } as (typeof message)['additional_kwargs'],\n };\n}\n\n/**\n * Mirror the termination reason and handoff flag into the converter's metadata\n * channel. The backend dual-writes both (top-level + `metadata`), so this lift\n * is the back-compat path for messages persisted before the mirror existed and\n * for any emitter that only stamps the top-level key. Idempotent; identity for\n * messages without the keys.\n */\nexport function liftTerminationIntoMetadata(message: LangChainMessage): LangChainMessage {\n if (message.type !== 'ai') return message;\n const kwargs = message.additional_kwargs as Record<string, unknown> | undefined;\n const reason = kwargs?.[ATHENA_TERMINATION_REASON_KEY];\n const handoff = kwargs?.[ATHENA_HANDOFF_KEY];\n const hasReason = typeof reason === 'string';\n const hasHandoff = handoff === true;\n if (!hasReason && !hasHandoff) return message;\n const existingMetadata = kwargs?.metadata;\n const metadata =\n typeof existingMetadata === 'object' && existingMetadata !== null\n ? (existingMetadata as Record<string, unknown>)\n : {};\n const reasonMirrored = !hasReason || metadata[ATHENA_TERMINATION_REASON_KEY] === reason;\n const handoffMirrored = !hasHandoff || metadata[ATHENA_HANDOFF_KEY] === true;\n if (reasonMirrored && handoffMirrored) return message;\n return {\n ...message,\n additional_kwargs: {\n ...kwargs,\n metadata: {\n ...metadata,\n ...(hasReason ? { [ATHENA_TERMINATION_REASON_KEY]: reason } : {}),\n ...(hasHandoff ? { [ATHENA_HANDOFF_KEY]: true } : {}),\n },\n } as (typeof message)['additional_kwargs'],\n };\n}\n\n/**\n * `additional_kwargs` key (and `metadata` mirror) carrying a failure marker's\n * structured error records (agora `athena_deep_agent/run_markers.py`\n * `failure_error_records`): `[{ error, provider?, message? }]`, where `error`\n * is the client code a surface branches on — `context_length_exceeded`,\n * `transient_api_error` (with `provider: 'azure_foundry' |\n * 'external_model_provider'`), `content_filter`, `request_too_large`, … — the\n * same shape the legacy agent node emitted.\n */\nexport const ATHENA_ERRORS_KEY = 'errors';\n\n/**\n * Mirror a failure marker's structured `errors` records into the converter's\n * metadata channel. The backend dual-writes them (top-level + `metadata`), so\n * this lift is the back-compat path for messages that carry only the top-level\n * key (an emitter that never mirrored, or a record persisted before the mirror\n * existed). Records already present in the metadata channel win; no-ops are\n * identity.\n */\nexport function liftErrorsIntoMetadata(message: LangChainMessage): LangChainMessage {\n if (message.type !== 'ai') return message;\n const kwargs = message.additional_kwargs as Record<string, unknown> | undefined;\n const errors = kwargs?.[ATHENA_ERRORS_KEY];\n if (!Array.isArray(errors) || errors.length === 0) return message;\n const existingMetadata = kwargs?.metadata;\n const metadata =\n typeof existingMetadata === 'object' && existingMetadata !== null\n ? (existingMetadata as Record<string, unknown>)\n : {};\n if (Array.isArray(metadata[ATHENA_ERRORS_KEY])) return message;\n return {\n ...message,\n additional_kwargs: {\n ...kwargs,\n metadata: { ...metadata, [ATHENA_ERRORS_KEY]: errors },\n } as (typeof message)['additional_kwargs'],\n };\n}\n\n/**\n * Read a failure's structured `errors` records out of a thread message's\n * **bound source messages**, bypassing the converter's metadata join.\n *\n * Same reason as {@link readRunMarkerFromSourceMessages}: assistant-ui chunks\n * consecutive assistant/tool messages into ONE `ThreadMessage` and copies\n * `metadata.custom` only from the FIRST assistant message of the chunk. The\n * failure marker that carries the records is appended after the assistant's\n * own turn, so `message.metadata.custom.errors` is silently dropped whenever\n * the run produced anything before failing. The bound sources are walked from\n * the tail (the terminal failure wins); the joined metadata is the fallback\n * for surfaces whose messages carry no bound sources. Returns the raw records\n * (callers validate the shape) or `undefined`.\n */\nexport function readMessageErrorsFromSourceMessages(\n message: Parameters<typeof getExternalStoreMessages>[0] & {\n metadata?: { custom?: Record<string, unknown> | undefined } | undefined;\n }\n): unknown[] | undefined {\n const sources = getExternalStoreMessages<LangChainMessage>(message);\n for (let index = sources.length - 1; index >= 0; index -= 1) {\n const source = sources[index];\n if (source?.type !== 'ai') continue;\n const kwargs = source.additional_kwargs as Record<string, unknown> | undefined;\n const metadata = kwargs?.metadata;\n const mirrored =\n typeof metadata === 'object' && metadata !== null\n ? (metadata as Record<string, unknown>)[ATHENA_ERRORS_KEY]\n : undefined;\n const errors = Array.isArray(mirrored) ? mirrored : kwargs?.[ATHENA_ERRORS_KEY];\n if (Array.isArray(errors) && errors.length > 0) return errors;\n }\n const joined = message.metadata?.custom?.[ATHENA_ERRORS_KEY];\n return Array.isArray(joined) && joined.length > 0 ? joined : undefined;\n}\n\n/**\n * Mirror the producing model into the converter's metadata channel. Wire AI\n * messages are full LangChain dumps whose `response_metadata` reports the model\n * that generated them (`model_name` / `model`), but the shipped converter maps\n * only `additional_kwargs.metadata` into `metadata.custom` — so without this\n * lift a surface can only show the CURRENT run config's model, mislabeling\n * earlier messages after a mid-thread model switch. A model already present in\n * the metadata channel (backend-stamped provenance) wins; no-ops are identity.\n */\nexport function liftModelIntoMetadata(message: LangChainMessage): LangChainMessage {\n if (message.type !== 'ai') return message;\n const responseMetadata = (\n message as { response_metadata?: { model_name?: unknown; model?: unknown } }\n ).response_metadata;\n let model: string | undefined;\n if (typeof responseMetadata?.model_name === 'string' && responseMetadata.model_name) {\n model = responseMetadata.model_name;\n } else if (typeof responseMetadata?.model === 'string' && responseMetadata.model) {\n model = responseMetadata.model;\n }\n if (!model) return message;\n const kwargs = message.additional_kwargs as Record<string, unknown> | undefined;\n const existingMetadata = kwargs?.metadata;\n const metadata =\n typeof existingMetadata === 'object' && existingMetadata !== null\n ? (existingMetadata as Record<string, unknown>)\n : {};\n if (typeof metadata.model === 'string' && metadata.model) return message;\n return {\n ...message,\n additional_kwargs: {\n ...kwargs,\n metadata: { ...metadata, model },\n } as (typeof message)['additional_kwargs'],\n };\n}\n\n/**\n * Mirror a per-message conversation-stats snapshot into the converter's\n * metadata channel. Backends stamp the snapshot at the legacy location —\n * top-level `additional_kwargs.conversation_stats` (the deep-agent stats\n * middleware and the legacy agent node both write it there) — but the shipped\n * converter maps only `additional_kwargs.metadata` into `metadata.custom`, so\n * without this lift the model badge can never show a message's OWN context and\n * falls back to thread-level live numbers. A snapshot already present in the\n * metadata channel wins; no-ops are identity.\n */\nexport function liftConversationStatsIntoMetadata(message: LangChainMessage): LangChainMessage {\n if (message.type !== 'ai') return message;\n const kwargs = message.additional_kwargs as Record<string, unknown> | undefined;\n const stats = kwargs?.conversation_stats;\n if (typeof stats !== 'object' || stats === null) return message;\n const existingMetadata = kwargs?.metadata;\n const metadata =\n typeof existingMetadata === 'object' && existingMetadata !== null\n ? (existingMetadata as Record<string, unknown>)\n : {};\n if (typeof metadata.conversation_stats === 'object' && metadata.conversation_stats !== null) {\n return message;\n }\n return {\n ...message,\n additional_kwargs: {\n ...kwargs,\n metadata: { ...metadata, conversation_stats: stats },\n } as (typeof message)['additional_kwargs'],\n };\n}\n\n/**\n * Mirror the compaction boundary flag into the converter's metadata channel.\n * A manual `/compact` persists its summary as a human message tagged with\n * top-level `additional_kwargs.compaction_start` (plus `_created_at` for the\n * divider timestamp) — the same location the legacy runtime used — but the\n * shipped converter maps only `additional_kwargs.metadata` into\n * `metadata.custom`, so without this lift the transcript's\n * \"Previous conversation summarized\" divider can never render. A flag already\n * present in the metadata channel wins; no-ops are identity.\n */\nexport function liftCompactionBoundaryIntoMetadata(message: LangChainMessage): LangChainMessage {\n if (message.type !== 'human') return message;\n const kwargs = message.additional_kwargs as Record<string, unknown> | undefined;\n if (kwargs?.compaction_start !== true) return message;\n const existingMetadata = kwargs?.metadata;\n const metadata =\n typeof existingMetadata === 'object' && existingMetadata !== null\n ? (existingMetadata as Record<string, unknown>)\n : {};\n if (metadata.compaction_start === true) return message;\n const createdAt = kwargs?._created_at;\n return {\n ...message,\n additional_kwargs: {\n ...kwargs,\n metadata: {\n ...metadata,\n compaction_start: true,\n ...(typeof createdAt === 'string' ? { _created_at: createdAt } : {}),\n },\n } as (typeof message)['additional_kwargs'],\n };\n}\n\n/**\n * Mirror the handoff summary flag into the converter's metadata channel.\n * Handoff summaries are persisted as human messages tagged with top-level\n * `additional_kwargs.handoff_summary` (plus `_created_at`) — the same location\n * the backend uses for message metadata — but the shipped converter maps only\n * `additional_kwargs.metadata` into `metadata.custom`, so without this lift\n * the summary would render as a normal user bubble. A flag already present in\n * the metadata channel wins; no-ops are identity.\n */\nexport function liftHandoffSummaryIntoMetadata(message: LangChainMessage): LangChainMessage {\n if (message.type !== 'human') return message;\n const kwargs = message.additional_kwargs as Record<string, unknown> | undefined;\n if (kwargs?.handoff_summary !== true) return message;\n const existingMetadata = kwargs?.metadata;\n const metadata =\n typeof existingMetadata === 'object' && existingMetadata !== null\n ? (existingMetadata as Record<string, unknown>)\n : {};\n if (metadata.handoff_summary === true) return message;\n const createdAt = kwargs?._created_at;\n return {\n ...message,\n additional_kwargs: {\n ...kwargs,\n metadata: {\n ...metadata,\n handoff_summary: true,\n ...(typeof createdAt === 'string' ? { _created_at: createdAt } : {}),\n },\n } as (typeof message)['additional_kwargs'],\n };\n}\n\n/**\n * Mirror an AOP run's provenance into the converter's metadata channel. The\n * backend stamps the kicking-off user message with top-level\n * `additional_kwargs.aop_source` (agora `events/helpers.py`\n * `_build_aop_source_metadata`), which the legacy converter surfaced as\n * `metadata.custom.aop_source` — where the \"AOP · <title>\" badge reads it. The\n * shipped `convertLangChainMessages` maps only `additional_kwargs.metadata`\n * into `metadata.custom`, so without this lift an AOP run's conversation never\n * shows which AOP asset kicked it off. Provenance already present in the\n * metadata channel wins; no-ops are identity.\n */\nexport function liftAopSourceIntoMetadata(message: LangChainMessage): LangChainMessage {\n if (message.type !== 'human') return message;\n const kwargs = message.additional_kwargs as Record<string, unknown> | undefined;\n const aopSource = kwargs?.aop_source;\n if (typeof aopSource !== 'object' || aopSource === null || Array.isArray(aopSource)) {\n return message;\n }\n const existingMetadata = kwargs?.metadata;\n const metadata =\n typeof existingMetadata === 'object' && existingMetadata !== null\n ? (existingMetadata as Record<string, unknown>)\n : {};\n if (typeof metadata.aop_source === 'object' && metadata.aop_source !== null) return message;\n return {\n ...message,\n additional_kwargs: {\n ...kwargs,\n metadata: { ...metadata, aop_source: aopSource },\n } as (typeof message)['additional_kwargs'],\n };\n}\n\nexport function liftBranchIntoMetadata(message: LangChainMessage): LangChainMessage {\n if (message.type !== 'human' && message.type !== 'ai') return message;\n const branch = (message as { branch?: unknown }).branch;\n if (typeof branch !== 'object' || branch === null || Array.isArray(branch)) return message;\n const kwargs = message.additional_kwargs as Record<string, unknown> | undefined;\n const existingMetadata = kwargs?.metadata;\n const metadata =\n typeof existingMetadata === 'object' && existingMetadata !== null\n ? (existingMetadata as Record<string, unknown>)\n : {};\n if (typeof metadata.branch === 'object' && metadata.branch !== null) return message;\n return {\n ...message,\n additional_kwargs: {\n ...kwargs,\n metadata: { ...metadata, branch },\n } as (typeof message)['additional_kwargs'],\n };\n}\n\n/**\n * `additional_kwargs` keys on a ToolMessage that are infrastructure rather than\n * user-set metadata. The filter below additionally skips every `_`-prefixed key.\n */\nconst TOOL_MESSAGE_INTERNAL_KWARGS: ReadonlySet<string> = new Set([\n 'reasoning',\n 'tool_outputs',\n 'hide_from_frontend',\n]);\n\n/**\n * Mirror a ToolMessage's user-set `additional_kwargs` into\n * `artifact._tool_message_metadata` — the location `useToolCallMetadata` reads\n * persisted per-tool-call metadata from (`email_sent_at`, draft edits,\n * `calendar_event_created_at`, …).\n *\n * The `setMessageMetadata` mutation persists those keys onto the ToolMessage's\n * `additional_kwargs` in the checkpoint, and the legacy converter lifted them\n * into the artifact. The shipped `convertLangChainMessages` forwards only\n * `artifact` and drops `additional_kwargs` for tool messages, so without this\n * lift the metadata survives in the checkpoint but never reaches a tool card\n * after a reload. Kwargs values win over same-key artifact entries (the kwargs\n * are the durable write target); messages with no user-set kwargs pass through\n * by identity.\n */\nexport function liftToolMessageMetadataIntoArtifact(message: LangChainMessage): LangChainMessage {\n if (message.type !== 'tool') return message;\n const kwargs = (message as { additional_kwargs?: Record<string, unknown> }).additional_kwargs;\n if (!kwargs) return message;\n const lifted: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(kwargs)) {\n if (key.startsWith('_') || TOOL_MESSAGE_INTERNAL_KWARGS.has(key)) continue;\n lifted[key] = value;\n }\n if (Object.keys(lifted).length === 0) return message;\n const artifact: unknown = message.artifact;\n const artifactRecord =\n typeof artifact === 'object' && artifact !== null && !Array.isArray(artifact)\n ? (artifact as Record<string, unknown>)\n : {};\n const existing = artifactRecord._tool_message_metadata;\n const existingRecord =\n typeof existing === 'object' && existing !== null && !Array.isArray(existing)\n ? (existing as Record<string, unknown>)\n : {};\n return {\n ...message,\n artifact: {\n ...artifactRecord,\n _tool_message_metadata: { ...existingRecord, ...lifted },\n },\n };\n}\n\n/**\n * Lift a windowed wire's tool-output truncation marker into the tool-call\n * part's `artifact` (`_athena_content_truncated: { chars, messageId }`).\n *\n * The host cuts long plain-text tool outputs on a windowed wire and stamps\n * `athena_content_truncated` on the wire message; the shipped converter joins\n * a ToolMessage into its assistant turn's tool-call part and keeps only\n * `artifact` and the result, so without this lift no tool card could tell the\n * output is cut nor which message to ask `history/tool_output` for. Messages\n * without the marker pass through by identity.\n */\nexport function liftTruncatedToolOutputIntoArtifact(message: LangChainMessage): LangChainMessage {\n if (message.type !== 'tool') return message;\n const marker = readTruncatedToolContent(message);\n if (marker === null || typeof message.id !== 'string') return message;\n const artifact: unknown = message.artifact;\n const artifactRecord =\n typeof artifact === 'object' && artifact !== null && !Array.isArray(artifact)\n ? (artifact as Record<string, unknown>)\n : {};\n return {\n ...message,\n artifact: {\n ...artifactRecord,\n [ATHENA_TRUNCATED_TOOL_OUTPUT_ARTIFACT_KEY]: { chars: marker.chars, messageId: message.id },\n },\n };\n}\n\n/**\n * Per-source cache for {@link substituteToolOutput}: a substituted copy is\n * reused while the wire message and the fetched output are both unchanged, so\n * the lift cache downstream keeps its identity contract.\n */\nconst substitutedToolOutputCache = new WeakMap<\n LangChainMessage,\n { content: unknown; substituted: LangChainMessage }\n>();\n\n/**\n * A cut tool message with its full output put back, when the store holds one\n * fetched for this very output (`history/tool_output` answered for the same\n * `chars` the wire's marker reports); the marker leaves with the cut.\n * Messages that are whole, whose output has not been fetched, or whose fetched\n * output belongs to a length the host has since replaced, pass through by\n * identity — the loader sees the mismatch and fetches again.\n */\nexport function substituteToolOutput(\n message: LangChainMessage,\n toolOutputs: DeepAgentToolOutputStore\n): LangChainMessage {\n if (message.type !== 'tool' || typeof message.id !== 'string') return message;\n const marker = readTruncatedToolContent(message);\n if (marker === null) return message;\n const entry = toolOutputs.entry(message.id);\n if (entry === undefined || entry.chars !== marker.chars) return message;\n const { content } = entry;\n const cached = substitutedToolOutputCache.get(message);\n if (cached !== undefined && cached.content === content) return cached.substituted;\n const { [DEEP_AGENT_TRUNCATED_CONTENT_KEY]: _marker, ...rest } = message as Record<\n string,\n unknown\n >;\n const substituted = { ...rest, content } as unknown as LangChainMessage;\n substitutedToolOutputCache.set(message, { content, substituted });\n return substituted;\n}\n\nconst hasHiddenFrontendFlag = (value: unknown): boolean => {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n return false;\n }\n const record = value as Record<string, unknown>;\n if (record.hide_from_frontend === true) return true;\n return hasHiddenFrontendFlag(record.custom);\n};\n\n/**\n * Internal orchestration messages are useful in a trace export but must never\n * become conversation bubbles. Agora stores the flag in `additional_kwargs`;\n * live converter state can expose the same value under `metadata.custom`.\n */\nexport function isMessageHiddenFromFrontend(message: unknown): boolean {\n if (typeof message !== 'object' || message === null || Array.isArray(message)) {\n return false;\n }\n const record = message as Record<string, unknown>;\n return (\n hasHiddenFrontendFlag(record.additional_kwargs) || hasHiddenFrontendFlag(record.metadata)\n );\n}\n\n/**\n * The message `type`s `convertLangChainMessages` actually maps: its\n * `switch (message.type)` covers exactly these four and has no `default`, so it\n * returns `undefined` for anything else (a `RemoveMessage` `type: \"remove\"`\n * from summarization/compaction, a partial chunk, a future type).\n */\nconst CONVERTIBLE_MESSAGE_TYPES: ReadonlySet<string> = new Set(['system', 'human', 'ai', 'tool']);\n\n/**\n * Whether `convertLangChainMessages` can map this message at all.\n *\n * An unmappable entry converts to `undefined`, which assistant-ui's\n * `convertExternalMessages` wraps as `outputs: [undefined]` and then reads\n * `output.role` off — throwing `Cannot read property 'role' of undefined` and\n * taking the entire thread down instead of dropping one message.\n */\nexport function isConvertibleChatMessage(message: unknown): boolean {\n if (typeof message !== 'object' || message === null || Array.isArray(message)) {\n return false;\n }\n const type: unknown = (message as Record<string, unknown>).type;\n return typeof type === 'string' && CONVERTIBLE_MESSAGE_TYPES.has(type);\n}\n\n/**\n * Per-source cache for the lift pipeline in {@link sanitizeLangChainMessages}.\n *\n * The pipeline runs on EVERY state pass (once per streamed token), and the two\n * lifts that fire for essentially every message — {@link liftBranchIntoMetadata}\n * (the backend stamps `branch` on all indexed messages) and\n * {@link liftModelIntoMetadata} — allocate a fresh object each time they lift.\n * Without a cache, every message's identity churns on every token, defeating\n * all downstream per-source memoization (`getExternalStoreMessages`-keyed\n * reads, converter callback caches) and re-deriving the whole thread per token.\n *\n * Object identity is a sound cache key here: every statewire fold (op apply,\n * optimistic, materialize) runs through `statewire`'s copy-on-write\n * `recordMutations`, whose `finalize` returns the ORIGINAL object for\n * untouched nodes and a fresh copy for touched ones. A message is therefore\n * REPLACED — never mutated in place — whenever any of its fields change\n * (including the streaming tail's string-insert content growth), and an\n * unchanged message keeps its exact identity across passes. The corpus test\n * (`statewire-corpus.test.ts`) pins this identity contract against the real\n * client, so a statewire upgrade that switched to in-place mutation would fail\n * there rather than silently serving stale lifts from this cache.\n *\n * Module-level on purpose: keys are the wire message objects themselves, so\n * entries die with their state trees and threads cannot collide.\n */\nconst liftedMessageCache = new WeakMap<LangChainMessage, LangChainMessage>();\n\n/** The full lift chain for one message, memoized per source object. */\nfunction liftMessageIntoMetadataChannels(message: LangChainMessage): LangChainMessage {\n const cached = liftedMessageCache.get(message);\n if (cached !== undefined) return cached;\n const lifted = liftBranchIntoMetadata(\n liftTruncatedToolOutputIntoArtifact(\n liftToolMessageMetadataIntoArtifact(\n liftAopSourceIntoMetadata(\n liftHandoffSummaryIntoMetadata(\n liftCompactionBoundaryIntoMetadata(\n liftConversationStatsIntoMetadata(\n liftModelIntoMetadata(\n liftErrorsIntoMetadata(\n liftTerminationIntoMetadata(liftRunMarkerIntoMetadata(message))\n )\n )\n )\n )\n )\n )\n )\n )\n );\n liftedMessageCache.set(message, lifted);\n return lifted;\n}\n\n/**\n * The one message pipeline every deep-agent surface renders through:\n *\n * 1. drop messages the shipped converter cannot map (`RemoveMessage` etc. —\n * an unmappable entry would crash the whole thread) and internal\n * orchestration messages flagged hidden-from-frontend;\n * 2. lift run-lifecycle markers into the converter's `metadata` channel\n * (see {@link liftRunMarkerIntoMetadata}) so the cancellation row styles as\n * a status divider instead of a plain assistant bubble, and the termination\n * reason + handoff flag alongside them (see\n * {@link liftTerminationIntoMetadata}) so every surface can render a\n * runtime ending (see `runtime-messages.ts`);\n * 3. lift each AI message's producing model into the same channel\n * (see {@link liftModelIntoMetadata}) so model badges reflect the model that\n * actually generated the message, not the currently configured one;\n * 4. lift each AI message's per-message conversation-stats snapshot into the\n * same channel (see {@link liftConversationStatsIntoMetadata}) so the model\n * badge's hover card shows that message's own context usage;\n * 5. lift the compaction boundary flag on `/compact` summary messages into the\n * same channel (see {@link liftCompactionBoundaryIntoMetadata}) so the\n * transcript renders its \"Previous conversation summarized\" divider;\n * 6. lift the handoff summary flag on carried-over context messages into the\n * same channel (see {@link liftHandoffSummaryIntoMetadata}) so the\n * transcript keeps the summary behind its context panel;\n * 7. lift an AOP run's provenance on its kicking-off user message into the\n * same channel (see {@link liftAopSourceIntoMetadata}) so the message\n * renders its \"AOP · <title>\" origin badge;\n * 8. lift each ToolMessage's user-set `additional_kwargs` into\n * `artifact._tool_message_metadata` (see\n * {@link liftToolMessageMetadataIntoArtifact}) so persisted per-tool-call\n * metadata reaches tool cards after a reload;\n * 9. lift a windowed wire's tool-output truncation marker into the tool-call\n * part's `artifact` (see {@link liftTruncatedToolOutputIntoArtifact}) so a\n * tool card can fetch the whole output — after first putting back any\n * output already fetched (see {@link substituteToolOutput});\n * 10. lift the host's per-message branch stamp into the same channel\n * (see {@link liftBranchIntoMetadata}) so the branch picker renders each\n * message's edit fan-out.\n *\n * Identity-stable per source: an unchanged source message maps to the\n * IDENTICAL lifted object across passes (see {@link liftedMessageCache}).\n */\nexport function sanitizeLangChainMessages(\n messages: readonly LangChainMessage[],\n { toolOutputs }: { toolOutputs?: DeepAgentToolOutputStore } = {}\n): LangChainMessage[] {\n return messages\n .filter((message) => isConvertibleChatMessage(message) && !isMessageHiddenFromFrontend(message))\n .map((message) =>\n liftMessageIntoMetadataChannels(\n toolOutputs ? substituteToolOutput(message, toolOutputs) : message\n )\n );\n}\n\n/** Type guard for the runtime state — an object carrying a `messages` array. */\nexport function isLangGraphState(value: unknown): value is LangGraphState {\n return (\n typeof value === 'object' &&\n value !== null &&\n 'messages' in value &&\n Array.isArray((value as LangGraphState).messages)\n );\n}\n\nexport interface ConvertDeepAgentStateOptions {\n laneProjection?: DeepAgentLaneProjection;\n /**\n * The viewer, as the backend would stamp them: the author of this client's\n * own not-yet-acknowledged lane entries (see {@link laneEntryAuthor}).\n */\n selfAuthor?: DeepAgentMessageAuthor | null;\n /**\n * Full tool outputs fetched back for a windowed wire's cut tool messages\n * (see {@link substituteToolOutput}); omitted on surfaces that never\n * declare a window.\n */\n toolOutputs?: DeepAgentToolOutputStore;\n /**\n * User turns this client took back ahead of the wire (see\n * {@link DeepAgentWithdrawnTurns}): omitted from the transcript, together\n * with the pending-assistant placeholder a withdrawn tail would earn.\n */\n withdrawnTurns?: DeepAgentWithdrawnTurns;\n}\n\n/**\n * Map server state → assistant-ui thread messages.\n *\n * `state.messages` is the authoritative LangChain history streamed by the\n * backend. The replicated run status (`runs[0].status`) drives the\n * pending-assistant placeholder and the running status. Lane entries render at\n * the tail, after the placeholder, while the run is live (the run is still\n * working the history above them); once the thread rests stopped/errored they\n * are omitted (surfaces with a queue panel list them there instead).\n */\nexport function convertDeepAgentState(\n state: unknown,\n {\n laneProjection = 'both',\n selfAuthor = null,\n toolOutputs,\n withdrawnTurns,\n }: ConvertDeepAgentStateOptions = {}\n) {\n // A wire without a snapshot yet (pre-attach, replay boundary) converts as the\n // typed pre-stream default: an empty resting thread, never `undefined`.\n const values = readDeepAgentStreamValues(state);\n const status = values.runs?.[0]?.status;\n const isRunning = status === 'running';\n const isWithdrawn = (message: LangChainMessage): boolean =>\n withdrawnTurns !== undefined && message.id !== undefined && withdrawnTurns.isWithdrawn(message.id);\n const laneTail = (\n status === 'stopped' || status === 'error'\n ? []\n : laneTailMessages(values, laneProjection, selfAuthor)\n ).filter((message) => !isWithdrawn(message));\n const sanitized: LangChainMessage[] = isLangGraphState(values)\n ? sanitizeLangChainMessages(values.messages, { toolOutputs })\n : [];\n // A withdrawn tail is a turn nobody is answering: no placeholder for it.\n const tail: LangChainMessage | undefined = sanitized[sanitized.length - 1];\n const tailWithdrawn: boolean = tail !== undefined && isWithdrawn(tail);\n const stateMessages = sanitized.filter((message) => !isWithdrawn(message));\n return {\n messages: messageConverter.toThreadMessages(\n [...withPendingAssistantMessage(stateMessages, isRunning && !tailWithdrawn), ...laneTail],\n isRunning\n ),\n isRunning,\n };\n}\n"],"names":["createMessageConverter","convertLangChainMessages","laneEntryAuthor","ATHENA_MESSAGE_AUTHOR_KEY","buildOptimisticHumanContent","runsMaterialize","getExternalStoreMessages","readTruncatedToolContent","ATHENA_TRUNCATED_TOOL_OUTPUT_ARTIFACT_KEY","DEEP_AGENT_TRUNCATED_CONTENT_KEY","readDeepAgentStreamValues"],"mappings":"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"}
1
+ {"version":3,"file":"messages.cjs","sources":["../../../../../agent-runtime/src/messages.ts"],"sourcesContent":["import { getExternalStoreMessages } from '@assistant-ui/core';\nimport { createMessageConverter } from '@assistant-ui/core/react';\nimport { convertLangChainMessages, type LangChainMessage } from '@assistant-ui/react-langgraph';\nimport { runsMaterialize, type StatewireThread } from '@assistant-ui/react-statewire';\nimport {\n ATHENA_MESSAGE_AUTHOR_KEY,\n type DeepAgentMessageAuthor,\n laneEntryAuthor,\n} from './message-author';\nimport { DEEP_AGENT_TRUNCATED_CONTENT_KEY } from './generated/runconfig-contract';\nimport {\n ATHENA_TRUNCATED_TOOL_OUTPUT_ARTIFACT_KEY,\n readTruncatedToolContent,\n} from './message-window';\nimport { buildOptimisticHumanContent, type OptimisticContentPart } from './optimistic-content';\nimport { readDeepAgentStreamValues } from './statewire-state';\nimport type { DeepAgentToolOutputStore } from './tool-outputs';\nimport { groupSpokenUserTurns } from './voice-turn-grouping';\nimport type { DeepAgentWithdrawnTurns } from './withdrawn-turns';\nimport type {\n DeepAgentLaneProjection,\n DeepAgentStreamValues,\n LangGraphState,\n} from './types';\n\n/**\n * Native message converter for the deep-agent runtime.\n *\n * This is the entire \"conversion\" layer: assistant-ui's shipped\n * `convertLangChainMessages` (from `@assistant-ui/react-langgraph`) wired\n * through `createMessageConverter`. No bespoke per-message mapping — the agora\n * `athena_deep_agent` graph streams standard LangChain messages via\n * `append_langgraph_event`, and the shipped converter renders them.\n */\nexport const messageConverter = createMessageConverter(convertLangChainMessages);\n\nconst wirePartsToOptimisticParts = (\n parts: StatewireThread.UserMessage['parts']\n): OptimisticContentPart[] =>\n parts.flatMap((part): OptimisticContentPart[] => {\n if (part.type === 'text') return [part];\n // Image attachments travel as `file` parts on the run/* commands; map\n // them back to `image` parts so the optimistic bubble renders them.\n if (part.mediaType.startsWith('image/')) return [{ type: 'image', image: part.url }];\n return [];\n });\n\n/**\n * `metadata.custom` flag marking a bubble that is a projection of a run-lane\n * entry — a message the server holds and has NOT dispatched yet.\n *\n * The lane is server-owned and replicated to every client on the thread, so\n * an item queued on one surface projects onto all of them. Surfaces without a\n * queue panel must therefore be able to tell a queued projection apart from a\n * submitted turn, or a message another device queued renders as if this device\n * had already sent it.\n *\n * `convertLangChainMessages` exposes `additional_kwargs.metadata` (and nothing\n * else) as the thread message's `metadata.custom`.\n */\nexport const ATHENA_QUEUED_LANE_KEY = 'athena_queued_lane';\n\n/**\n * Whether a thread message's `metadata.custom` marks it as an undispatched\n * lane projection. Returns a primitive, so it is safe to use directly as a\n * `useAuiState` selector.\n */\nexport function isQueuedLaneMessage(custom: Record<string, unknown> | undefined): boolean {\n return custom?.[ATHENA_QUEUED_LANE_KEY] === true;\n}\n\n/**\n * Project one run-lane entry into the optimistic human message shape.\n *\n * `queued` marks entries still waiting in a lane; a dispatched entry becomes an\n * ordinary submitted turn and carries no marker. Either way the bubble carries\n * its author (see {@link laneEntryAuthor}) under the same `metadata.custom`\n * key the backend stamps on the persisted turn, so the optimistic and\n * authoritative copies of one send render identically — and a second poster's\n * avatar shows on their own send without waiting for the acknowledgment.\n */\nconst laneEntryMessage = ({\n entry,\n queued,\n selfAuthor,\n}: {\n entry: StatewireThread.RunsQueueEntry;\n queued: boolean;\n selfAuthor: DeepAgentMessageAuthor | null;\n}): LangChainMessage => {\n const author = laneEntryAuthor(entry, selfAuthor);\n const metadata: Record<string, unknown> = {\n ...(queued ? { [ATHENA_QUEUED_LANE_KEY]: true } : {}),\n ...(author ? { [ATHENA_MESSAGE_AUTHOR_KEY]: author } : {}),\n };\n return {\n id: entry.id,\n type: 'human',\n content: buildOptimisticHumanContent(wirePartsToOptimisticParts(entry.parts)),\n ...(Object.keys(metadata).length > 0 ? { additional_kwargs: { metadata } } : {}),\n };\n};\n\nexport interface DeepAgentMaterializeOptions {\n /** The viewer, stamped onto this client's own not-yet-acknowledged sends. */\n selfAuthor?: DeepAgentMessageAuthor | null;\n}\n\n/**\n * Materialize fold for the statewire path: makes the server's dispatch record\n * concrete in the LangGraph state itself, so direct state reads see dispatched\n * messages and the lib's post-converter dispatch merge stays off.\n */\nexport const createDeepAgentMaterialize =\n ({ selfAuthor = null }: DeepAgentMaterializeOptions = {}) =>\n (draft: DeepAgentStreamValues): void => {\n runsMaterialize(draft, {\n append: (message) => {\n (draft.messages ??= []).push(\n laneEntryMessage({ entry: message, queued: false, selfAuthor })\n );\n },\n rollback: (to) => {\n if (to === null) {\n draft.messages = [];\n return;\n }\n const messages = draft.messages ?? [];\n const keepIndex = messages.findIndex((message) => message.id === to);\n // Unresolved rollback anchor: fail OPEN with the full history (mirrors the edit fold).\n if (keepIndex !== -1) messages.splice(keepIndex + 1);\n },\n });\n };\n\n/** The materialize fold without a viewer identity (surfaces that pass none). */\nexport const deepAgentMaterialize = createDeepAgentMaterialize();\n\n/**\n * Project run lanes into human messages for the converter tail, so a queued or\n * steered item stays visible below the pending-assistant placeholder until its\n * dispatch. Surfaces with a queue panel project only the steer lane\n * (`'steer-only'`); surfaces without one project both lanes (`'both'`).\n *\n * Every projected entry carries {@link ATHENA_QUEUED_LANE_KEY} so a surface\n * without a queue panel can render it as queued rather than as a submitted\n * turn.\n */\nexport const laneTailMessages = (\n state: unknown,\n laneProjection: DeepAgentLaneProjection = 'both',\n selfAuthor: DeepAgentMessageAuthor | null = null\n): LangChainMessage[] => {\n const run = (state as DeepAgentStreamValues | null)?.runs?.[0];\n if (!run) return [];\n const entries =\n laneProjection === 'steer-only' ? run.steerQueue : [...run.queue, ...run.steerQueue];\n return entries.map((entry) => laneEntryMessage({ entry, queued: true, selfAuthor }));\n};\n\n/** Stable id for the derived running-assistant placeholder. */\nexport const PENDING_ASSISTANT_MESSAGE_ID = '__athena_deep_agent_pending_assistant__';\n\n/**\n * Append an empty assistant turn while a run is in flight and the visible\n * history does not already end in one, so the thread shows its \"Thinking…\"\n * status line from the moment a message is submitted.\n *\n * Mirrors `hasUpcomingMessage` in assistant-ui's legacy external-store runtime,\n * which synthesized this placeholder itself. `StatewireThread` renders the\n * converter's message list verbatim, so the placeholder has to come from here —\n * derived from the replicated run status on every render (never written into\n * transport state) so it cannot outlive the run, and carrying a stable id so\n * the streaming bubble's identity does not churn when the real assistant\n * message arrives.\n *\n * `tool` messages convert into assistant turns too, so a mid-run tool result\n * must not produce a second placeholder row.\n */\nexport function withPendingAssistantMessage(\n messages: LangChainMessage[],\n isRunning: boolean\n): LangChainMessage[] {\n if (!isRunning) return messages;\n const lastType = messages.at(-1)?.type;\n if (lastType === 'ai' || lastType === 'tool') return messages;\n // Empty-array content plus an explicit running status — the thinking shimmer\n // keys on `status running && zero content parts`, and the explicit status\n // keeps it pulsing when lane entries render below (the last-message auto\n // status would demote a mid-list placeholder to an empty complete bubble).\n // Never persisted, so it cannot outlive the run.\n return [\n ...messages,\n {\n type: 'ai',\n content: [],\n id: PENDING_ASSISTANT_MESSAGE_ID,\n status: { type: 'running' },\n } satisfies LangChainMessage,\n ];\n}\n\n/** Stable id for the synthetic assistant turn that carries a failure when the run produced none. */\nconst RUN_ERROR_SYNTHETIC_AI_ID = '__athena_deep_agent_run_error__';\n\n/**\n * Surface a run failure the base-correct way: stamp `status: { type:'incomplete',\n * reason:'error', error }` onto the assistant turn so `<Thread/>` renders it via\n * `ErrorPrimitive`. This is the core runtime's model (see `useLanggraphRuntime`\n * `onError`) — NOT a fabricated `⚠️ The run failed` assistant text message, and\n * NOT a magic-string contract shared with the agora backend.\n *\n * Stamps a trailing assistant message; if the visible history does not already\n * end with an assistant turn, appends a synthetic empty assistant turn with a\n * STABLE id so it never churns across renders. A `null` error is a no-op. The\n * shipped `convertLangChainMessages` converter passes `message.status` through.\n */\nexport function applyRunErrorStatus(\n messages: LangChainMessage[],\n errorMessage: string | null\n): LangChainMessage[] {\n if (errorMessage === null) return messages;\n const status = { type: 'incomplete', reason: 'error', error: errorMessage } as const;\n const lastMessage = messages.at(-1);\n if (lastMessage?.type !== 'ai') {\n return [\n ...messages,\n { type: 'ai', content: '', id: RUN_ERROR_SYNTHETIC_AI_ID, status } satisfies LangChainMessage,\n ];\n }\n return [...messages.slice(0, -1), { ...lastMessage, status }];\n}\n\n/**\n * `additional_kwargs` flag the backend stamps on run-lifecycle marker messages\n * (agora `athena_deep_agent/run_markers.py`) — e.g. the \"⏹️ Run stopped by\n * user.\" message persisted when a run is cancelled.\n */\nexport const ATHENA_RUN_MARKER_KEY = 'athena_run_marker';\n\n/** The {@link ATHENA_RUN_MARKER_KEY} value for a user-cancelled run. */\nexport const ATHENA_RUN_MARKER_CANCELLED = 'cancelled';\n\n/**\n * `additional_kwargs` key carrying the stop source behind a cancellation\n * marker (agora `stop_signals.py`: `user_stop`, `send_now`, `transport_cancel`,\n * `unattributed`).\n */\nexport const ATHENA_RUN_MARKER_SOURCE_KEY = 'athena_run_marker_source';\n\n/** The stop source for a \"Send now\" promotion pre-empting the live run. */\nexport const ATHENA_STOP_SOURCE_SEND_NOW = 'send_now';\n\n/**\n * The stop source for a spoken follow-up that redirected a voice-delegated\n * run through the steer lane: the user spoke, they never pressed Stop, so it\n * renders as a redirect rather than as \"Conversation stopped\".\n */\nexport const ATHENA_STOP_SOURCE_VOICE_STEER = 'voice_steer';\n\n/** The stop source for a pod-drain pause (server update), not a user stop. */\nexport const ATHENA_STOP_SOURCE_POD_DRAIN = 'pod_drain';\n\n/**\n * The {@link ATHENA_RUN_MARKER_SOURCE_KEY} value for a run whose hosting\n * instance closed under it (a server update that reached its eviction\n * deadline mid-step, or a placement lost to a lease lapse). Same family as a\n * pod drain: nobody stopped the run, the backend resumes it automatically.\n */\nexport const ATHENA_STOP_SOURCE_INSTANCE_CLOSED = 'instance_closed';\n\n/**\n * Whether a cancellation marker's source names the infrastructure — a pod\n * drain or a closed instance — rather than a person or a client action. These\n * endings read as a pause while the server resumes the run.\n */\nexport function isServerUpdateStopSource(source: string | undefined): boolean {\n return source === ATHENA_STOP_SOURCE_POD_DRAIN || source === ATHENA_STOP_SOURCE_INSTANCE_CLOSED;\n}\n\n/**\n * The {@link ATHENA_RUN_MARKER_KEY} value for a failed run (graph error,\n * including the `GraphRecursionError` \"tool call limit was reached\" case).\n * The marker message's text content carries the user-facing failure copy.\n */\nexport const ATHENA_RUN_MARKER_FAILED = 'failed';\n\n/**\n * `additional_kwargs` key carrying the canonical reason a run ended (agora\n * `athena_deep_agent/termination.py`): `user_stop`, `recursion_limit`,\n * `duplicate_call_guard`, `step_budget`, `ended_after_tools`, `model_refusal`,\n * `completed`, …\n * Stamped on run-lifecycle markers and on handoff messages.\n */\nexport const ATHENA_TERMINATION_REASON_KEY = 'athena_termination_reason';\n\n/**\n * `additional_kwargs` flag on the closing message the runtime's **handoff\n * turn** wrote (one tools-disabled model call made when a guard or the step\n * budget ends the run). The message text is the agent's closing summary; the\n * flag lets clients frame it as a runtime ending rather than a normal reply.\n */\nexport const ATHENA_HANDOFF_KEY = 'athena_handoff';\n\n/**\n * Read a run-lifecycle marker out of a thread message's **bound source\n * messages**, bypassing the converter's metadata join entirely.\n *\n * Why this exists: assistant-ui chunks consecutive assistant/tool messages\n * into ONE `ThreadMessage`, and the upstream join copies `metadata.custom`\n * only from the FIRST assistant message of the chunk\n * (`external-message-converter.ts`: the merge sits inside\n * `if (assistantMessage.content.length === 0)`). A failure marker appended\n * after the assistant's own turn is therefore never the first message in its\n * chunk, so `message.metadata.custom.athena_run_marker` is silently dropped\n * and the whole-message gate can't fire.\n *\n * `getExternalStoreMessages` returns the raw LangChain messages bound to the\n * joined thread message, so the marker is found wherever it sits in the chunk.\n * Returns the marker value plus its text content (the user-facing copy).\n */\nexport interface RunMarkerInfo {\n marker: string;\n /** The stop source stamped by the backend, when the marker carries one. */\n source: string | undefined;\n /** The marker message's text — the user-facing copy. */\n text: string | undefined;\n /**\n * True when the marker is the ONLY source message behind this thread message,\n * i.e. it owns the whole row (a cancelled run drops its partial turn, so its\n * marker stands alone).\n *\n * False when the chunk also holds the assistant's own work. Replacing the row\n * in that case would hide completed prose and tool cards — the marker must be\n * styled in place instead.\n */\n isSoleSourceMessage: boolean;\n}\n\nexport function readRunMarkerFromSourceMessages(\n message: Parameters<typeof getExternalStoreMessages>[0] & {\n metadata?: { custom?: Record<string, unknown> | undefined } | undefined;\n content?: unknown;\n }\n): RunMarkerInfo | undefined {\n const sources = getExternalStoreMessages<LangChainMessage>(message);\n for (const source of sources) {\n if (source?.type !== 'ai') continue;\n const kwargs = source.additional_kwargs as Record<string, unknown> | undefined;\n const marker = kwargs?.[ATHENA_RUN_MARKER_KEY];\n if (typeof marker !== 'string') continue;\n const markerSource = kwargs?.[ATHENA_RUN_MARKER_SOURCE_KEY];\n return {\n marker,\n source: typeof markerSource === 'string' ? markerSource : undefined,\n text: typeof source.content === 'string' ? source.content : undefined,\n isSoleSourceMessage: sources.length === 1,\n };\n }\n // Fallback for surfaces whose messages carry no bound source messages (or a\n // converter that already merged the flag): read the joined metadata. Without\n // sources to count, a single text part is the best available \"marker owns the\n // row\" signal.\n const joined = message.metadata?.custom?.[ATHENA_RUN_MARKER_KEY];\n if (typeof joined !== 'string') return undefined;\n const joinedSource = message.metadata?.custom?.[ATHENA_RUN_MARKER_SOURCE_KEY];\n const parts = message.content;\n return {\n marker: joined,\n source: typeof joinedSource === 'string' ? joinedSource : undefined,\n text: undefined,\n isSoleSourceMessage: Array.isArray(parts) ? parts.length <= 1 : true,\n };\n}\n\n/**\n * The marker value when the marker is the SOLE source message behind a thread\n * message (it owns the whole row), else `undefined`.\n *\n * Returns a primitive so it is safe to use directly as a `useAuiState`\n * selector: `readRunMarkerFromSourceMessages` builds a fresh object on every\n * call, and selector snapshots are compared with `Object.is` — returning that\n * object from a selector makes `getSnapshot` unstable and loops the render.\n */\nexport function readSoleRunMarker(\n message: Parameters<typeof readRunMarkerFromSourceMessages>[0]\n): string | undefined {\n const found = readRunMarkerFromSourceMessages(message);\n return found?.isSoleSourceMessage ? found.marker : undefined;\n}\n\n/**\n * Whether a run marker renders nothing: a `send_now` promotion cancels the\n * live run as plumbing, not as a user-visible stop, so its cancellation\n * marker stays in the data for provenance but paints no divider.\n */\nexport function isSilentRunMarker(found: RunMarkerInfo): boolean {\n return (\n found.marker === ATHENA_RUN_MARKER_CANCELLED && found.source === ATHENA_STOP_SOURCE_SEND_NOW\n );\n}\n\n/**\n * Whether a cancellation marker records a server-update pause — a cooperative\n * pod drain, or the hosting instance closing under the run — rather than a\n * stop the user asked for. Rendered with distinct \"paused by a server update\"\n * copy instead of the \"Conversation stopped\" pill; the backend resumes the run.\n */\nexport function isDrainRunMarker(found: RunMarkerInfo): boolean {\n return found.marker === ATHENA_RUN_MARKER_CANCELLED && isServerUpdateStopSource(found.source);\n}\n\n/**\n * Surface the backend's top-level `additional_kwargs.athena_run_marker` flag\n * where the shipped converter can see it.\n *\n * `convertLangChainMessages` exposes ONLY `additional_kwargs.metadata` as the\n * thread message's `metadata.custom`; every other `additional_kwargs` key is\n * dropped. Marker messages persisted before the backend mirrored the flag into\n * `metadata` carry it top-level only, so this lift keeps those threads styling\n * correctly. Non-marker messages pass through with their identity unchanged,\n * and already-mirrored markers are left as-is (idempotent).\n */\nexport function liftRunMarkerIntoMetadata(message: LangChainMessage): LangChainMessage {\n if (message.type !== 'ai') return message;\n const kwargs = message.additional_kwargs as Record<string, unknown> | undefined;\n const marker = kwargs?.[ATHENA_RUN_MARKER_KEY];\n if (typeof marker !== 'string') return message;\n const existingMetadata = kwargs?.metadata;\n const metadata =\n typeof existingMetadata === 'object' && existingMetadata !== null\n ? (existingMetadata as Record<string, unknown>)\n : {};\n if (metadata[ATHENA_RUN_MARKER_KEY] === marker) return message;\n return {\n ...message,\n additional_kwargs: {\n ...kwargs,\n metadata: { ...metadata, [ATHENA_RUN_MARKER_KEY]: marker },\n } as (typeof message)['additional_kwargs'],\n };\n}\n\n/**\n * Mirror the termination reason and handoff flag into the converter's metadata\n * channel. The backend dual-writes both (top-level + `metadata`), so this lift\n * is the back-compat path for messages persisted before the mirror existed and\n * for any emitter that only stamps the top-level key. Idempotent; identity for\n * messages without the keys.\n */\nexport function liftTerminationIntoMetadata(message: LangChainMessage): LangChainMessage {\n if (message.type !== 'ai') return message;\n const kwargs = message.additional_kwargs as Record<string, unknown> | undefined;\n const reason = kwargs?.[ATHENA_TERMINATION_REASON_KEY];\n const handoff = kwargs?.[ATHENA_HANDOFF_KEY];\n const hasReason = typeof reason === 'string';\n const hasHandoff = handoff === true;\n if (!hasReason && !hasHandoff) return message;\n const existingMetadata = kwargs?.metadata;\n const metadata =\n typeof existingMetadata === 'object' && existingMetadata !== null\n ? (existingMetadata as Record<string, unknown>)\n : {};\n const reasonMirrored = !hasReason || metadata[ATHENA_TERMINATION_REASON_KEY] === reason;\n const handoffMirrored = !hasHandoff || metadata[ATHENA_HANDOFF_KEY] === true;\n if (reasonMirrored && handoffMirrored) return message;\n return {\n ...message,\n additional_kwargs: {\n ...kwargs,\n metadata: {\n ...metadata,\n ...(hasReason ? { [ATHENA_TERMINATION_REASON_KEY]: reason } : {}),\n ...(hasHandoff ? { [ATHENA_HANDOFF_KEY]: true } : {}),\n },\n } as (typeof message)['additional_kwargs'],\n };\n}\n\n/**\n * `additional_kwargs` key (and `metadata` mirror) carrying a failure marker's\n * structured error records (agora `athena_deep_agent/run_markers.py`\n * `failure_error_records`): `[{ error, provider?, message? }]`, where `error`\n * is the client code a surface branches on — `context_length_exceeded`,\n * `transient_api_error` (with `provider: 'azure_foundry' |\n * 'external_model_provider'`), `content_filter`, `request_too_large`, … — the\n * same shape the legacy agent node emitted.\n */\nexport const ATHENA_ERRORS_KEY = 'errors';\n\n/**\n * Mirror a failure marker's structured `errors` records into the converter's\n * metadata channel. The backend dual-writes them (top-level + `metadata`), so\n * this lift is the back-compat path for messages that carry only the top-level\n * key (an emitter that never mirrored, or a record persisted before the mirror\n * existed). Records already present in the metadata channel win; no-ops are\n * identity.\n */\nexport function liftErrorsIntoMetadata(message: LangChainMessage): LangChainMessage {\n if (message.type !== 'ai') return message;\n const kwargs = message.additional_kwargs as Record<string, unknown> | undefined;\n const errors = kwargs?.[ATHENA_ERRORS_KEY];\n if (!Array.isArray(errors) || errors.length === 0) return message;\n const existingMetadata = kwargs?.metadata;\n const metadata =\n typeof existingMetadata === 'object' && existingMetadata !== null\n ? (existingMetadata as Record<string, unknown>)\n : {};\n if (Array.isArray(metadata[ATHENA_ERRORS_KEY])) return message;\n return {\n ...message,\n additional_kwargs: {\n ...kwargs,\n metadata: { ...metadata, [ATHENA_ERRORS_KEY]: errors },\n } as (typeof message)['additional_kwargs'],\n };\n}\n\n/**\n * Read a failure's structured `errors` records out of a thread message's\n * **bound source messages**, bypassing the converter's metadata join.\n *\n * Same reason as {@link readRunMarkerFromSourceMessages}: assistant-ui chunks\n * consecutive assistant/tool messages into ONE `ThreadMessage` and copies\n * `metadata.custom` only from the FIRST assistant message of the chunk. The\n * failure marker that carries the records is appended after the assistant's\n * own turn, so `message.metadata.custom.errors` is silently dropped whenever\n * the run produced anything before failing. The bound sources are walked from\n * the tail (the terminal failure wins); the joined metadata is the fallback\n * for surfaces whose messages carry no bound sources. Returns the raw records\n * (callers validate the shape) or `undefined`.\n */\nexport function readMessageErrorsFromSourceMessages(\n message: Parameters<typeof getExternalStoreMessages>[0] & {\n metadata?: { custom?: Record<string, unknown> | undefined } | undefined;\n }\n): unknown[] | undefined {\n const sources = getExternalStoreMessages<LangChainMessage>(message);\n for (let index = sources.length - 1; index >= 0; index -= 1) {\n const source = sources[index];\n if (source?.type !== 'ai') continue;\n const kwargs = source.additional_kwargs as Record<string, unknown> | undefined;\n const metadata = kwargs?.metadata;\n const mirrored =\n typeof metadata === 'object' && metadata !== null\n ? (metadata as Record<string, unknown>)[ATHENA_ERRORS_KEY]\n : undefined;\n const errors = Array.isArray(mirrored) ? mirrored : kwargs?.[ATHENA_ERRORS_KEY];\n if (Array.isArray(errors) && errors.length > 0) return errors;\n }\n const joined = message.metadata?.custom?.[ATHENA_ERRORS_KEY];\n return Array.isArray(joined) && joined.length > 0 ? joined : undefined;\n}\n\n/**\n * Mirror the producing model into the converter's metadata channel. Wire AI\n * messages are full LangChain dumps whose `response_metadata` reports the model\n * that generated them (`model_name` / `model`), but the shipped converter maps\n * only `additional_kwargs.metadata` into `metadata.custom` — so without this\n * lift a surface can only show the CURRENT run config's model, mislabeling\n * earlier messages after a mid-thread model switch. A model already present in\n * the metadata channel (backend-stamped provenance) wins; no-ops are identity.\n */\nexport function liftModelIntoMetadata(message: LangChainMessage): LangChainMessage {\n if (message.type !== 'ai') return message;\n const responseMetadata = (\n message as { response_metadata?: { model_name?: unknown; model?: unknown } }\n ).response_metadata;\n let model: string | undefined;\n if (typeof responseMetadata?.model_name === 'string' && responseMetadata.model_name) {\n model = responseMetadata.model_name;\n } else if (typeof responseMetadata?.model === 'string' && responseMetadata.model) {\n model = responseMetadata.model;\n }\n if (!model) return message;\n const kwargs = message.additional_kwargs as Record<string, unknown> | undefined;\n const existingMetadata = kwargs?.metadata;\n const metadata =\n typeof existingMetadata === 'object' && existingMetadata !== null\n ? (existingMetadata as Record<string, unknown>)\n : {};\n if (typeof metadata.model === 'string' && metadata.model) return message;\n return {\n ...message,\n additional_kwargs: {\n ...kwargs,\n metadata: { ...metadata, model },\n } as (typeof message)['additional_kwargs'],\n };\n}\n\n/**\n * Mirror a per-message conversation-stats snapshot into the converter's\n * metadata channel. Backends stamp the snapshot at the legacy location —\n * top-level `additional_kwargs.conversation_stats` (the deep-agent stats\n * middleware and the legacy agent node both write it there) — but the shipped\n * converter maps only `additional_kwargs.metadata` into `metadata.custom`, so\n * without this lift the model badge can never show a message's OWN context and\n * falls back to thread-level live numbers. A snapshot already present in the\n * metadata channel wins; no-ops are identity.\n */\nexport function liftConversationStatsIntoMetadata(message: LangChainMessage): LangChainMessage {\n if (message.type !== 'ai') return message;\n const kwargs = message.additional_kwargs as Record<string, unknown> | undefined;\n const stats = kwargs?.conversation_stats;\n if (typeof stats !== 'object' || stats === null) return message;\n const existingMetadata = kwargs?.metadata;\n const metadata =\n typeof existingMetadata === 'object' && existingMetadata !== null\n ? (existingMetadata as Record<string, unknown>)\n : {};\n if (typeof metadata.conversation_stats === 'object' && metadata.conversation_stats !== null) {\n return message;\n }\n return {\n ...message,\n additional_kwargs: {\n ...kwargs,\n metadata: { ...metadata, conversation_stats: stats },\n } as (typeof message)['additional_kwargs'],\n };\n}\n\n/**\n * Mirror the compaction boundary flag into the converter's metadata channel.\n * A manual `/compact` persists its summary as a human message tagged with\n * top-level `additional_kwargs.compaction_start` (plus `_created_at` for the\n * divider timestamp) — the same location the legacy runtime used — but the\n * shipped converter maps only `additional_kwargs.metadata` into\n * `metadata.custom`, so without this lift the transcript's\n * \"Previous conversation summarized\" divider can never render. A flag already\n * present in the metadata channel wins; no-ops are identity.\n */\nexport function liftCompactionBoundaryIntoMetadata(message: LangChainMessage): LangChainMessage {\n if (message.type !== 'human') return message;\n const kwargs = message.additional_kwargs as Record<string, unknown> | undefined;\n if (kwargs?.compaction_start !== true) return message;\n const existingMetadata = kwargs?.metadata;\n const metadata =\n typeof existingMetadata === 'object' && existingMetadata !== null\n ? (existingMetadata as Record<string, unknown>)\n : {};\n if (metadata.compaction_start === true) return message;\n const createdAt = kwargs?._created_at;\n return {\n ...message,\n additional_kwargs: {\n ...kwargs,\n metadata: {\n ...metadata,\n compaction_start: true,\n ...(typeof createdAt === 'string' ? { _created_at: createdAt } : {}),\n },\n } as (typeof message)['additional_kwargs'],\n };\n}\n\n/**\n * Mirror the handoff summary flag into the converter's metadata channel.\n * Handoff summaries are persisted as human messages tagged with top-level\n * `additional_kwargs.handoff_summary` (plus `_created_at`) — the same location\n * the backend uses for message metadata — but the shipped converter maps only\n * `additional_kwargs.metadata` into `metadata.custom`, so without this lift\n * the summary would render as a normal user bubble. A flag already present in\n * the metadata channel wins; no-ops are identity.\n */\nexport function liftHandoffSummaryIntoMetadata(message: LangChainMessage): LangChainMessage {\n if (message.type !== 'human') return message;\n const kwargs = message.additional_kwargs as Record<string, unknown> | undefined;\n if (kwargs?.handoff_summary !== true) return message;\n const existingMetadata = kwargs?.metadata;\n const metadata =\n typeof existingMetadata === 'object' && existingMetadata !== null\n ? (existingMetadata as Record<string, unknown>)\n : {};\n if (metadata.handoff_summary === true) return message;\n const createdAt = kwargs?._created_at;\n return {\n ...message,\n additional_kwargs: {\n ...kwargs,\n metadata: {\n ...metadata,\n handoff_summary: true,\n ...(typeof createdAt === 'string' ? { _created_at: createdAt } : {}),\n },\n } as (typeof message)['additional_kwargs'],\n };\n}\n\n/**\n * Mirror an AOP run's provenance into the converter's metadata channel. The\n * backend stamps the kicking-off user message with top-level\n * `additional_kwargs.aop_source` (agora `events/helpers.py`\n * `_build_aop_source_metadata`), which the legacy converter surfaced as\n * `metadata.custom.aop_source` — where the \"AOP · <title>\" badge reads it. The\n * shipped `convertLangChainMessages` maps only `additional_kwargs.metadata`\n * into `metadata.custom`, so without this lift an AOP run's conversation never\n * shows which AOP asset kicked it off. Provenance already present in the\n * metadata channel wins; no-ops are identity.\n */\nexport function liftAopSourceIntoMetadata(message: LangChainMessage): LangChainMessage {\n if (message.type !== 'human') return message;\n const kwargs = message.additional_kwargs as Record<string, unknown> | undefined;\n const aopSource = kwargs?.aop_source;\n if (typeof aopSource !== 'object' || aopSource === null || Array.isArray(aopSource)) {\n return message;\n }\n const existingMetadata = kwargs?.metadata;\n const metadata =\n typeof existingMetadata === 'object' && existingMetadata !== null\n ? (existingMetadata as Record<string, unknown>)\n : {};\n if (typeof metadata.aop_source === 'object' && metadata.aop_source !== null) return message;\n return {\n ...message,\n additional_kwargs: {\n ...kwargs,\n metadata: { ...metadata, aop_source: aopSource },\n } as (typeof message)['additional_kwargs'],\n };\n}\n\nexport function liftBranchIntoMetadata(message: LangChainMessage): LangChainMessage {\n if (message.type !== 'human' && message.type !== 'ai') return message;\n const branch = (message as { branch?: unknown }).branch;\n if (typeof branch !== 'object' || branch === null || Array.isArray(branch)) return message;\n const kwargs = message.additional_kwargs as Record<string, unknown> | undefined;\n const existingMetadata = kwargs?.metadata;\n const metadata =\n typeof existingMetadata === 'object' && existingMetadata !== null\n ? (existingMetadata as Record<string, unknown>)\n : {};\n if (typeof metadata.branch === 'object' && metadata.branch !== null) return message;\n return {\n ...message,\n additional_kwargs: {\n ...kwargs,\n metadata: { ...metadata, branch },\n } as (typeof message)['additional_kwargs'],\n };\n}\n\n/**\n * `additional_kwargs` keys on a ToolMessage that are infrastructure rather than\n * user-set metadata. The filter below additionally skips every `_`-prefixed key.\n */\nconst TOOL_MESSAGE_INTERNAL_KWARGS: ReadonlySet<string> = new Set([\n 'reasoning',\n 'tool_outputs',\n 'hide_from_frontend',\n]);\n\n/**\n * Mirror a ToolMessage's user-set `additional_kwargs` into\n * `artifact._tool_message_metadata` — the location `useToolCallMetadata` reads\n * persisted per-tool-call metadata from (`email_sent_at`, draft edits,\n * `calendar_event_created_at`, …).\n *\n * The `setMessageMetadata` mutation persists those keys onto the ToolMessage's\n * `additional_kwargs` in the checkpoint, and the legacy converter lifted them\n * into the artifact. The shipped `convertLangChainMessages` forwards only\n * `artifact` and drops `additional_kwargs` for tool messages, so without this\n * lift the metadata survives in the checkpoint but never reaches a tool card\n * after a reload. Kwargs values win over same-key artifact entries (the kwargs\n * are the durable write target); messages with no user-set kwargs pass through\n * by identity.\n */\nexport function liftToolMessageMetadataIntoArtifact(message: LangChainMessage): LangChainMessage {\n if (message.type !== 'tool') return message;\n const kwargs = (message as { additional_kwargs?: Record<string, unknown> }).additional_kwargs;\n if (!kwargs) return message;\n const lifted: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(kwargs)) {\n if (key.startsWith('_') || TOOL_MESSAGE_INTERNAL_KWARGS.has(key)) continue;\n lifted[key] = value;\n }\n if (Object.keys(lifted).length === 0) return message;\n const artifact: unknown = message.artifact;\n const artifactRecord =\n typeof artifact === 'object' && artifact !== null && !Array.isArray(artifact)\n ? (artifact as Record<string, unknown>)\n : {};\n const existing = artifactRecord._tool_message_metadata;\n const existingRecord =\n typeof existing === 'object' && existing !== null && !Array.isArray(existing)\n ? (existing as Record<string, unknown>)\n : {};\n return {\n ...message,\n artifact: {\n ...artifactRecord,\n _tool_message_metadata: { ...existingRecord, ...lifted },\n },\n };\n}\n\n/**\n * Lift a windowed wire's tool-output truncation marker into the tool-call\n * part's `artifact` (`_athena_content_truncated: { chars, messageId }`).\n *\n * The host cuts long plain-text tool outputs on a windowed wire and stamps\n * `athena_content_truncated` on the wire message; the shipped converter joins\n * a ToolMessage into its assistant turn's tool-call part and keeps only\n * `artifact` and the result, so without this lift no tool card could tell the\n * output is cut nor which message to ask `history/tool_output` for. Messages\n * without the marker pass through by identity.\n */\nexport function liftTruncatedToolOutputIntoArtifact(message: LangChainMessage): LangChainMessage {\n if (message.type !== 'tool') return message;\n const marker = readTruncatedToolContent(message);\n if (marker === null || typeof message.id !== 'string') return message;\n const artifact: unknown = message.artifact;\n const artifactRecord =\n typeof artifact === 'object' && artifact !== null && !Array.isArray(artifact)\n ? (artifact as Record<string, unknown>)\n : {};\n return {\n ...message,\n artifact: {\n ...artifactRecord,\n [ATHENA_TRUNCATED_TOOL_OUTPUT_ARTIFACT_KEY]: { chars: marker.chars, messageId: message.id },\n },\n };\n}\n\n/**\n * Per-source cache for {@link substituteToolOutput}: a substituted copy is\n * reused while the wire message and the fetched output are both unchanged, so\n * the lift cache downstream keeps its identity contract.\n */\nconst substitutedToolOutputCache = new WeakMap<\n LangChainMessage,\n { content: unknown; substituted: LangChainMessage }\n>();\n\n/**\n * A cut tool message with its full output put back, when the store holds one\n * fetched for this very output (`history/tool_output` answered for the same\n * `chars` the wire's marker reports); the marker leaves with the cut.\n * Messages that are whole, whose output has not been fetched, or whose fetched\n * output belongs to a length the host has since replaced, pass through by\n * identity — the loader sees the mismatch and fetches again.\n */\nexport function substituteToolOutput(\n message: LangChainMessage,\n toolOutputs: DeepAgentToolOutputStore\n): LangChainMessage {\n if (message.type !== 'tool' || typeof message.id !== 'string') return message;\n const marker = readTruncatedToolContent(message);\n if (marker === null) return message;\n const entry = toolOutputs.entry(message.id);\n if (entry === undefined || entry.chars !== marker.chars) return message;\n const { content } = entry;\n const cached = substitutedToolOutputCache.get(message);\n if (cached !== undefined && cached.content === content) return cached.substituted;\n const { [DEEP_AGENT_TRUNCATED_CONTENT_KEY]: _marker, ...rest } = message as Record<\n string,\n unknown\n >;\n const substituted = { ...rest, content } as unknown as LangChainMessage;\n substitutedToolOutputCache.set(message, { content, substituted });\n return substituted;\n}\n\nconst hasHiddenFrontendFlag = (value: unknown): boolean => {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n return false;\n }\n const record = value as Record<string, unknown>;\n if (record.hide_from_frontend === true) return true;\n return hasHiddenFrontendFlag(record.custom);\n};\n\n/**\n * Internal orchestration messages are useful in a trace export but must never\n * become conversation bubbles. Agora stores the flag in `additional_kwargs`;\n * live converter state can expose the same value under `metadata.custom`.\n */\nexport function isMessageHiddenFromFrontend(message: unknown): boolean {\n if (typeof message !== 'object' || message === null || Array.isArray(message)) {\n return false;\n }\n const record = message as Record<string, unknown>;\n return (\n hasHiddenFrontendFlag(record.additional_kwargs) || hasHiddenFrontendFlag(record.metadata)\n );\n}\n\n/**\n * The message `type`s `convertLangChainMessages` actually maps: its\n * `switch (message.type)` covers exactly these four and has no `default`, so it\n * returns `undefined` for anything else (a `RemoveMessage` `type: \"remove\"`\n * from summarization/compaction, a partial chunk, a future type).\n */\nconst CONVERTIBLE_MESSAGE_TYPES: ReadonlySet<string> = new Set(['system', 'human', 'ai', 'tool']);\n\n/**\n * Whether `convertLangChainMessages` can map this message at all.\n *\n * An unmappable entry converts to `undefined`, which assistant-ui's\n * `convertExternalMessages` wraps as `outputs: [undefined]` and then reads\n * `output.role` off — throwing `Cannot read property 'role' of undefined` and\n * taking the entire thread down instead of dropping one message.\n */\nexport function isConvertibleChatMessage(message: unknown): boolean {\n if (typeof message !== 'object' || message === null || Array.isArray(message)) {\n return false;\n }\n const type: unknown = (message as Record<string, unknown>).type;\n return typeof type === 'string' && CONVERTIBLE_MESSAGE_TYPES.has(type);\n}\n\n/**\n * Per-source cache for the lift pipeline in {@link sanitizeLangChainMessages}.\n *\n * The pipeline runs on EVERY state pass (once per streamed token), and the two\n * lifts that fire for essentially every message — {@link liftBranchIntoMetadata}\n * (the backend stamps `branch` on all indexed messages) and\n * {@link liftModelIntoMetadata} — allocate a fresh object each time they lift.\n * Without a cache, every message's identity churns on every token, defeating\n * all downstream per-source memoization (`getExternalStoreMessages`-keyed\n * reads, converter callback caches) and re-deriving the whole thread per token.\n *\n * Object identity is a sound cache key here: every statewire fold (op apply,\n * optimistic, materialize) runs through `statewire`'s copy-on-write\n * `recordMutations`, whose `finalize` returns the ORIGINAL object for\n * untouched nodes and a fresh copy for touched ones. A message is therefore\n * REPLACED — never mutated in place — whenever any of its fields change\n * (including the streaming tail's string-insert content growth), and an\n * unchanged message keeps its exact identity across passes. The corpus test\n * (`statewire-corpus.test.ts`) pins this identity contract against the real\n * client, so a statewire upgrade that switched to in-place mutation would fail\n * there rather than silently serving stale lifts from this cache.\n *\n * Module-level on purpose: keys are the wire message objects themselves, so\n * entries die with their state trees and threads cannot collide.\n */\nconst liftedMessageCache = new WeakMap<LangChainMessage, LangChainMessage>();\n\n/** The full lift chain for one message, memoized per source object. */\nfunction liftMessageIntoMetadataChannels(message: LangChainMessage): LangChainMessage {\n const cached = liftedMessageCache.get(message);\n if (cached !== undefined) return cached;\n const lifted = liftBranchIntoMetadata(\n liftTruncatedToolOutputIntoArtifact(\n liftToolMessageMetadataIntoArtifact(\n liftAopSourceIntoMetadata(\n liftHandoffSummaryIntoMetadata(\n liftCompactionBoundaryIntoMetadata(\n liftConversationStatsIntoMetadata(\n liftModelIntoMetadata(\n liftErrorsIntoMetadata(\n liftTerminationIntoMetadata(liftRunMarkerIntoMetadata(message))\n )\n )\n )\n )\n )\n )\n )\n )\n );\n liftedMessageCache.set(message, lifted);\n return lifted;\n}\n\n/**\n * The one message pipeline every deep-agent surface renders through:\n *\n * 1. drop messages the shipped converter cannot map (`RemoveMessage` etc. —\n * an unmappable entry would crash the whole thread) and internal\n * orchestration messages flagged hidden-from-frontend;\n * 2. lift run-lifecycle markers into the converter's `metadata` channel\n * (see {@link liftRunMarkerIntoMetadata}) so the cancellation row styles as\n * a status divider instead of a plain assistant bubble, and the termination\n * reason + handoff flag alongside them (see\n * {@link liftTerminationIntoMetadata}) so every surface can render a\n * runtime ending (see `runtime-messages.ts`);\n * 3. lift each AI message's producing model into the same channel\n * (see {@link liftModelIntoMetadata}) so model badges reflect the model that\n * actually generated the message, not the currently configured one;\n * 4. lift each AI message's per-message conversation-stats snapshot into the\n * same channel (see {@link liftConversationStatsIntoMetadata}) so the model\n * badge's hover card shows that message's own context usage;\n * 5. lift the compaction boundary flag on `/compact` summary messages into the\n * same channel (see {@link liftCompactionBoundaryIntoMetadata}) so the\n * transcript renders its \"Previous conversation summarized\" divider;\n * 6. lift the handoff summary flag on carried-over context messages into the\n * same channel (see {@link liftHandoffSummaryIntoMetadata}) so the\n * transcript keeps the summary behind its context panel;\n * 7. lift an AOP run's provenance on its kicking-off user message into the\n * same channel (see {@link liftAopSourceIntoMetadata}) so the message\n * renders its \"AOP · <title>\" origin badge;\n * 8. lift each ToolMessage's user-set `additional_kwargs` into\n * `artifact._tool_message_metadata` (see\n * {@link liftToolMessageMetadataIntoArtifact}) so persisted per-tool-call\n * metadata reaches tool cards after a reload;\n * 9. lift a windowed wire's tool-output truncation marker into the tool-call\n * part's `artifact` (see {@link liftTruncatedToolOutputIntoArtifact}) so a\n * tool card can fetch the whole output — after first putting back any\n * output already fetched (see {@link substituteToolOutput});\n * 10. lift the host's per-message branch stamp into the same channel\n * (see {@link liftBranchIntoMetadata}) so the branch picker renders each\n * message's edit fan-out.\n *\n * Identity-stable per source: an unchanged source message maps to the\n * IDENTICAL lifted object across passes (see {@link liftedMessageCache}).\n */\nexport function sanitizeLangChainMessages(\n messages: readonly LangChainMessage[],\n { toolOutputs }: { toolOutputs?: DeepAgentToolOutputStore } = {}\n): LangChainMessage[] {\n return messages\n .filter((message) => isConvertibleChatMessage(message) && !isMessageHiddenFromFrontend(message))\n .map((message) =>\n liftMessageIntoMetadataChannels(\n toolOutputs ? substituteToolOutput(message, toolOutputs) : message\n )\n );\n}\n\n/** Type guard for the runtime state — an object carrying a `messages` array. */\nexport function isLangGraphState(value: unknown): value is LangGraphState {\n return (\n typeof value === 'object' &&\n value !== null &&\n 'messages' in value &&\n Array.isArray((value as LangGraphState).messages)\n );\n}\n\nexport interface ConvertDeepAgentStateOptions {\n laneProjection?: DeepAgentLaneProjection;\n /**\n * The viewer, as the backend would stamp them: the author of this client's\n * own not-yet-acknowledged lane entries (see {@link laneEntryAuthor}).\n */\n selfAuthor?: DeepAgentMessageAuthor | null;\n /**\n * Full tool outputs fetched back for a windowed wire's cut tool messages\n * (see {@link substituteToolOutput}); omitted on surfaces that never\n * declare a window.\n */\n toolOutputs?: DeepAgentToolOutputStore;\n /**\n * User turns this client took back ahead of the wire (see\n * {@link DeepAgentWithdrawnTurns}): omitted from the transcript, together\n * with the pending-assistant placeholder a withdrawn tail would earn.\n */\n withdrawnTurns?: DeepAgentWithdrawnTurns;\n}\n\n/**\n * Map server state → assistant-ui thread messages.\n *\n * `state.messages` is the authoritative LangChain history streamed by the\n * backend. The replicated run status (`runs[0].status`) drives the\n * pending-assistant placeholder and the running status. Lane entries render at\n * the tail, after the placeholder, while the run is live (the run is still\n * working the history above them); once the thread rests stopped/errored they\n * are omitted (surfaces with a queue panel list them there instead).\n * Consecutive spoken user turns collapse into one user message (see\n * {@link groupSpokenUserTurns}) before conversion.\n */\nexport function convertDeepAgentState(\n state: unknown,\n {\n laneProjection = 'both',\n selfAuthor = null,\n toolOutputs,\n withdrawnTurns,\n }: ConvertDeepAgentStateOptions = {}\n) {\n // A wire without a snapshot yet (pre-attach, replay boundary) converts as the\n // typed pre-stream default: an empty resting thread, never `undefined`.\n const values = readDeepAgentStreamValues(state);\n const status = values.runs?.[0]?.status;\n const isRunning = status === 'running';\n const isWithdrawn = (message: LangChainMessage): boolean =>\n withdrawnTurns !== undefined && message.id !== undefined && withdrawnTurns.isWithdrawn(message.id);\n const laneTail = (\n status === 'stopped' || status === 'error'\n ? []\n : laneTailMessages(values, laneProjection, selfAuthor)\n ).filter((message) => !isWithdrawn(message));\n const sanitized: LangChainMessage[] = isLangGraphState(values)\n ? sanitizeLangChainMessages(values.messages, { toolOutputs })\n : [];\n // A withdrawn tail is a turn nobody is answering: no placeholder for it.\n const tail: LangChainMessage | undefined = sanitized[sanitized.length - 1];\n const tailWithdrawn: boolean = tail !== undefined && isWithdrawn(tail);\n const stateMessages = groupSpokenUserTurns(sanitized.filter((message) => !isWithdrawn(message)));\n return {\n messages: messageConverter.toThreadMessages(\n [...withPendingAssistantMessage(stateMessages, isRunning && !tailWithdrawn), ...laneTail],\n isRunning\n ),\n isRunning,\n };\n}\n"],"names":["createMessageConverter","convertLangChainMessages","laneEntryAuthor","ATHENA_MESSAGE_AUTHOR_KEY","buildOptimisticHumanContent","runsMaterialize","getExternalStoreMessages","readTruncatedToolContent","ATHENA_TRUNCATED_TOOL_OUTPUT_ARTIFACT_KEY","DEEP_AGENT_TRUNCATED_CONTENT_KEY","readDeepAgentStreamValues","groupSpokenUserTurns"],"mappings":"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"}
@@ -7,6 +7,7 @@ import { DEEP_AGENT_TRUNCATED_CONTENT_KEY } from "./generated/runconfig-contract
7
7
  import { readTruncatedToolContent, ATHENA_TRUNCATED_TOOL_OUTPUT_ARTIFACT_KEY } from "./message-window.js";
8
8
  import { buildOptimisticHumanContent } from "./optimistic-content.js";
9
9
  import { readDeepAgentStreamValues } from "./statewire-state.js";
10
+ import { groupSpokenUserTurns } from "./voice-turn-grouping.js";
10
11
  const messageConverter = createMessageConverter(convertLangChainMessages);
11
12
  const wirePartsToOptimisticParts = (parts) => parts.flatMap((part) => {
12
13
  if (part.type === "text") return [part];
@@ -414,7 +415,7 @@ function convertDeepAgentState(state, {
414
415
  const sanitized = isLangGraphState(values) ? sanitizeLangChainMessages(values.messages, { toolOutputs }) : [];
415
416
  const tail = sanitized[sanitized.length - 1];
416
417
  const tailWithdrawn = tail !== void 0 && isWithdrawn(tail);
417
- const stateMessages = sanitized.filter((message) => !isWithdrawn(message));
418
+ const stateMessages = groupSpokenUserTurns(sanitized.filter((message) => !isWithdrawn(message)));
418
419
  return {
419
420
  messages: messageConverter.toThreadMessages(
420
421
  [...withPendingAssistantMessage(stateMessages, isRunning && !tailWithdrawn), ...laneTail],