@athenaintel/react 0.30.0 → 0.35.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 (210) hide show
  1. package/README.md +160 -9
  2. package/dist/agent-runtime/src/athena-asset-drag.cjs +44 -0
  3. package/dist/agent-runtime/src/athena-asset-drag.cjs.map +1 -0
  4. package/dist/agent-runtime/src/athena-asset-drag.js +44 -0
  5. package/dist/agent-runtime/src/athena-asset-drag.js.map +1 -0
  6. package/dist/agent-runtime/src/composer-markdown.cjs.map +1 -1
  7. package/dist/agent-runtime/src/composer-markdown.js.map +1 -1
  8. package/dist/agent-runtime/src/config-mutations.cjs +59 -0
  9. package/dist/agent-runtime/src/config-mutations.cjs.map +1 -0
  10. package/dist/agent-runtime/src/config-mutations.js +59 -0
  11. package/dist/agent-runtime/src/config-mutations.js.map +1 -0
  12. package/dist/agent-runtime/src/deep-agent-transport-recipe.cjs +2 -3
  13. package/dist/agent-runtime/src/deep-agent-transport-recipe.cjs.map +1 -1
  14. package/dist/agent-runtime/src/deep-agent-transport-recipe.js +3 -4
  15. package/dist/agent-runtime/src/deep-agent-transport-recipe.js.map +1 -1
  16. package/dist/agent-runtime/src/edit-command.cjs.map +1 -1
  17. package/dist/agent-runtime/src/edit-command.js.map +1 -1
  18. package/dist/agent-runtime/src/generated/runconfig-contract.cjs +6 -0
  19. package/dist/agent-runtime/src/generated/runconfig-contract.cjs.map +1 -1
  20. package/dist/agent-runtime/src/generated/runconfig-contract.js +6 -0
  21. package/dist/agent-runtime/src/generated/runconfig-contract.js.map +1 -1
  22. package/dist/agent-runtime/src/messages.cjs +57 -2
  23. package/dist/agent-runtime/src/messages.cjs.map +1 -1
  24. package/dist/agent-runtime/src/messages.js +57 -2
  25. package/dist/agent-runtime/src/messages.js.map +1 -1
  26. package/dist/agent-runtime/src/quota-safe-local-storage-session.cjs +72 -3
  27. package/dist/agent-runtime/src/quota-safe-local-storage-session.cjs.map +1 -1
  28. package/dist/agent-runtime/src/quota-safe-local-storage-session.js +73 -4
  29. package/dist/agent-runtime/src/quota-safe-local-storage-session.js.map +1 -1
  30. package/dist/agent-runtime/src/statewire-transport.cjs +4 -13
  31. package/dist/agent-runtime/src/statewire-transport.cjs.map +1 -1
  32. package/dist/agent-runtime/src/statewire-transport.js +5 -14
  33. package/dist/agent-runtime/src/statewire-transport.js.map +1 -1
  34. package/dist/agent-runtime/src/tool-metadata.cjs +26 -7
  35. package/dist/agent-runtime/src/tool-metadata.cjs.map +1 -1
  36. package/dist/agent-runtime/src/tool-metadata.js +26 -7
  37. package/dist/agent-runtime/src/tool-metadata.js.map +1 -1
  38. package/dist/agent-runtime/src/use-deep-agent-config.cjs +117 -1
  39. package/dist/agent-runtime/src/use-deep-agent-config.cjs.map +1 -1
  40. package/dist/agent-runtime/src/use-deep-agent-config.js +118 -2
  41. package/dist/agent-runtime/src/use-deep-agent-config.js.map +1 -1
  42. package/dist/agent-runtime/src/use-deep-agent-thread.cjs.map +1 -1
  43. package/dist/agent-runtime/src/use-deep-agent-thread.js.map +1 -1
  44. package/dist/assets/AthenaAssetEmbed.cjs +9 -2
  45. package/dist/assets/AthenaAssetEmbed.cjs.map +1 -1
  46. package/dist/assets/AthenaAssetEmbed.d.ts +9 -0
  47. package/dist/assets/AthenaAssetEmbed.js +11 -4
  48. package/dist/assets/AthenaAssetEmbed.js.map +1 -1
  49. package/dist/assets/asset-embed-messages.cjs +6 -0
  50. package/dist/assets/asset-embed-messages.cjs.map +1 -1
  51. package/dist/assets/asset-embed-messages.d.ts +7 -0
  52. package/dist/assets/asset-embed-messages.js +6 -0
  53. package/dist/assets/asset-embed-messages.js.map +1 -1
  54. package/dist/assets/use-asset-embed.cjs +71 -71
  55. package/dist/assets/use-asset-embed.cjs.map +1 -1
  56. package/dist/assets/use-asset-embed.d.ts +20 -7
  57. package/dist/assets/use-asset-embed.js +72 -72
  58. package/dist/assets/use-asset-embed.js.map +1 -1
  59. package/dist/catalog/agent-model-options.cjs +122 -0
  60. package/dist/catalog/agent-model-options.cjs.map +1 -0
  61. package/dist/catalog/agent-model-options.d.ts +115 -0
  62. package/dist/catalog/agent-model-options.js +122 -0
  63. package/dist/catalog/agent-model-options.js.map +1 -0
  64. package/dist/catalog/chat-pickers.cjs +184 -4
  65. package/dist/catalog/chat-pickers.cjs.map +1 -1
  66. package/dist/catalog/chat-pickers.d.ts +27 -3
  67. package/dist/catalog/chat-pickers.js +187 -7
  68. package/dist/catalog/chat-pickers.js.map +1 -1
  69. package/dist/catalog/runtime-catalog.cjs +117 -3
  70. package/dist/catalog/runtime-catalog.cjs.map +1 -1
  71. package/dist/catalog/runtime-catalog.d.ts +104 -0
  72. package/dist/catalog/runtime-catalog.js +119 -5
  73. package/dist/catalog/runtime-catalog.js.map +1 -1
  74. package/dist/chat/AthenaChat.cjs +41 -26
  75. package/dist/chat/AthenaChat.cjs.map +1 -1
  76. package/dist/chat/AthenaChat.d.ts +8 -7
  77. package/dist/chat/AthenaChat.js +41 -26
  78. package/dist/chat/AthenaChat.js.map +1 -1
  79. package/dist/chat/super-grouping/SuperGroupingCard.cjs +61 -14
  80. package/dist/chat/super-grouping/SuperGroupingCard.cjs.map +1 -1
  81. package/dist/chat/super-grouping/SuperGroupingCard.d.ts +26 -12
  82. package/dist/chat/super-grouping/SuperGroupingCard.js +63 -16
  83. package/dist/chat/super-grouping/SuperGroupingCard.js.map +1 -1
  84. package/dist/chat/super-grouping/SuperGroupingTranscript.cjs +80 -53
  85. package/dist/chat/super-grouping/SuperGroupingTranscript.cjs.map +1 -1
  86. package/dist/chat/super-grouping/SuperGroupingTranscript.d.ts +10 -10
  87. package/dist/chat/super-grouping/SuperGroupingTranscript.js +81 -54
  88. package/dist/chat/super-grouping/SuperGroupingTranscript.js.map +1 -1
  89. package/dist/chat/super-grouping/activity-timeline.cjs +19 -4
  90. package/dist/chat/super-grouping/activity-timeline.cjs.map +1 -1
  91. package/dist/chat/super-grouping/activity-timeline.d.ts +24 -5
  92. package/dist/chat/super-grouping/activity-timeline.js +19 -4
  93. package/dist/chat/super-grouping/activity-timeline.js.map +1 -1
  94. package/dist/chat/super-grouping/index.cjs +2 -0
  95. package/dist/chat/super-grouping/index.cjs.map +1 -1
  96. package/dist/chat/super-grouping/index.d.ts +7 -6
  97. package/dist/chat/super-grouping/index.js +3 -1
  98. package/dist/chat/super-grouping/toolkit-map.cjs +4 -1
  99. package/dist/chat/super-grouping/toolkit-map.cjs.map +1 -1
  100. package/dist/chat/super-grouping/toolkit-map.js +4 -1
  101. package/dist/chat/super-grouping/toolkit-map.js.map +1 -1
  102. package/dist/chat-ui/composer/tiptap-composer.cjs +40 -4
  103. package/dist/chat-ui/composer/tiptap-composer.cjs.map +1 -1
  104. package/dist/chat-ui/composer/tiptap-composer.js +40 -4
  105. package/dist/chat-ui/composer/tiptap-composer.js.map +1 -1
  106. package/dist/chat-ui/index.cjs +1 -0
  107. package/dist/chat-ui/index.cjs.map +1 -1
  108. package/dist/chat-ui/index.d.ts +1 -1
  109. package/dist/chat-ui/index.js +2 -1
  110. package/dist/chat-ui/mentions/mention-chip.cjs +29 -2
  111. package/dist/chat-ui/mentions/mention-chip.cjs.map +1 -1
  112. package/dist/chat-ui/mentions/mention-chip.d.ts +2 -5
  113. package/dist/chat-ui/mentions/mention-chip.js +30 -3
  114. package/dist/chat-ui/mentions/mention-chip.js.map +1 -1
  115. package/dist/chat-ui/mentions/mention-extension.cjs +1 -1
  116. package/dist/chat-ui/mentions/mention-extension.cjs.map +1 -1
  117. package/dist/chat-ui/mentions/mention-extension.js +1 -1
  118. package/dist/chat-ui/mentions/mention-extension.js.map +1 -1
  119. package/dist/chat-ui/mentions/suggestions/data/api-client.cjs +33 -2
  120. package/dist/chat-ui/mentions/suggestions/data/api-client.cjs.map +1 -1
  121. package/dist/chat-ui/mentions/suggestions/data/api-client.d.ts +14 -0
  122. package/dist/chat-ui/mentions/suggestions/data/api-client.js +33 -2
  123. package/dist/chat-ui/mentions/suggestions/data/api-client.js.map +1 -1
  124. package/dist/chat-ui/mentions/use-account-workspace-id.cjs +61 -0
  125. package/dist/chat-ui/mentions/use-account-workspace-id.cjs.map +1 -0
  126. package/dist/chat-ui/mentions/use-account-workspace-id.d.ts +3 -0
  127. package/dist/chat-ui/mentions/use-account-workspace-id.js +61 -0
  128. package/dist/chat-ui/mentions/use-account-workspace-id.js.map +1 -0
  129. package/dist/chat-ui/mentions/use-member-avatar.cjs +75 -0
  130. package/dist/chat-ui/mentions/use-member-avatar.cjs.map +1 -0
  131. package/dist/chat-ui/mentions/use-member-avatar.d.ts +2 -0
  132. package/dist/chat-ui/mentions/use-member-avatar.js +75 -0
  133. package/dist/chat-ui/mentions/use-member-avatar.js.map +1 -0
  134. package/dist/collab/client.cjs +80 -19
  135. package/dist/collab/client.cjs.map +1 -1
  136. package/dist/collab/client.d.ts +22 -0
  137. package/dist/collab/client.js +80 -19
  138. package/dist/collab/client.js.map +1 -1
  139. package/dist/diagnostics/athena-diagnostics.cjs.map +1 -1
  140. package/dist/diagnostics/athena-diagnostics.d.ts +3 -1
  141. package/dist/diagnostics/athena-diagnostics.js.map +1 -1
  142. package/dist/index.cjs +5 -0
  143. package/dist/index.cjs.map +1 -1
  144. package/dist/index.d.ts +6 -3
  145. package/dist/index.js +7 -2
  146. package/dist/index.js.map +1 -1
  147. package/dist/lib/use-athena-rest-query.cjs +4 -0
  148. package/dist/lib/use-athena-rest-query.cjs.map +1 -1
  149. package/dist/lib/use-athena-rest-query.d.ts +6 -0
  150. package/dist/lib/use-athena-rest-query.js +4 -0
  151. package/dist/lib/use-athena-rest-query.js.map +1 -1
  152. package/dist/provider/AthenaProvider.cjs +57 -6
  153. package/dist/provider/AthenaProvider.cjs.map +1 -1
  154. package/dist/provider/AthenaProvider.d.ts +21 -5
  155. package/dist/provider/AthenaProvider.js +59 -8
  156. package/dist/provider/AthenaProvider.js.map +1 -1
  157. package/dist/provider/agent-selection-context.cjs +6 -0
  158. package/dist/provider/agent-selection-context.cjs.map +1 -0
  159. package/dist/provider/agent-selection-context.d.ts +14 -0
  160. package/dist/provider/agent-selection-context.js +6 -0
  161. package/dist/provider/agent-selection-context.js.map +1 -0
  162. package/dist/react/package.json.cjs +1 -1
  163. package/dist/react/package.json.js +1 -1
  164. package/dist/runtime/StatewireThreadConfigLaneSync.cjs +125 -0
  165. package/dist/runtime/StatewireThreadConfigLaneSync.cjs.map +1 -0
  166. package/dist/runtime/StatewireThreadConfigLaneSync.d.ts +54 -0
  167. package/dist/runtime/StatewireThreadConfigLaneSync.js +125 -0
  168. package/dist/runtime/StatewireThreadConfigLaneSync.js.map +1 -0
  169. package/dist/runtime/statewire-lifecycle.cjs +5 -2
  170. package/dist/runtime/statewire-lifecycle.cjs.map +1 -1
  171. package/dist/runtime/statewire-lifecycle.d.ts +5 -0
  172. package/dist/runtime/statewire-lifecycle.js +5 -2
  173. package/dist/runtime/statewire-lifecycle.js.map +1 -1
  174. package/dist/runtime/statewire-model-lane.cjs +16 -0
  175. package/dist/runtime/statewire-model-lane.cjs.map +1 -0
  176. package/dist/runtime/statewire-model-lane.d.ts +66 -0
  177. package/dist/runtime/statewire-model-lane.js +16 -0
  178. package/dist/runtime/statewire-model-lane.js.map +1 -0
  179. package/dist/runtime/statewire-run-config.cjs +16 -2
  180. package/dist/runtime/statewire-run-config.cjs.map +1 -1
  181. package/dist/runtime/statewire-run-config.d.ts +10 -0
  182. package/dist/runtime/statewire-run-config.js +16 -2
  183. package/dist/runtime/statewire-run-config.js.map +1 -1
  184. package/dist/runtime/statewire-server-lane-context.cjs +24 -0
  185. package/dist/runtime/statewire-server-lane-context.cjs.map +1 -0
  186. package/dist/runtime/statewire-server-lane-context.d.ts +30 -0
  187. package/dist/runtime/statewire-server-lane-context.js +24 -0
  188. package/dist/runtime/statewire-server-lane-context.js.map +1 -0
  189. package/dist/runtime/statewire-thread-config-lane.cjs +84 -0
  190. package/dist/runtime/statewire-thread-config-lane.cjs.map +1 -0
  191. package/dist/runtime/statewire-thread-config-lane.d.ts +94 -0
  192. package/dist/runtime/statewire-thread-config-lane.js +84 -0
  193. package/dist/runtime/statewire-thread-config-lane.js.map +1 -0
  194. package/dist/runtime/useAthenaStatewireRuntime.cjs +38 -18
  195. package/dist/runtime/useAthenaStatewireRuntime.cjs.map +1 -1
  196. package/dist/runtime/useAthenaStatewireRuntime.d.ts +8 -1
  197. package/dist/runtime/useAthenaStatewireRuntime.js +39 -19
  198. package/dist/runtime/useAthenaStatewireRuntime.js.map +1 -1
  199. package/dist/styles.css +1 -1
  200. package/dist/threads/api.cjs +2 -1
  201. package/dist/threads/api.cjs.map +1 -1
  202. package/dist/threads/api.d.ts +1 -0
  203. package/dist/threads/api.js +2 -1
  204. package/dist/threads/api.js.map +1 -1
  205. package/dist/threads/use-athena-thread-model.cjs +22 -0
  206. package/dist/threads/use-athena-thread-model.cjs.map +1 -0
  207. package/dist/threads/use-athena-thread-model.d.ts +27 -0
  208. package/dist/threads/use-athena-thread-model.js +22 -0
  209. package/dist/threads/use-athena-thread-model.js.map +1 -0
  210. package/package.json +4 -4
package/README.md CHANGED
@@ -16,11 +16,10 @@ npm install @propelauth/react
16
16
 
17
17
  ## Upgrading
18
18
 
19
- SDK0.29.0 uses native Statewire wire2026-09-30 with Athena application2026-09-13
20
- when installed with public, unpatched dependencies, over HTTP/SSE: published
21
- statewire0.20.0 pings a WebSocket with empty frames that Athena's hosts reject,
22
- so the SDK opens a socket only over the patched client the monorepo's own apps
23
- install. It requires a compatible backend. Publication is temporarily withheld until production HTTP+WebSocket
19
+ SDK0.29.0 uses native Statewire wire2026-09-30 with Athena application2026-09-13.
20
+ With statewire 0.21 it opens a WebSocket on public, unpatched installs too: its
21
+ `{}` ping is answered by Athena's hosts from Python statewire 0.19.1 on. It
22
+ requires a compatible backend. Publication is temporarily withheld until production HTTP+WebSocket
24
23
  and authorization tests pass and a compatible backend rollback release is
25
24
  retained. The publish workflow requires a manual
26
25
  `PRODUCTION_NATIVE_CONTEXT_AND_ROLLBACK_QUALIFIED` acknowledgement; dry runs
@@ -145,6 +144,64 @@ const [thread] = useState(() => ({ threadId: mintThreadId(), isNew: true }));
145
144
  embedded frame's text) into the chat the person last focused.
146
145
  - `SessionList`'s `onOpenInNewTab` hands the host a session to open in such a chat.
147
146
 
147
+ ### A model picker, and a session's own model
148
+
149
+ Pass the picked model as `model`. Changing it while a session is open applies
150
+ to that session's next turn:
151
+
152
+ - On a session that has not run yet, the next send carries it, and the server
153
+ keeps it as the session's model.
154
+ - On a session that has run, the server runs every turn with the model the
155
+ session's server config holds (another client, such as Athena itself, can
156
+ change it too), so the SDK writes the new model there
157
+ (`config/patch {model}`). Setting `model` back to `undefined` clears it, so
158
+ the collab agent's own model, or the default, applies again.
159
+ - The model a session is opened with is never written: opening a session never
160
+ overrides the model someone chose for it.
161
+
162
+ Show the session's real model when it opens with `useAthenaThreadModel()`
163
+ (inside `<AthenaProvider>`):
164
+
165
+ ```tsx
166
+ import { useAthenaThreadModel, useAthenaStatewireThreadId } from '@athenaintel/react';
167
+ import { useEffect, useRef } from 'react';
168
+
169
+ function SessionModelSeed({ onSessionModel }: { onSessionModel: (model: string | null) => void }) {
170
+ const threadId = useAthenaStatewireThreadId();
171
+ const { model, configured, ready } = useAthenaThreadModel();
172
+ const seededFor = useRef<string | null>(null);
173
+ useEffect(() => {
174
+ // Once per session, after its snapshot lands; a session without server
175
+ // config keeps the picker's current value.
176
+ if (!ready || seededFor.current === threadId) return;
177
+ seededFor.current = threadId;
178
+ if (configured) onSessionModel(model); // null: the agent's / default model ("Auto")
179
+ }, [threadId, model, configured, ready, onSessionModel]);
180
+ return null;
181
+ }
182
+ ```
183
+
184
+ `model` is `null` when the session pins no model; `configured` is `false` for
185
+ a session without server config yet; wait for `ready` before reading either. A
186
+ refused write (a view-only collaborator, or a workspace whose sessions cannot
187
+ hold server config) is reported as a `statewire.config` diagnostics event, and
188
+ the session keeps running with its own model.
189
+
190
+ The same holds for `systemPrompt`, `tools` (with the backend-bound
191
+ `frontendTools`), `workbench` and `knowledgeBase`. On a session that has run,
192
+ the server runs every turn with the values its server config holds, so
193
+ changing one of these props while the session is open writes it there: one
194
+ `config/patch` per change, carrying exactly the changed keys (`model`,
195
+ `system_prompt`, `enabled_tools`, `workbench`, `knowledge_base`). A blank
196
+ prompt or an empty list clears its key, so the collab agent's own applies
197
+ again. The values a session is opened with are never written, and a host that
198
+ never changes these props after the first send writes nothing. The
199
+ `statewire.config` event names the refused keys other than the model in
200
+ `config_keys`, never their values.
201
+
202
+ For a collab agent whose author restricts it to its own models, see
203
+ [Offering an agent's own models](#offering-an-agents-own-models).
204
+
148
205
  If `config.appUrl` is omitted, the provider resolves it from the parent bridge or from the matching Athena environment defaults. Known Athena staging and production `apiUrl` and `backendUrl` values automatically fall back to the corresponding frontend origin.
149
206
 
150
207
  Use `trustedParentOrigins` when the SDK runs inside an iframe on a private-cloud or custom parent domain and you want to explicitly allow postMessage auth/config from that parent. Only the window that frames the app is heard. By default `athenaintel.com` hosts, localhost and your own origin are trusted, except the hosts that serve customer-written code (`*.app.athenaintel.com`, `*.code-sandpack.athenaintel.com` and friends; see "What's new in 0.28.0").
@@ -214,10 +271,10 @@ What the statewire transport wires up in `AthenaChat`:
214
271
  its `classifyToolResult`; the fallback takes host seams (`displayName`,
215
272
  `icon`, `extraAction`, `notice`, `renderResult`, `onOpenAsset`,
216
273
  `hideInternalIdArgs`) instead of being copied. `@athenaintel/react/super-grouping`
217
- exports the grouped ("conversational") view of an assistant turn —
218
- `SuperGroupingTranscript` (what `AthenaChat` renders), `SuperGroupingCard`,
219
- `SuperGroupingFinalText`, `CollapsibleGroup`, the activity-timeline
220
- derivation, and the tool-based grouping function — with
274
+ exports the collapsed view (previously called the conversational view) of
275
+ an assistant turn — `SuperGroupingTranscript` (what `AthenaChat` renders),
276
+ `SuperGroupingCard`, `SuperGroupingFinalText`, `CollapsibleGroup`, the
277
+ activity-timeline derivation, and the tool-based grouping function — with
221
278
  slots for the host's text/tool renderers and controls (`renderHeaderActions`,
222
279
  `renderRunningMeta`, `summaryDetails`, `waitingTools`, `expansionKey`), so
223
280
  Olympus and the Chrome extension mount one card instead of copies.
@@ -717,6 +774,41 @@ Both use the same bearer / API key as session creation. The full recipe
717
774
  template skill `voice-session-guide.md`. `createAthenaVoiceSession` is also
718
775
  exported if you hold the bridge state yourself.
719
776
 
777
+ ## What's new in 0.35.0
778
+
779
+ **`AthenaAssetEmbed` and `useAssetEmbed` can offer Quick Edit.** Pass `allowQuickEdit` (default `false`) with `readOnly={false}` to mint an editable embed link whose viewer can press ⌘J (Ctrl+J off a Mac), or pick **AI Quick Edit** from the right-click menu, to rewrite a selection in a spreadsheet or Word document. It applies to the `minimal` display mode, where the viewer does not sign in; in `full` mode the signed-in viewer already has Athena's own Quick Edit. The request carries `allow_quick_edit: true` only when you ask, so a backend that predates it never sees the field. Runs are billed to the asset's workspace under the account that created the link, and each link has its own hourly cap. A link minted with `embedUrl` says for itself whether it offers Quick Edit. See the embeds guide in the Athena docs.
780
+
781
+ **`useAssetEmbed` mints through TanStack Query.** It uses your `QueryClientProvider`'s client when there is one, else one client for the page. As before, embeds of the same asset, mode and identity share one minted link until it expires. A rotated token for the same session mints none, and a link is never minted again while it is valid: no refetch on window focus or reconnect, no retry. The query key carries a fingerprint of the identity, never the token or API key itself.
782
+
783
+
784
+ **A prompt, tool list, workbench or knowledge base changed on a session that has run applies.** 0.32.0 did this for `model` only. Once a session has server config, the server runs every turn with the values that config holds and ignores the ones a send carries, so a `systemPrompt`, `tools`/`frontendTools`, `workbench` or `knowledgeBase` changed after the first turn used to be dropped. Changing one while such a session is open now writes it there: one `config/patch` per change, carrying exactly the changed keys (`system_prompt`, `enabled_tools`, `workbench`, `knowledge_base`, beside `model`). A blank prompt or an empty list clears its key, so the collab agent's own applies. As with the model, the values a session is opened with are never written, a host that never changes these props after the first send sees no new writes, and a refused write is a `statewire.config` diagnostics event (`config_keys` names the keys). See [A model picker, and a session's own model](#a-model-picker-and-a-sessions-own-model).
785
+
786
+ **`ChatModelPicker` follows an agent's "Allow any workspace model" switch.** Restriction is opt-in per agent: with the switch on (the default, and every agent saved before it existed) a chosen agent's picker is the workspace catalog with "Agent's own model", as before, now naming the agent's default (its channel's override on a channel that has one). With the switch off (`allowAnyModel: false` in the published definition), the picker lists exactly the models the agent's author listed, by their labels, with the channel's override as the default there. One option is a label; picking the default hands the host `null` (the existing "Agent's own model"); a session's model outside the list is shown, never replaced. Which agent: `agent`, or `agentSelected` inside the provider, which then reads the provider's own agent and channel. Without an agent the picker is unchanged. **`useAthenaAgentModelOptions({ agent, channel })`** returns the same choices and `allowAnyModel` for a picker of your own. See [Offering an agent's own models](#offering-an-agents-own-models).
787
+
788
+ **`fetchGraphQL` failures carry their code.** It throws an `AthenaGraphQLError` (exported from `@athenaintel/react/chat-ui`) with the HTTP `status` or the first GraphQL error's `extensions.code`; the messages are unchanged.
789
+
790
+ ## What's new in 0.33.0
791
+
792
+ **Chat runs over the WebSocket on a plain npm install.** The SDK used to open a socket only over the statewire client the monorepo patched, because published statewire 0.20 pinged with empty frames that Athena's hosts closed. So every app installed from npm fell back to HTTP/SSE. The SDK now depends on statewire 0.21.0 and `@assistant-ui/react-statewire` 0.31.0. Athena's hosts answer the ping, and the SDK opens a WebSocket unless the host asks for SSE (#45143). A rejected model context (for example one over the 64 KiB cap) is reported through `onError`, and sends are held until a valid one is accepted, instead of throwing during render. Nothing to change in your app beyond taking this version; keep `statewire`, `@assistant-ui/react-statewire` and `react` deduplicated as before.
793
+
794
+ ## What's new in 0.32.0
795
+
796
+ **A model picked after mount reaches the server.** The provider registered its run config once per thread attach, so a `model` (or `channel`, `tools`, `systemPrompt`, …) changed after mount never reached the next send unless something else happened to re-sync the context. The registration is now keyed by the built run config's value: a changed value re-syncs before the next send, while fresh array literals per render still change nothing.
797
+
798
+ **A model changed on a session that has run applies.** Once a session has server config, the server runs every turn with the model that config holds and ignores the one a send carries, so a picker change after the first turn used to be dropped. Changing `model` while such a session is open now writes it there (`config/patch {model}`; `undefined` clears it so the collab agent's own model applies). The model a session is opened with is never written. See [A model picker, and a session's own model](#a-model-picker-and-a-sessions-own-model).
799
+
800
+ **`useAthenaThreadModel()`** returns `{ model, configured, ready }`: the model the open session's server config holds, so an app can show a session's real model when it opens. A refused write is reported as the new `statewire.config` diagnostics event.
801
+
802
+ **A collab agent's session no longer records the SDK default as its model.** With `agent` set and no `model`, the run config's `panel_config` (which the server stores on the session's first run and replays to every client) carried `DEFAULT_STATEWIRE_MODEL`, so Athena showed that model for sessions running the agent's own. It now names no model there.
803
+
804
+ ## What's new in 0.31.0
805
+
806
+ **Draft tool cards stay in the conversation.** In the grouped view (`groupToolCalls`), a tool flagged `inline` in the shared display registry (`@athenaintel/agent-runtime` `ToolDisplayMeta.inline`; today the email and calendar-event draft tools) renders its card below the activity header, outside the "Used N tools" and "Show tool details" disclosures, and stays mounted whether the card is collapsed or not, so the person can edit and send the draft without opening anything. The call still appears as a step in the timeline, the run does not pause, and the card is not rendered a second time inside the details. The transcript view `AthenaChat` renders (`SuperGroupingTranscript`) does the same per activity segment: the draft card sits under its segment's step row, and a segment whose only step is a draft offers no empty "Show tool details". `SuperGroupingCard` and `SuperGroupingTranscript` take `isInlineTool` to override the registry per host; `excludePartsFromGroups` is exported for hosts that build their own grouped view.
807
+
808
+ ## What's new in 0.30.1
809
+
810
+ **Spoken words no longer retype from an empty bubble while a voice turn speaks over a run.** In the grouped (conversational) view, the answer below the card is the text after the last tool call plus the words of every spoken voice turn. `SuperGroupingFinalText` used to join those parts into one string, so when a spoken turn kept streaming while the run's own reply streamed after it, the joined text changed in the middle rather than at its end. `MarkdownText`'s streaming reveal restarts whenever its text stops extending what it showed, so the whole answer was wiped and retyped on every spoken word. Each answer part now renders through its own `TextComponent` (keyed by its content index) inside a `data-slot="super-grouping-final-text"` column, and only the last part carries the running status and caret. `getFinalTextParts` (with `FinalTextSegment`) is exported beside `getFinalText`, which still returns the joined string. Nothing to change in your app.
811
+
720
812
  ## What's new in 0.30.0
721
813
 
722
814
  **Session workbench, closer to Devin's.** The "+" menu now sits right after the tabs and has a search box (Enter opens the first match). Inside `AthenaLayout`, a maximize button gives the workspace the chat's width; the chat stays mounted, hidden, and collapsing a maximized workspace brings it back. With every tab closed, the column lists each pane centred with a one-line `description`, which is a new optional field on `SessionWorkbenchPane` that the built-in panes now carry. Tabs use 13px labels, and the close control stays quiet until the tab is hovered, focused or selected. Pure helper: `filterWorkbenchPanes`.
@@ -1224,6 +1316,63 @@ Pass them only when you deliberately want to override the agent — an explicit
1224
1316
  `model` is honoured as a caller override, with the agent's model as the default
1225
1317
  beneath it.
1226
1318
 
1319
+ ### Offering an agent's own models
1320
+
1321
+ An agent's author lists the models it may run (the agent editor's Models
1322
+ section), and decides with its **Allow any workspace model** switch whether
1323
+ people may still pick any other. Restriction is opt-in: the switch is on by
1324
+ default, and every agent saved before it existed allows any model.
1325
+
1326
+ With an agent chosen, `ChatModelPicker` reads the agent's published definition:
1327
+
1328
+ - **Allow any workspace model on** (the default; also an agent the person
1329
+ cannot view): the workspace's models and "Agent's own model" (`null`),
1330
+ exactly as before, with the agent's default (its channel's override when the
1331
+ chat runs on a channel that has one) named under "Agent's own model".
1332
+ - **Switch off** (`allowAnyModel: false`): exactly the agent's own models, not
1333
+ the workspace's.
1334
+
1335
+ ```tsx
1336
+ <ChatModelPicker
1337
+ value={modelId} // null: the agent's own model
1338
+ onChange={setModelId}
1339
+ defaultModelId="claude-opus-5-5"
1340
+ agent={agentId} // or agentSelected, inside the provider: its own agent and channel
1341
+ channel="askbob_web"
1342
+ />
1343
+ ```
1344
+
1345
+ For an agent with the switch off:
1346
+
1347
+ - The options are the agent's labeled models, by the author's labels; an
1348
+ automatic name shows the model's catalog name. A legacy agent with no list
1349
+ offers its single model.
1350
+ - The channel's model override is that channel's default, added when the list
1351
+ lacks it. Picking the default hands you `null`: pass `model={undefined}` to
1352
+ the provider, so the agent's (or the channel's) default runs and a session
1353
+ that pinned another model is unpinned. Picking it while the session pins
1354
+ nothing writes nothing. A choice of yours that names the default model is a
1355
+ pin the agent's next default would not follow, so the menu then also offers
1356
+ "Agent's own model" (`null`).
1357
+ - A model the session pins outside the list, or a choice of yours the agent
1358
+ does not list, is shown as it is, never replaced.
1359
+ - The agent is read again every minute while the picker shows, so a newly
1360
+ published list appears without a reload.
1361
+ - One option is a label, not a menu.
1362
+ - Until the agent has been read, the workspace menu shows but replaces nothing
1363
+ on your behalf, since your model may be one only the agent offers.
1364
+
1365
+ `useAthenaAgentModelOptions({ agent, channel })` is the same data for a picker
1366
+ of your own: `{ status, options: [{ model, label, isDefault, pinnedOutside }],
1367
+ defaultModel, allowAnyModel }`. Offer only `options` when `allowAnyModel` is
1368
+ `false`; it is `true` when the definition says nothing, `null` or `true`.
1369
+ `agent` is the asset id or the `collab_agent:<id>` ref. The agent is read
1370
+ through the permission-checked asset query (`getAssetById`, VIEW on the agent)
1371
+ with the provider's credentials, so `status` is `unavailable` (no options,
1372
+ `allowAnyModel: true`, not an error) for a person who cannot view it, or an
1373
+ agent never published. The session's pinned model comes from
1374
+ `useAthenaThreadModel()`.
1375
+
1227
1376
  ### Failure codes
1228
1377
 
1229
1378
  A refused selection surfaces as an `AthenaSdkError` with
@@ -1364,6 +1513,8 @@ host that opens a cited place in Athena on the web writes it into the Spaces lin
1364
1513
 
1365
1514
  Use `AthenaAssetEmbed` to render a native Athena asset without chat, thread history, tabs, or the SDK split-pane layout. It defaults to the editable, full native asset experience (`readOnly={false}` and `displayMode="full"`). The embed token and the current user must still have edit permission; otherwise Athena renders the asset read-only.
1366
1515
 
1516
+ With `displayMode="minimal"`, where the viewer does not sign in, pass `allowQuickEdit` (and `readOnly={false}`) to let the viewer Quick Edit a spreadsheet or Word document with ⌘J. Its runs are billed to the asset's workspace, with an hourly cap per link.
1517
+
1367
1518
  ```tsx
1368
1519
  import { useState } from 'react';
1369
1520
  import {
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const ATHENA_ASSET_DRAG_MIME = "application/x-athena-asset+json";
4
+ const FLEXLAYOUT_TAB_DRAG_MIME = "application/x-flexlayout-tab";
5
+ const FLEXLAYOUT_DRAG_TEXT_SENTINEL = "--flexlayout--";
6
+ const ATHENA_ASSET_DRAG_TYPES = [
7
+ ATHENA_ASSET_DRAG_MIME,
8
+ FLEXLAYOUT_TAB_DRAG_MIME
9
+ ];
10
+ function isAthenaAssetDrag(types) {
11
+ return Array.from(types).some((type) => ATHENA_ASSET_DRAG_TYPES.includes(type));
12
+ }
13
+ function parseAthenaAsset(raw) {
14
+ if (!raw) return null;
15
+ try {
16
+ const value = JSON.parse(raw);
17
+ if (typeof value !== "object" || value === null) return null;
18
+ const { id, title, assetType } = value;
19
+ if (typeof id !== "string" || !id) return null;
20
+ return {
21
+ id,
22
+ title: typeof title === "string" && title ? title : "Untitled",
23
+ assetType: typeof assetType === "string" ? assetType : ""
24
+ };
25
+ } catch {
26
+ return null;
27
+ }
28
+ }
29
+ function readAthenaAssetDrop(dataTransfer) {
30
+ const asset = parseAthenaAsset(dataTransfer.getData(ATHENA_ASSET_DRAG_MIME));
31
+ if (asset) return { kind: "asset", asset };
32
+ const tabAssetId = dataTransfer.getData(FLEXLAYOUT_TAB_DRAG_MIME);
33
+ if (tabAssetId) return { kind: "unresolved", assetId: tabAssetId };
34
+ if (dataTransfer.getData("text/plain") === FLEXLAYOUT_DRAG_TEXT_SENTINEL) {
35
+ return { kind: "unresolved", assetId: null };
36
+ }
37
+ return null;
38
+ }
39
+ exports.ATHENA_ASSET_DRAG_MIME = ATHENA_ASSET_DRAG_MIME;
40
+ exports.FLEXLAYOUT_DRAG_TEXT_SENTINEL = FLEXLAYOUT_DRAG_TEXT_SENTINEL;
41
+ exports.FLEXLAYOUT_TAB_DRAG_MIME = FLEXLAYOUT_TAB_DRAG_MIME;
42
+ exports.isAthenaAssetDrag = isAthenaAssetDrag;
43
+ exports.readAthenaAssetDrop = readAthenaAssetDrop;
44
+ //# sourceMappingURL=athena-asset-drag.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"athena-asset-drag.cjs","sources":["../../../../agent-runtime/src/athena-asset-drag.ts"],"sourcesContent":["import type { UploadedChatAsset } from \"./asset-attachment\";\n\n/**\n * Cross-surface drag payload for an Athena asset. Olympus sets it when a\n * Spaces tab or sidebar row starts a native drag so a composer in another\n * document (the Chrome extension side panel, an embedded SDK chat) can insert\n * an asset mention instead of the drag's incidental text.\n */\nexport const ATHENA_ASSET_DRAG_MIME = \"application/x-athena-asset+json\";\n\n/** Olympus's FlexLayout tab drags carry the tab's asset id under this type. */\nexport const FLEXLAYOUT_TAB_DRAG_MIME = \"application/x-flexlayout-tab\";\n\n/** The `text/plain` value FlexLayout writes on every tab drag. */\nexport const FLEXLAYOUT_DRAG_TEXT_SENTINEL = \"--flexlayout--\";\n\nconst ATHENA_ASSET_DRAG_TYPES: readonly string[] = [\n ATHENA_ASSET_DRAG_MIME,\n FLEXLAYOUT_TAB_DRAG_MIME,\n];\n\n/** The `DataTransfer` surface read here (agent-runtime has no DOM lib). */\ninterface DragData {\n getData(format: string): string;\n}\n\nexport function serializeAthenaAssetDrag(asset: UploadedChatAsset): string {\n return JSON.stringify({\n id: asset.id,\n title: asset.title,\n assetType: asset.assetType,\n });\n}\n\n/** Whether a drag carries an Athena asset — readable during `dragover`. */\nexport function isAthenaAssetDrag(types: Iterable<string>): boolean {\n return Array.from(types).some((type) => ATHENA_ASSET_DRAG_TYPES.includes(type));\n}\n\nfunction parseAthenaAsset(raw: string): UploadedChatAsset | null {\n if (!raw) return null;\n try {\n const value: unknown = JSON.parse(raw);\n if (typeof value !== \"object\" || value === null) return null;\n const { id, title, assetType } = value as Record<string, unknown>;\n if (typeof id !== \"string\" || !id) return null;\n return {\n id,\n title: typeof title === \"string\" && title ? title : \"Untitled\",\n assetType: typeof assetType === \"string\" ? assetType : \"\",\n };\n } catch {\n return null;\n }\n}\n\nexport type AthenaAssetDrop =\n | { kind: \"asset\"; asset: UploadedChatAsset }\n /** An Athena asset drag whose metadata did not travel — swallow it rather\n * than let the editor paste the sentinel text. */\n | { kind: \"unresolved\"; assetId: string | null }\n | null;\n\n/** Reads an Athena asset from a drop, or `null` when the drop is not one. */\nexport function readAthenaAssetDrop(dataTransfer: DragData): AthenaAssetDrop {\n const asset = parseAthenaAsset(dataTransfer.getData(ATHENA_ASSET_DRAG_MIME));\n if (asset) return { kind: \"asset\", asset };\n const tabAssetId = dataTransfer.getData(FLEXLAYOUT_TAB_DRAG_MIME);\n if (tabAssetId) return { kind: \"unresolved\", assetId: tabAssetId };\n if (dataTransfer.getData(\"text/plain\") === FLEXLAYOUT_DRAG_TEXT_SENTINEL) {\n return { kind: \"unresolved\", assetId: null };\n }\n return null;\n}\n"],"names":[],"mappings":";;AAQO,MAAM,yBAAyB;AAG/B,MAAM,2BAA2B;AAGjC,MAAM,gCAAgC;AAE7C,MAAM,0BAA6C;AAAA,EACjD;AAAA,EACA;AACF;AAgBO,SAAS,kBAAkB,OAAkC;AAClE,SAAO,MAAM,KAAK,KAAK,EAAE,KAAK,CAAC,SAAS,wBAAwB,SAAS,IAAI,CAAC;AAChF;AAEA,SAAS,iBAAiB,KAAuC;AAC/D,MAAI,CAAC,IAAK,QAAO;AACjB,MAAI;AACF,UAAM,QAAiB,KAAK,MAAM,GAAG;AACrC,QAAI,OAAO,UAAU,YAAY,UAAU,KAAM,QAAO;AACxD,UAAM,EAAE,IAAI,OAAO,UAAA,IAAc;AACjC,QAAI,OAAO,OAAO,YAAY,CAAC,GAAI,QAAO;AAC1C,WAAO;AAAA,MACL;AAAA,MACA,OAAO,OAAO,UAAU,YAAY,QAAQ,QAAQ;AAAA,MACpD,WAAW,OAAO,cAAc,WAAW,YAAY;AAAA,IAAA;AAAA,EAE3D,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAUO,SAAS,oBAAoB,cAAyC;AAC3E,QAAM,QAAQ,iBAAiB,aAAa,QAAQ,sBAAsB,CAAC;AAC3E,MAAI,MAAO,QAAO,EAAE,MAAM,SAAS,MAAA;AACnC,QAAM,aAAa,aAAa,QAAQ,wBAAwB;AAChE,MAAI,WAAY,QAAO,EAAE,MAAM,cAAc,SAAS,WAAA;AACtD,MAAI,aAAa,QAAQ,YAAY,MAAM,+BAA+B;AACxE,WAAO,EAAE,MAAM,cAAc,SAAS,KAAA;AAAA,EACxC;AACA,SAAO;AACT;;;;;;"}
@@ -0,0 +1,44 @@
1
+ const ATHENA_ASSET_DRAG_MIME = "application/x-athena-asset+json";
2
+ const FLEXLAYOUT_TAB_DRAG_MIME = "application/x-flexlayout-tab";
3
+ const FLEXLAYOUT_DRAG_TEXT_SENTINEL = "--flexlayout--";
4
+ const ATHENA_ASSET_DRAG_TYPES = [
5
+ ATHENA_ASSET_DRAG_MIME,
6
+ FLEXLAYOUT_TAB_DRAG_MIME
7
+ ];
8
+ function isAthenaAssetDrag(types) {
9
+ return Array.from(types).some((type) => ATHENA_ASSET_DRAG_TYPES.includes(type));
10
+ }
11
+ function parseAthenaAsset(raw) {
12
+ if (!raw) return null;
13
+ try {
14
+ const value = JSON.parse(raw);
15
+ if (typeof value !== "object" || value === null) return null;
16
+ const { id, title, assetType } = value;
17
+ if (typeof id !== "string" || !id) return null;
18
+ return {
19
+ id,
20
+ title: typeof title === "string" && title ? title : "Untitled",
21
+ assetType: typeof assetType === "string" ? assetType : ""
22
+ };
23
+ } catch {
24
+ return null;
25
+ }
26
+ }
27
+ function readAthenaAssetDrop(dataTransfer) {
28
+ const asset = parseAthenaAsset(dataTransfer.getData(ATHENA_ASSET_DRAG_MIME));
29
+ if (asset) return { kind: "asset", asset };
30
+ const tabAssetId = dataTransfer.getData(FLEXLAYOUT_TAB_DRAG_MIME);
31
+ if (tabAssetId) return { kind: "unresolved", assetId: tabAssetId };
32
+ if (dataTransfer.getData("text/plain") === FLEXLAYOUT_DRAG_TEXT_SENTINEL) {
33
+ return { kind: "unresolved", assetId: null };
34
+ }
35
+ return null;
36
+ }
37
+ export {
38
+ ATHENA_ASSET_DRAG_MIME,
39
+ FLEXLAYOUT_DRAG_TEXT_SENTINEL,
40
+ FLEXLAYOUT_TAB_DRAG_MIME,
41
+ isAthenaAssetDrag,
42
+ readAthenaAssetDrop
43
+ };
44
+ //# sourceMappingURL=athena-asset-drag.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"athena-asset-drag.js","sources":["../../../../agent-runtime/src/athena-asset-drag.ts"],"sourcesContent":["import type { UploadedChatAsset } from \"./asset-attachment\";\n\n/**\n * Cross-surface drag payload for an Athena asset. Olympus sets it when a\n * Spaces tab or sidebar row starts a native drag so a composer in another\n * document (the Chrome extension side panel, an embedded SDK chat) can insert\n * an asset mention instead of the drag's incidental text.\n */\nexport const ATHENA_ASSET_DRAG_MIME = \"application/x-athena-asset+json\";\n\n/** Olympus's FlexLayout tab drags carry the tab's asset id under this type. */\nexport const FLEXLAYOUT_TAB_DRAG_MIME = \"application/x-flexlayout-tab\";\n\n/** The `text/plain` value FlexLayout writes on every tab drag. */\nexport const FLEXLAYOUT_DRAG_TEXT_SENTINEL = \"--flexlayout--\";\n\nconst ATHENA_ASSET_DRAG_TYPES: readonly string[] = [\n ATHENA_ASSET_DRAG_MIME,\n FLEXLAYOUT_TAB_DRAG_MIME,\n];\n\n/** The `DataTransfer` surface read here (agent-runtime has no DOM lib). */\ninterface DragData {\n getData(format: string): string;\n}\n\nexport function serializeAthenaAssetDrag(asset: UploadedChatAsset): string {\n return JSON.stringify({\n id: asset.id,\n title: asset.title,\n assetType: asset.assetType,\n });\n}\n\n/** Whether a drag carries an Athena asset — readable during `dragover`. */\nexport function isAthenaAssetDrag(types: Iterable<string>): boolean {\n return Array.from(types).some((type) => ATHENA_ASSET_DRAG_TYPES.includes(type));\n}\n\nfunction parseAthenaAsset(raw: string): UploadedChatAsset | null {\n if (!raw) return null;\n try {\n const value: unknown = JSON.parse(raw);\n if (typeof value !== \"object\" || value === null) return null;\n const { id, title, assetType } = value as Record<string, unknown>;\n if (typeof id !== \"string\" || !id) return null;\n return {\n id,\n title: typeof title === \"string\" && title ? title : \"Untitled\",\n assetType: typeof assetType === \"string\" ? assetType : \"\",\n };\n } catch {\n return null;\n }\n}\n\nexport type AthenaAssetDrop =\n | { kind: \"asset\"; asset: UploadedChatAsset }\n /** An Athena asset drag whose metadata did not travel — swallow it rather\n * than let the editor paste the sentinel text. */\n | { kind: \"unresolved\"; assetId: string | null }\n | null;\n\n/** Reads an Athena asset from a drop, or `null` when the drop is not one. */\nexport function readAthenaAssetDrop(dataTransfer: DragData): AthenaAssetDrop {\n const asset = parseAthenaAsset(dataTransfer.getData(ATHENA_ASSET_DRAG_MIME));\n if (asset) return { kind: \"asset\", asset };\n const tabAssetId = dataTransfer.getData(FLEXLAYOUT_TAB_DRAG_MIME);\n if (tabAssetId) return { kind: \"unresolved\", assetId: tabAssetId };\n if (dataTransfer.getData(\"text/plain\") === FLEXLAYOUT_DRAG_TEXT_SENTINEL) {\n return { kind: \"unresolved\", assetId: null };\n }\n return null;\n}\n"],"names":[],"mappings":"AAQO,MAAM,yBAAyB;AAG/B,MAAM,2BAA2B;AAGjC,MAAM,gCAAgC;AAE7C,MAAM,0BAA6C;AAAA,EACjD;AAAA,EACA;AACF;AAgBO,SAAS,kBAAkB,OAAkC;AAClE,SAAO,MAAM,KAAK,KAAK,EAAE,KAAK,CAAC,SAAS,wBAAwB,SAAS,IAAI,CAAC;AAChF;AAEA,SAAS,iBAAiB,KAAuC;AAC/D,MAAI,CAAC,IAAK,QAAO;AACjB,MAAI;AACF,UAAM,QAAiB,KAAK,MAAM,GAAG;AACrC,QAAI,OAAO,UAAU,YAAY,UAAU,KAAM,QAAO;AACxD,UAAM,EAAE,IAAI,OAAO,UAAA,IAAc;AACjC,QAAI,OAAO,OAAO,YAAY,CAAC,GAAI,QAAO;AAC1C,WAAO;AAAA,MACL;AAAA,MACA,OAAO,OAAO,UAAU,YAAY,QAAQ,QAAQ;AAAA,MACpD,WAAW,OAAO,cAAc,WAAW,YAAY;AAAA,IAAA;AAAA,EAE3D,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAUO,SAAS,oBAAoB,cAAyC;AAC3E,QAAM,QAAQ,iBAAiB,aAAa,QAAQ,sBAAsB,CAAC;AAC3E,MAAI,MAAO,QAAO,EAAE,MAAM,SAAS,MAAA;AACnC,QAAM,aAAa,aAAa,QAAQ,wBAAwB;AAChE,MAAI,WAAY,QAAO,EAAE,MAAM,cAAc,SAAS,WAAA;AACtD,MAAI,aAAa,QAAQ,YAAY,MAAM,+BAA+B;AACxE,WAAO,EAAE,MAAM,cAAc,SAAS,KAAA;AAAA,EACxC;AACA,SAAO;AACT;"}
@@ -1 +1 @@
1
- {"version":3,"file":"composer-markdown.cjs","sources":["../../../../agent-runtime/src/composer-markdown.ts"],"sourcesContent":["/**\n * The composer markdown a chat message was sent from.\n *\n * The chat composer sends a message's plain text exactly as typed, because the\n * model reads the text literally, and writes real formatting (bold, lists,\n * code, mention chips) as markdown. The sent text alone no longer says which\n * characters are formatting and which were typed: literal `**strong**` and\n * bold `strong` both send `**strong**`, and `List<T>`, `a\\\\b` or `2*3*4` read\n * as markup when the text is rendered as markdown. So the composer also\n * attaches the draft the text was serialized from (the same document with its\n * plain text escaped for markdown) under {@link DEEP_AGENT_COMPOSER_MARKDOWN_KEY}\n * in the message's metadata, and only when the two differ.\n *\n * It rides the statewire message, the queue and steer lanes and the persisted\n * HumanMessage (`additional_kwargs.metadata`, which the converter surfaces as\n * `metadata.custom`). It is display-only: the bubble renders it, and Edit,\n * branch-and-edit and a withdrawn turn load it back into the composer. The\n * model reads the message text, never this. A message without it (older\n * messages, other clients) renders its text as before.\n *\n * agora validates it with the same rule\n * (`agora/graphs/athena_deep_agent/composer_markdown.py`).\n */\nexport const DEEP_AGENT_COMPOSER_MARKDOWN_KEY = 'composer_markdown';\n\nconst DRAFT_CHARACTER_REFERENCES: Readonly<Record<string, string>> = {\n lt: '<',\n gt: '>',\n amp: '&',\n '#32': ' ',\n '#9': '\\t',\n};\n\n/**\n * A sent text in comparable form: every backslash dropped (the draft's\n * escapes, and the `\\` before a hard break's newline, are backslashes).\n */\nconst comparableText = (text: string): string => text.replace(/\\\\/g, '');\n\n/**\n * A draft in comparable form: every backslash dropped, then the character\n * references the draft serializer writes decoded in one pass (`&lt;` and\n * `&gt;` for typed `<` and `>`, `&amp;` for a typed `&` that starts a\n * reference, so typed `&lt;` is `&amp;lt;`, and `&#32;` / `&#9;` for the space\n * or tab a line starts with, which markdown would otherwise drop).\n */\nconst comparableDraft = (markdown: string): string =>\n comparableText(markdown).replace(\n /&(lt|gt|amp|#32|#9);/g,\n (reference, name: string) => DRAFT_CHARACTER_REFERENCES[name] ?? reference\n );\n\n/**\n * Escaping at most turns one character into four (`<` → `&lt;`). A line's\n * first space becomes five (`&#32;`), but only once a line, so a whole line\n * with its line break stays under four a character; the slack covers short\n * texts. agora caps a stored draft the same way.\n */\nconst MAX_EXPANSION = 4;\nconst EXPANSION_SLACK_CHARS = 1024;\n\n/**\n * Whether `markdown` is the draft of `text`: no longer than escaping could make\n * it, and equal to it once backslashes are dropped and the draft's character\n * references decoded. A draft that would show other\n * words than the text the model read (another message's, a client's mistake) is\n * not taken; such a message renders its text as before.\n */\nexport function isComposerMarkdownOfText({\n markdown,\n text,\n}: {\n markdown: string;\n text: string;\n}): boolean {\n return (\n markdown.length <= text.length * MAX_EXPANSION + EXPANSION_SLACK_CHARS &&\n comparableDraft(markdown) === comparableText(text)\n );\n}\n\n/** The references a draft writes a line's first space or tab as. */\nconst DRAFT_WHITESPACE_REFERENCES: readonly string[] = ['&#32;', '&#9;'];\n\nconst isWhitespace = (character: string): boolean => /\\s/.test(character);\n\n/** A hard break in a draft: `\\` before the newline (the text holds the newline alone). */\nconst DRAFT_HARD_BREAK = '\\\\\\n';\n\n/** Whether an odd run of backslashes ends at `end`: its last one escapes what follows, not a typed `\\` (`\\\\`). */\nfunction endsInEscapingBackslash({\n markdown,\n start,\n end,\n}: {\n markdown: string;\n start: number;\n end: number;\n}): boolean {\n let backslashes = 0;\n while (end - backslashes > start && markdown.charAt(end - backslashes - 1) === '\\\\') {\n backslashes += 1;\n }\n return backslashes % 2 === 1;\n}\n\n/**\n * `markdown`, a draft, trimmed as its text is trimmed (`text.trim()`): a\n * send that trims its text stays a draft of it. `markdown.trim()` would keep\n * the `&#32;` a draft writes for the space its first line starts with, and the\n * `\\` of a line break whose newline it trims.\n */\nexport function trimComposerDraft(markdown: string): string {\n let start = 0;\n let end = markdown.length;\n for (;;) {\n while (start < end && isWhitespace(markdown.charAt(start))) start += 1;\n const reference = [...DRAFT_WHITESPACE_REFERENCES, DRAFT_HARD_BREAK].find(\n (candidate) => start + candidate.length <= end && markdown.startsWith(candidate, start)\n );\n if (reference === undefined) break;\n start += reference.length;\n }\n for (;;) {\n while (end > start && isWhitespace(markdown.charAt(end - 1))) end -= 1;\n const reference = DRAFT_WHITESPACE_REFERENCES.find(\n (candidate) => end - candidate.length >= start && markdown.endsWith(candidate, end)\n );\n if (reference !== undefined) {\n end -= reference.length;\n } else if (\n markdown.charAt(end) === '\\n' &&\n endsInEscapingBackslash({ markdown, start, end })\n ) {\n // The newline just trimmed was a line break's: its `\\` goes with it.\n end -= 1;\n } else {\n break;\n }\n }\n return markdown.slice(start, end);\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\n/**\n * The composer markdown to render or load for a message whose metadata is\n * `metadata` (a message's `metadata.custom`, or a lane entry's `metadata`) and\n * whose text is `text`; `null` when it carries none, or one that is not a\n * draft of `text`.\n */\nexport function readDeepAgentComposerMarkdown({\n metadata,\n text,\n}: {\n metadata: unknown;\n text: string;\n}): string | null {\n if (!isRecord(metadata)) return null;\n const markdown: unknown = metadata[DEEP_AGENT_COMPOSER_MARKDOWN_KEY];\n return typeof markdown === 'string' && isComposerMarkdownOfText({ markdown, text })\n ? markdown\n : null;\n}\n\n/**\n * How long a staged composer markdown waits for its submission\n * ({@link stageComposerMarkdown}).\n */\nexport const STAGED_COMPOSER_MARKDOWN_TTL_MS = 10_000;\n\ninterface StagedComposerMarkdown {\n readonly markdown: string;\n readonly stagedAt: number;\n}\n\n/**\n * The staged drafts live on `globalThis`, not in module scope, because two\n * copies of this module run in one page: the SDK bundles agent-runtime into its\n * dist, so its composer stages through the SDK's copy, while the Chrome\n * extension and the Office add-in build their runtime on agent-runtime directly\n * and take the draft through their own (see `asset-attachment.ts`).\n */\nconst STAGED_COMPOSER_MARKDOWN = Symbol.for('@athenaintel/agent-runtime/staged-composer-markdown');\n\ntype StagedComposerMarkdownHost = typeof globalThis & {\n [STAGED_COMPOSER_MARKDOWN]?: Map<string, StagedComposerMarkdown>;\n};\n\nfunction stagedComposerMarkdownByThread(): Map<string, StagedComposerMarkdown> {\n const host = globalThis as StagedComposerMarkdownHost;\n return (host[STAGED_COMPOSER_MARKDOWN] ??= new Map());\n}\n\n/**\n * Stage the draft `markdown` of `text` for `threadId`'s next submission.\n *\n * statewire captures message metadata once per new user message\n * (`getMessageMetadata`, patched into `@assistant-ui/react-statewire@0.30.0`)\n * without saying which message it is for, so a composer stages its draft right\n * before it sends and the runtime's getter takes it for the submission that\n * follows ({@link withStagedComposerMarkdown}). A draft equal to its text adds\n * nothing and clears the slot. A slot nobody takes within\n * {@link STAGED_COMPOSER_MARKDOWN_TTL_MS} expires; one a submission the\n * composer did not make takes is harmless, since a composer markdown that is\n * not a draft of the message's own text is neither kept by agora nor rendered.\n */\nexport function stageComposerMarkdown({\n threadId,\n text,\n markdown,\n now = Date.now(),\n}: {\n threadId: string;\n text: string;\n markdown: string;\n /** Test seam: the time of the send. */\n now?: number;\n}): void {\n const staged = stagedComposerMarkdownByThread();\n if (markdown === text) {\n staged.delete(threadId);\n return;\n }\n staged.set(threadId, { markdown, stagedAt: now });\n}\n\n/**\n * The message metadata for `threadId`'s submission now being journaled: the\n * staged composer markdown, if one is waiting and fresh. Takes the slot.\n */\nexport function takeStagedComposerMarkdownMetadata(\n threadId: string,\n now: number = Date.now()\n): Record<string, unknown> | undefined {\n const staged = stagedComposerMarkdownByThread();\n const entry = staged.get(threadId);\n staged.delete(threadId);\n if (entry === undefined || now - entry.stagedAt > STAGED_COMPOSER_MARKDOWN_TTL_MS) {\n return undefined;\n }\n return { [DEEP_AGENT_COMPOSER_MARKDOWN_KEY]: entry.markdown };\n}\n\ntype MessageMetadataGetter = () =>\n | Record<string, unknown>\n | undefined\n | Promise<Record<string, unknown> | undefined>;\n\n/**\n * A runtime's `getMessageMetadata` that also carries the composer markdown a\n * composer staged for `threadId` ({@link stageComposerMarkdown}), on top of the\n * host's own metadata (`getMessageMetadata`, when it has one). The slot is\n * taken synchronously at submission, before the host's capture resolves, so a\n * draft never rides a later message. With nothing staged the host's own value\n * is returned as is.\n */\nexport function withStagedComposerMarkdown({\n threadId,\n getMessageMetadata,\n}: {\n threadId: string;\n getMessageMetadata?: MessageMetadataGetter | undefined;\n}): MessageMetadataGetter {\n return () => {\n const staged = takeStagedComposerMarkdownMetadata(threadId);\n const captured = getMessageMetadata?.();\n if (staged === undefined) return captured;\n const merge = (host: Record<string, unknown> | undefined): Record<string, unknown> => ({\n ...host,\n ...staged,\n });\n return captured instanceof Promise ? captured.then(merge) : merge(captured);\n };\n}\n\n/** Test seam: drop every staged composer markdown. */\nexport function resetStagedComposerMarkdown(): void {\n stagedComposerMarkdownByThread().clear();\n}\n\n/**\n * `metadata` with the composer markdown set to the draft `markdown` of `text`,\n * or without it when the draft equals the text (it adds nothing then).\n * Returns `undefined` when nothing is left.\n */\nexport function withDeepAgentComposerMarkdown({\n metadata,\n text,\n markdown,\n}: {\n metadata: Readonly<Record<string, unknown>> | undefined;\n text: string;\n markdown: string;\n}): Record<string, unknown> | undefined {\n const { [DEEP_AGENT_COMPOSER_MARKDOWN_KEY]: _previous, ...rest } = metadata ?? {};\n const next: Record<string, unknown> =\n markdown === text ? rest : { ...rest, [DEEP_AGENT_COMPOSER_MARKDOWN_KEY]: markdown };\n return Object.keys(next).length > 0 ? next : undefined;\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"composer-markdown.cjs","sources":["../../../../agent-runtime/src/composer-markdown.ts"],"sourcesContent":["/**\n * The composer markdown a chat message was sent from.\n *\n * The chat composer sends a message's plain text exactly as typed, because the\n * model reads the text literally, and writes real formatting (bold, lists,\n * code, mention chips) as markdown. The sent text alone no longer says which\n * characters are formatting and which were typed: literal `**strong**` and\n * bold `strong` both send `**strong**`, and `List<T>`, `a\\\\b` or `2*3*4` read\n * as markup when the text is rendered as markdown. So the composer also\n * attaches the draft the text was serialized from (the same document with its\n * plain text escaped for markdown) under {@link DEEP_AGENT_COMPOSER_MARKDOWN_KEY}\n * in the message's metadata, and only when the two differ.\n *\n * It rides the statewire message, the queue and steer lanes and the persisted\n * HumanMessage (`additional_kwargs.metadata`, which the converter surfaces as\n * `metadata.custom`). It is display-only: the bubble renders it, and Edit,\n * branch-and-edit and a withdrawn turn load it back into the composer. The\n * model reads the message text, never this. A message without it (older\n * messages, other clients) renders its text as before.\n *\n * agora validates it with the same rule\n * (`agora/graphs/athena_deep_agent/composer_markdown.py`).\n */\nexport const DEEP_AGENT_COMPOSER_MARKDOWN_KEY = 'composer_markdown';\n\nconst DRAFT_CHARACTER_REFERENCES: Readonly<Record<string, string>> = {\n lt: '<',\n gt: '>',\n amp: '&',\n '#32': ' ',\n '#9': '\\t',\n};\n\n/**\n * A sent text in comparable form: every backslash dropped (the draft's\n * escapes, and the `\\` before a hard break's newline, are backslashes).\n */\nconst comparableText = (text: string): string => text.replace(/\\\\/g, '');\n\n/**\n * A draft in comparable form: every backslash dropped, then the character\n * references the draft serializer writes decoded in one pass (`&lt;` and\n * `&gt;` for typed `<` and `>`, `&amp;` for a typed `&` that starts a\n * reference, so typed `&lt;` is `&amp;lt;`, and `&#32;` / `&#9;` for the space\n * or tab a line starts with, which markdown would otherwise drop).\n */\nconst comparableDraft = (markdown: string): string =>\n comparableText(markdown).replace(\n /&(lt|gt|amp|#32|#9);/g,\n (reference, name: string) => DRAFT_CHARACTER_REFERENCES[name] ?? reference\n );\n\n/**\n * Escaping at most turns one character into four (`<` → `&lt;`). A line's\n * first space becomes five (`&#32;`), but only once a line, so a whole line\n * with its line break stays under four a character; the slack covers short\n * texts. agora caps a stored draft the same way.\n */\nconst MAX_EXPANSION = 4;\nconst EXPANSION_SLACK_CHARS = 1024;\n\n/**\n * Whether `markdown` is the draft of `text`: no longer than escaping could make\n * it, and equal to it once backslashes are dropped and the draft's character\n * references decoded. A draft that would show other\n * words than the text the model read (another message's, a client's mistake) is\n * not taken; such a message renders its text as before.\n */\nexport function isComposerMarkdownOfText({\n markdown,\n text,\n}: {\n markdown: string;\n text: string;\n}): boolean {\n return (\n markdown.length <= text.length * MAX_EXPANSION + EXPANSION_SLACK_CHARS &&\n comparableDraft(markdown) === comparableText(text)\n );\n}\n\n/** The references a draft writes a line's first space or tab as. */\nconst DRAFT_WHITESPACE_REFERENCES: readonly string[] = ['&#32;', '&#9;'];\n\nconst isWhitespace = (character: string): boolean => /\\s/.test(character);\n\n/** A hard break in a draft: `\\` before the newline (the text holds the newline alone). */\nconst DRAFT_HARD_BREAK = '\\\\\\n';\n\n/** Whether an odd run of backslashes ends at `end`: its last one escapes what follows, not a typed `\\` (`\\\\`). */\nfunction endsInEscapingBackslash({\n markdown,\n start,\n end,\n}: {\n markdown: string;\n start: number;\n end: number;\n}): boolean {\n let backslashes = 0;\n while (end - backslashes > start && markdown.charAt(end - backslashes - 1) === '\\\\') {\n backslashes += 1;\n }\n return backslashes % 2 === 1;\n}\n\n/**\n * `markdown`, a draft, trimmed as its text is trimmed (`text.trim()`): a\n * send that trims its text stays a draft of it. `markdown.trim()` would keep\n * the `&#32;` a draft writes for the space its first line starts with, and the\n * `\\` of a line break whose newline it trims.\n */\nexport function trimComposerDraft(markdown: string): string {\n let start = 0;\n let end = markdown.length;\n for (;;) {\n while (start < end && isWhitespace(markdown.charAt(start))) start += 1;\n const reference = [...DRAFT_WHITESPACE_REFERENCES, DRAFT_HARD_BREAK].find(\n (candidate) => start + candidate.length <= end && markdown.startsWith(candidate, start)\n );\n if (reference === undefined) break;\n start += reference.length;\n }\n for (;;) {\n while (end > start && isWhitespace(markdown.charAt(end - 1))) end -= 1;\n const reference = DRAFT_WHITESPACE_REFERENCES.find(\n (candidate) => end - candidate.length >= start && markdown.endsWith(candidate, end)\n );\n if (reference !== undefined) {\n end -= reference.length;\n } else if (\n markdown.charAt(end) === '\\n' &&\n endsInEscapingBackslash({ markdown, start, end })\n ) {\n // The newline just trimmed was a line break's: its `\\` goes with it.\n end -= 1;\n } else {\n break;\n }\n }\n return markdown.slice(start, end);\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\n/**\n * The composer markdown to render or load for a message whose metadata is\n * `metadata` (a message's `metadata.custom`, or a lane entry's `metadata`) and\n * whose text is `text`; `null` when it carries none, or one that is not a\n * draft of `text`.\n */\nexport function readDeepAgentComposerMarkdown({\n metadata,\n text,\n}: {\n metadata: unknown;\n text: string;\n}): string | null {\n if (!isRecord(metadata)) return null;\n const markdown: unknown = metadata[DEEP_AGENT_COMPOSER_MARKDOWN_KEY];\n return typeof markdown === 'string' && isComposerMarkdownOfText({ markdown, text })\n ? markdown\n : null;\n}\n\n/**\n * How long a staged composer markdown waits for its submission\n * ({@link stageComposerMarkdown}).\n */\nexport const STAGED_COMPOSER_MARKDOWN_TTL_MS = 10_000;\n\ninterface StagedComposerMarkdown {\n readonly markdown: string;\n readonly stagedAt: number;\n}\n\n/**\n * The staged drafts live on `globalThis`, not in module scope, because two\n * copies of this module run in one page: the SDK bundles agent-runtime into its\n * dist, so its composer stages through the SDK's copy, while the Chrome\n * extension and the Office add-in build their runtime on agent-runtime directly\n * and take the draft through their own (see `asset-attachment.ts`).\n */\nconst STAGED_COMPOSER_MARKDOWN = Symbol.for('@athenaintel/agent-runtime/staged-composer-markdown');\n\ntype StagedComposerMarkdownHost = typeof globalThis & {\n [STAGED_COMPOSER_MARKDOWN]?: Map<string, StagedComposerMarkdown>;\n};\n\nfunction stagedComposerMarkdownByThread(): Map<string, StagedComposerMarkdown> {\n const host = globalThis as StagedComposerMarkdownHost;\n return (host[STAGED_COMPOSER_MARKDOWN] ??= new Map());\n}\n\n/**\n * Stage the draft `markdown` of `text` for `threadId`'s next submission.\n *\n * statewire captures message metadata once per new user message\n * (`getMessageMetadata`, patched into `@assistant-ui/react-statewire@0.31.0`)\n * without saying which message it is for, so a composer stages its draft right\n * before it sends and the runtime's getter takes it for the submission that\n * follows ({@link withStagedComposerMarkdown}). A draft equal to its text adds\n * nothing and clears the slot. A slot nobody takes within\n * {@link STAGED_COMPOSER_MARKDOWN_TTL_MS} expires; one a submission the\n * composer did not make takes is harmless, since a composer markdown that is\n * not a draft of the message's own text is neither kept by agora nor rendered.\n */\nexport function stageComposerMarkdown({\n threadId,\n text,\n markdown,\n now = Date.now(),\n}: {\n threadId: string;\n text: string;\n markdown: string;\n /** Test seam: the time of the send. */\n now?: number;\n}): void {\n const staged = stagedComposerMarkdownByThread();\n if (markdown === text) {\n staged.delete(threadId);\n return;\n }\n staged.set(threadId, { markdown, stagedAt: now });\n}\n\n/**\n * The message metadata for `threadId`'s submission now being journaled: the\n * staged composer markdown, if one is waiting and fresh. Takes the slot.\n */\nexport function takeStagedComposerMarkdownMetadata(\n threadId: string,\n now: number = Date.now()\n): Record<string, unknown> | undefined {\n const staged = stagedComposerMarkdownByThread();\n const entry = staged.get(threadId);\n staged.delete(threadId);\n if (entry === undefined || now - entry.stagedAt > STAGED_COMPOSER_MARKDOWN_TTL_MS) {\n return undefined;\n }\n return { [DEEP_AGENT_COMPOSER_MARKDOWN_KEY]: entry.markdown };\n}\n\ntype MessageMetadataGetter = () =>\n | Record<string, unknown>\n | undefined\n | Promise<Record<string, unknown> | undefined>;\n\n/**\n * A runtime's `getMessageMetadata` that also carries the composer markdown a\n * composer staged for `threadId` ({@link stageComposerMarkdown}), on top of the\n * host's own metadata (`getMessageMetadata`, when it has one). The slot is\n * taken synchronously at submission, before the host's capture resolves, so a\n * draft never rides a later message. With nothing staged the host's own value\n * is returned as is.\n */\nexport function withStagedComposerMarkdown({\n threadId,\n getMessageMetadata,\n}: {\n threadId: string;\n getMessageMetadata?: MessageMetadataGetter | undefined;\n}): MessageMetadataGetter {\n return () => {\n const staged = takeStagedComposerMarkdownMetadata(threadId);\n const captured = getMessageMetadata?.();\n if (staged === undefined) return captured;\n const merge = (host: Record<string, unknown> | undefined): Record<string, unknown> => ({\n ...host,\n ...staged,\n });\n return captured instanceof Promise ? captured.then(merge) : merge(captured);\n };\n}\n\n/** Test seam: drop every staged composer markdown. */\nexport function resetStagedComposerMarkdown(): void {\n stagedComposerMarkdownByThread().clear();\n}\n\n/**\n * `metadata` with the composer markdown set to the draft `markdown` of `text`,\n * or without it when the draft equals the text (it adds nothing then).\n * Returns `undefined` when nothing is left.\n */\nexport function withDeepAgentComposerMarkdown({\n metadata,\n text,\n markdown,\n}: {\n metadata: Readonly<Record<string, unknown>> | undefined;\n text: string;\n markdown: string;\n}): Record<string, unknown> | undefined {\n const { [DEEP_AGENT_COMPOSER_MARKDOWN_KEY]: _previous, ...rest } = metadata ?? {};\n const next: Record<string, unknown> =\n markdown === text ? rest : { ...rest, [DEEP_AGENT_COMPOSER_MARKDOWN_KEY]: markdown };\n return Object.keys(next).length > 0 ? next : undefined;\n}\n"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"composer-markdown.js","sources":["../../../../agent-runtime/src/composer-markdown.ts"],"sourcesContent":["/**\n * The composer markdown a chat message was sent from.\n *\n * The chat composer sends a message's plain text exactly as typed, because the\n * model reads the text literally, and writes real formatting (bold, lists,\n * code, mention chips) as markdown. The sent text alone no longer says which\n * characters are formatting and which were typed: literal `**strong**` and\n * bold `strong` both send `**strong**`, and `List<T>`, `a\\\\b` or `2*3*4` read\n * as markup when the text is rendered as markdown. So the composer also\n * attaches the draft the text was serialized from (the same document with its\n * plain text escaped for markdown) under {@link DEEP_AGENT_COMPOSER_MARKDOWN_KEY}\n * in the message's metadata, and only when the two differ.\n *\n * It rides the statewire message, the queue and steer lanes and the persisted\n * HumanMessage (`additional_kwargs.metadata`, which the converter surfaces as\n * `metadata.custom`). It is display-only: the bubble renders it, and Edit,\n * branch-and-edit and a withdrawn turn load it back into the composer. The\n * model reads the message text, never this. A message without it (older\n * messages, other clients) renders its text as before.\n *\n * agora validates it with the same rule\n * (`agora/graphs/athena_deep_agent/composer_markdown.py`).\n */\nexport const DEEP_AGENT_COMPOSER_MARKDOWN_KEY = 'composer_markdown';\n\nconst DRAFT_CHARACTER_REFERENCES: Readonly<Record<string, string>> = {\n lt: '<',\n gt: '>',\n amp: '&',\n '#32': ' ',\n '#9': '\\t',\n};\n\n/**\n * A sent text in comparable form: every backslash dropped (the draft's\n * escapes, and the `\\` before a hard break's newline, are backslashes).\n */\nconst comparableText = (text: string): string => text.replace(/\\\\/g, '');\n\n/**\n * A draft in comparable form: every backslash dropped, then the character\n * references the draft serializer writes decoded in one pass (`&lt;` and\n * `&gt;` for typed `<` and `>`, `&amp;` for a typed `&` that starts a\n * reference, so typed `&lt;` is `&amp;lt;`, and `&#32;` / `&#9;` for the space\n * or tab a line starts with, which markdown would otherwise drop).\n */\nconst comparableDraft = (markdown: string): string =>\n comparableText(markdown).replace(\n /&(lt|gt|amp|#32|#9);/g,\n (reference, name: string) => DRAFT_CHARACTER_REFERENCES[name] ?? reference\n );\n\n/**\n * Escaping at most turns one character into four (`<` → `&lt;`). A line's\n * first space becomes five (`&#32;`), but only once a line, so a whole line\n * with its line break stays under four a character; the slack covers short\n * texts. agora caps a stored draft the same way.\n */\nconst MAX_EXPANSION = 4;\nconst EXPANSION_SLACK_CHARS = 1024;\n\n/**\n * Whether `markdown` is the draft of `text`: no longer than escaping could make\n * it, and equal to it once backslashes are dropped and the draft's character\n * references decoded. A draft that would show other\n * words than the text the model read (another message's, a client's mistake) is\n * not taken; such a message renders its text as before.\n */\nexport function isComposerMarkdownOfText({\n markdown,\n text,\n}: {\n markdown: string;\n text: string;\n}): boolean {\n return (\n markdown.length <= text.length * MAX_EXPANSION + EXPANSION_SLACK_CHARS &&\n comparableDraft(markdown) === comparableText(text)\n );\n}\n\n/** The references a draft writes a line's first space or tab as. */\nconst DRAFT_WHITESPACE_REFERENCES: readonly string[] = ['&#32;', '&#9;'];\n\nconst isWhitespace = (character: string): boolean => /\\s/.test(character);\n\n/** A hard break in a draft: `\\` before the newline (the text holds the newline alone). */\nconst DRAFT_HARD_BREAK = '\\\\\\n';\n\n/** Whether an odd run of backslashes ends at `end`: its last one escapes what follows, not a typed `\\` (`\\\\`). */\nfunction endsInEscapingBackslash({\n markdown,\n start,\n end,\n}: {\n markdown: string;\n start: number;\n end: number;\n}): boolean {\n let backslashes = 0;\n while (end - backslashes > start && markdown.charAt(end - backslashes - 1) === '\\\\') {\n backslashes += 1;\n }\n return backslashes % 2 === 1;\n}\n\n/**\n * `markdown`, a draft, trimmed as its text is trimmed (`text.trim()`): a\n * send that trims its text stays a draft of it. `markdown.trim()` would keep\n * the `&#32;` a draft writes for the space its first line starts with, and the\n * `\\` of a line break whose newline it trims.\n */\nexport function trimComposerDraft(markdown: string): string {\n let start = 0;\n let end = markdown.length;\n for (;;) {\n while (start < end && isWhitespace(markdown.charAt(start))) start += 1;\n const reference = [...DRAFT_WHITESPACE_REFERENCES, DRAFT_HARD_BREAK].find(\n (candidate) => start + candidate.length <= end && markdown.startsWith(candidate, start)\n );\n if (reference === undefined) break;\n start += reference.length;\n }\n for (;;) {\n while (end > start && isWhitespace(markdown.charAt(end - 1))) end -= 1;\n const reference = DRAFT_WHITESPACE_REFERENCES.find(\n (candidate) => end - candidate.length >= start && markdown.endsWith(candidate, end)\n );\n if (reference !== undefined) {\n end -= reference.length;\n } else if (\n markdown.charAt(end) === '\\n' &&\n endsInEscapingBackslash({ markdown, start, end })\n ) {\n // The newline just trimmed was a line break's: its `\\` goes with it.\n end -= 1;\n } else {\n break;\n }\n }\n return markdown.slice(start, end);\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\n/**\n * The composer markdown to render or load for a message whose metadata is\n * `metadata` (a message's `metadata.custom`, or a lane entry's `metadata`) and\n * whose text is `text`; `null` when it carries none, or one that is not a\n * draft of `text`.\n */\nexport function readDeepAgentComposerMarkdown({\n metadata,\n text,\n}: {\n metadata: unknown;\n text: string;\n}): string | null {\n if (!isRecord(metadata)) return null;\n const markdown: unknown = metadata[DEEP_AGENT_COMPOSER_MARKDOWN_KEY];\n return typeof markdown === 'string' && isComposerMarkdownOfText({ markdown, text })\n ? markdown\n : null;\n}\n\n/**\n * How long a staged composer markdown waits for its submission\n * ({@link stageComposerMarkdown}).\n */\nexport const STAGED_COMPOSER_MARKDOWN_TTL_MS = 10_000;\n\ninterface StagedComposerMarkdown {\n readonly markdown: string;\n readonly stagedAt: number;\n}\n\n/**\n * The staged drafts live on `globalThis`, not in module scope, because two\n * copies of this module run in one page: the SDK bundles agent-runtime into its\n * dist, so its composer stages through the SDK's copy, while the Chrome\n * extension and the Office add-in build their runtime on agent-runtime directly\n * and take the draft through their own (see `asset-attachment.ts`).\n */\nconst STAGED_COMPOSER_MARKDOWN = Symbol.for('@athenaintel/agent-runtime/staged-composer-markdown');\n\ntype StagedComposerMarkdownHost = typeof globalThis & {\n [STAGED_COMPOSER_MARKDOWN]?: Map<string, StagedComposerMarkdown>;\n};\n\nfunction stagedComposerMarkdownByThread(): Map<string, StagedComposerMarkdown> {\n const host = globalThis as StagedComposerMarkdownHost;\n return (host[STAGED_COMPOSER_MARKDOWN] ??= new Map());\n}\n\n/**\n * Stage the draft `markdown` of `text` for `threadId`'s next submission.\n *\n * statewire captures message metadata once per new user message\n * (`getMessageMetadata`, patched into `@assistant-ui/react-statewire@0.30.0`)\n * without saying which message it is for, so a composer stages its draft right\n * before it sends and the runtime's getter takes it for the submission that\n * follows ({@link withStagedComposerMarkdown}). A draft equal to its text adds\n * nothing and clears the slot. A slot nobody takes within\n * {@link STAGED_COMPOSER_MARKDOWN_TTL_MS} expires; one a submission the\n * composer did not make takes is harmless, since a composer markdown that is\n * not a draft of the message's own text is neither kept by agora nor rendered.\n */\nexport function stageComposerMarkdown({\n threadId,\n text,\n markdown,\n now = Date.now(),\n}: {\n threadId: string;\n text: string;\n markdown: string;\n /** Test seam: the time of the send. */\n now?: number;\n}): void {\n const staged = stagedComposerMarkdownByThread();\n if (markdown === text) {\n staged.delete(threadId);\n return;\n }\n staged.set(threadId, { markdown, stagedAt: now });\n}\n\n/**\n * The message metadata for `threadId`'s submission now being journaled: the\n * staged composer markdown, if one is waiting and fresh. Takes the slot.\n */\nexport function takeStagedComposerMarkdownMetadata(\n threadId: string,\n now: number = Date.now()\n): Record<string, unknown> | undefined {\n const staged = stagedComposerMarkdownByThread();\n const entry = staged.get(threadId);\n staged.delete(threadId);\n if (entry === undefined || now - entry.stagedAt > STAGED_COMPOSER_MARKDOWN_TTL_MS) {\n return undefined;\n }\n return { [DEEP_AGENT_COMPOSER_MARKDOWN_KEY]: entry.markdown };\n}\n\ntype MessageMetadataGetter = () =>\n | Record<string, unknown>\n | undefined\n | Promise<Record<string, unknown> | undefined>;\n\n/**\n * A runtime's `getMessageMetadata` that also carries the composer markdown a\n * composer staged for `threadId` ({@link stageComposerMarkdown}), on top of the\n * host's own metadata (`getMessageMetadata`, when it has one). The slot is\n * taken synchronously at submission, before the host's capture resolves, so a\n * draft never rides a later message. With nothing staged the host's own value\n * is returned as is.\n */\nexport function withStagedComposerMarkdown({\n threadId,\n getMessageMetadata,\n}: {\n threadId: string;\n getMessageMetadata?: MessageMetadataGetter | undefined;\n}): MessageMetadataGetter {\n return () => {\n const staged = takeStagedComposerMarkdownMetadata(threadId);\n const captured = getMessageMetadata?.();\n if (staged === undefined) return captured;\n const merge = (host: Record<string, unknown> | undefined): Record<string, unknown> => ({\n ...host,\n ...staged,\n });\n return captured instanceof Promise ? captured.then(merge) : merge(captured);\n };\n}\n\n/** Test seam: drop every staged composer markdown. */\nexport function resetStagedComposerMarkdown(): void {\n stagedComposerMarkdownByThread().clear();\n}\n\n/**\n * `metadata` with the composer markdown set to the draft `markdown` of `text`,\n * or without it when the draft equals the text (it adds nothing then).\n * Returns `undefined` when nothing is left.\n */\nexport function withDeepAgentComposerMarkdown({\n metadata,\n text,\n markdown,\n}: {\n metadata: Readonly<Record<string, unknown>> | undefined;\n text: string;\n markdown: string;\n}): Record<string, unknown> | undefined {\n const { [DEEP_AGENT_COMPOSER_MARKDOWN_KEY]: _previous, ...rest } = metadata ?? {};\n const next: Record<string, unknown> =\n markdown === text ? rest : { ...rest, [DEEP_AGENT_COMPOSER_MARKDOWN_KEY]: markdown };\n return Object.keys(next).length > 0 ? next : undefined;\n}\n"],"names":[],"mappings":"AAuBO,MAAM,mCAAmC;AAEhD,MAAM,6BAA+D;AAAA,EACnE,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,KAAK;AAAA,EACL,OAAO;AAAA,EACP,MAAM;AACR;AAMA,MAAM,iBAAiB,CAAC,SAAyB,KAAK,QAAQ,OAAO,EAAE;AASvE,MAAM,kBAAkB,CAAC,aACvB,eAAe,QAAQ,EAAE;AAAA,EACvB;AAAA,EACA,CAAC,WAAW,SAAiB,2BAA2B,IAAI,KAAK;AACnE;AAQF,MAAM,gBAAgB;AACtB,MAAM,wBAAwB;AASvB,SAAS,yBAAyB;AAAA,EACvC;AAAA,EACA;AACF,GAGY;AACV,SACE,SAAS,UAAU,KAAK,SAAS,gBAAgB,yBACjD,gBAAgB,QAAQ,MAAM,eAAe,IAAI;AAErD;AAGA,MAAM,8BAAiD,CAAC,SAAS,MAAM;AAEvE,MAAM,eAAe,CAAC,cAA+B,KAAK,KAAK,SAAS;AAGxE,MAAM,mBAAmB;AAGzB,SAAS,wBAAwB;AAAA,EAC/B;AAAA,EACA;AAAA,EACA;AACF,GAIY;AACV,MAAI,cAAc;AAClB,SAAO,MAAM,cAAc,SAAS,SAAS,OAAO,MAAM,cAAc,CAAC,MAAM,MAAM;AACnF,mBAAe;AAAA,EACjB;AACA,SAAO,cAAc,MAAM;AAC7B;AAQO,SAAS,kBAAkB,UAA0B;AAC1D,MAAI,QAAQ;AACZ,MAAI,MAAM,SAAS;AACnB,aAAS;AACP,WAAO,QAAQ,OAAO,aAAa,SAAS,OAAO,KAAK,CAAC,EAAG,UAAS;AACrE,UAAM,YAAY,CAAC,GAAG,6BAA6B,gBAAgB,EAAE;AAAA,MACnE,CAAC,cAAc,QAAQ,UAAU,UAAU,OAAO,SAAS,WAAW,WAAW,KAAK;AAAA,IAAA;AAExF,QAAI,cAAc,OAAW;AAC7B,aAAS,UAAU;AAAA,EACrB;AACA,aAAS;AACP,WAAO,MAAM,SAAS,aAAa,SAAS,OAAO,MAAM,CAAC,CAAC,EAAG,QAAO;AACrE,UAAM,YAAY,4BAA4B;AAAA,MAC5C,CAAC,cAAc,MAAM,UAAU,UAAU,SAAS,SAAS,SAAS,WAAW,GAAG;AAAA,IAAA;AAEpF,QAAI,cAAc,QAAW;AAC3B,aAAO,UAAU;AAAA,IACnB,WACE,SAAS,OAAO,GAAG,MAAM,QACzB,wBAAwB,EAAE,UAAU,OAAO,IAAA,CAAK,GAChD;AAEA,aAAO;AAAA,IACT,OAAO;AACL;AAAA,IACF;AAAA,EACF;AACA,SAAO,SAAS,MAAM,OAAO,GAAG;AAClC;AAEA,MAAM,WAAW,CAAC,UAChB,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAQ9D,SAAS,8BAA8B;AAAA,EAC5C;AAAA,EACA;AACF,GAGkB;AAChB,MAAI,CAAC,SAAS,QAAQ,EAAG,QAAO;AAChC,QAAM,WAAoB,SAAS,gCAAgC;AACnE,SAAO,OAAO,aAAa,YAAY,yBAAyB,EAAE,UAAU,KAAA,CAAM,IAC9E,WACA;AACN;AAMO,MAAM,kCAAkC;AAc/C,MAAM,2BAA2B,OAAO,IAAI,qDAAqD;AAMjG,SAAS,iCAAsE;AAC7E,QAAM,OAAO;AACb,SAAQ,oEAAmC,oBAAI,IAAA;AACjD;AAeO,SAAS,sBAAsB;AAAA,EACpC;AAAA,EACA;AAAA,EACA;AAAA,EACA,MAAM,KAAK,IAAA;AACb,GAMS;AACP,QAAM,SAAS,+BAAA;AACf,MAAI,aAAa,MAAM;AACrB,WAAO,OAAO,QAAQ;AACtB;AAAA,EACF;AACA,SAAO,IAAI,UAAU,EAAE,UAAU,UAAU,KAAK;AAClD;AAMO,SAAS,mCACd,UACA,MAAc,KAAK,OACkB;AACrC,QAAM,SAAS,+BAAA;AACf,QAAM,QAAQ,OAAO,IAAI,QAAQ;AACjC,SAAO,OAAO,QAAQ;AACtB,MAAI,UAAU,UAAa,MAAM,MAAM,WAAW,iCAAiC;AACjF,WAAO;AAAA,EACT;AACA,SAAO,EAAE,CAAC,gCAAgC,GAAG,MAAM,SAAA;AACrD;AAeO,SAAS,2BAA2B;AAAA,EACzC;AAAA,EACA;AACF,GAG0B;AACxB,SAAO,MAAM;AACX,UAAM,SAAS,mCAAmC,QAAQ;AAC1D,UAAM,WAAW;AACjB,QAAI,WAAW,OAAW,QAAO;AACjC,UAAM,QAAQ,CAAC,UAAwE;AAAA,MACrF,GAAG;AAAA,MACH,GAAG;AAAA,IAAA;AAEL,WAAO,oBAAoB,UAAU,SAAS,KAAK,KAAK,IAAI,MAAM,QAAQ;AAAA,EAC5E;AACF;"}
1
+ {"version":3,"file":"composer-markdown.js","sources":["../../../../agent-runtime/src/composer-markdown.ts"],"sourcesContent":["/**\n * The composer markdown a chat message was sent from.\n *\n * The chat composer sends a message's plain text exactly as typed, because the\n * model reads the text literally, and writes real formatting (bold, lists,\n * code, mention chips) as markdown. The sent text alone no longer says which\n * characters are formatting and which were typed: literal `**strong**` and\n * bold `strong` both send `**strong**`, and `List<T>`, `a\\\\b` or `2*3*4` read\n * as markup when the text is rendered as markdown. So the composer also\n * attaches the draft the text was serialized from (the same document with its\n * plain text escaped for markdown) under {@link DEEP_AGENT_COMPOSER_MARKDOWN_KEY}\n * in the message's metadata, and only when the two differ.\n *\n * It rides the statewire message, the queue and steer lanes and the persisted\n * HumanMessage (`additional_kwargs.metadata`, which the converter surfaces as\n * `metadata.custom`). It is display-only: the bubble renders it, and Edit,\n * branch-and-edit and a withdrawn turn load it back into the composer. The\n * model reads the message text, never this. A message without it (older\n * messages, other clients) renders its text as before.\n *\n * agora validates it with the same rule\n * (`agora/graphs/athena_deep_agent/composer_markdown.py`).\n */\nexport const DEEP_AGENT_COMPOSER_MARKDOWN_KEY = 'composer_markdown';\n\nconst DRAFT_CHARACTER_REFERENCES: Readonly<Record<string, string>> = {\n lt: '<',\n gt: '>',\n amp: '&',\n '#32': ' ',\n '#9': '\\t',\n};\n\n/**\n * A sent text in comparable form: every backslash dropped (the draft's\n * escapes, and the `\\` before a hard break's newline, are backslashes).\n */\nconst comparableText = (text: string): string => text.replace(/\\\\/g, '');\n\n/**\n * A draft in comparable form: every backslash dropped, then the character\n * references the draft serializer writes decoded in one pass (`&lt;` and\n * `&gt;` for typed `<` and `>`, `&amp;` for a typed `&` that starts a\n * reference, so typed `&lt;` is `&amp;lt;`, and `&#32;` / `&#9;` for the space\n * or tab a line starts with, which markdown would otherwise drop).\n */\nconst comparableDraft = (markdown: string): string =>\n comparableText(markdown).replace(\n /&(lt|gt|amp|#32|#9);/g,\n (reference, name: string) => DRAFT_CHARACTER_REFERENCES[name] ?? reference\n );\n\n/**\n * Escaping at most turns one character into four (`<` → `&lt;`). A line's\n * first space becomes five (`&#32;`), but only once a line, so a whole line\n * with its line break stays under four a character; the slack covers short\n * texts. agora caps a stored draft the same way.\n */\nconst MAX_EXPANSION = 4;\nconst EXPANSION_SLACK_CHARS = 1024;\n\n/**\n * Whether `markdown` is the draft of `text`: no longer than escaping could make\n * it, and equal to it once backslashes are dropped and the draft's character\n * references decoded. A draft that would show other\n * words than the text the model read (another message's, a client's mistake) is\n * not taken; such a message renders its text as before.\n */\nexport function isComposerMarkdownOfText({\n markdown,\n text,\n}: {\n markdown: string;\n text: string;\n}): boolean {\n return (\n markdown.length <= text.length * MAX_EXPANSION + EXPANSION_SLACK_CHARS &&\n comparableDraft(markdown) === comparableText(text)\n );\n}\n\n/** The references a draft writes a line's first space or tab as. */\nconst DRAFT_WHITESPACE_REFERENCES: readonly string[] = ['&#32;', '&#9;'];\n\nconst isWhitespace = (character: string): boolean => /\\s/.test(character);\n\n/** A hard break in a draft: `\\` before the newline (the text holds the newline alone). */\nconst DRAFT_HARD_BREAK = '\\\\\\n';\n\n/** Whether an odd run of backslashes ends at `end`: its last one escapes what follows, not a typed `\\` (`\\\\`). */\nfunction endsInEscapingBackslash({\n markdown,\n start,\n end,\n}: {\n markdown: string;\n start: number;\n end: number;\n}): boolean {\n let backslashes = 0;\n while (end - backslashes > start && markdown.charAt(end - backslashes - 1) === '\\\\') {\n backslashes += 1;\n }\n return backslashes % 2 === 1;\n}\n\n/**\n * `markdown`, a draft, trimmed as its text is trimmed (`text.trim()`): a\n * send that trims its text stays a draft of it. `markdown.trim()` would keep\n * the `&#32;` a draft writes for the space its first line starts with, and the\n * `\\` of a line break whose newline it trims.\n */\nexport function trimComposerDraft(markdown: string): string {\n let start = 0;\n let end = markdown.length;\n for (;;) {\n while (start < end && isWhitespace(markdown.charAt(start))) start += 1;\n const reference = [...DRAFT_WHITESPACE_REFERENCES, DRAFT_HARD_BREAK].find(\n (candidate) => start + candidate.length <= end && markdown.startsWith(candidate, start)\n );\n if (reference === undefined) break;\n start += reference.length;\n }\n for (;;) {\n while (end > start && isWhitespace(markdown.charAt(end - 1))) end -= 1;\n const reference = DRAFT_WHITESPACE_REFERENCES.find(\n (candidate) => end - candidate.length >= start && markdown.endsWith(candidate, end)\n );\n if (reference !== undefined) {\n end -= reference.length;\n } else if (\n markdown.charAt(end) === '\\n' &&\n endsInEscapingBackslash({ markdown, start, end })\n ) {\n // The newline just trimmed was a line break's: its `\\` goes with it.\n end -= 1;\n } else {\n break;\n }\n }\n return markdown.slice(start, end);\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\n/**\n * The composer markdown to render or load for a message whose metadata is\n * `metadata` (a message's `metadata.custom`, or a lane entry's `metadata`) and\n * whose text is `text`; `null` when it carries none, or one that is not a\n * draft of `text`.\n */\nexport function readDeepAgentComposerMarkdown({\n metadata,\n text,\n}: {\n metadata: unknown;\n text: string;\n}): string | null {\n if (!isRecord(metadata)) return null;\n const markdown: unknown = metadata[DEEP_AGENT_COMPOSER_MARKDOWN_KEY];\n return typeof markdown === 'string' && isComposerMarkdownOfText({ markdown, text })\n ? markdown\n : null;\n}\n\n/**\n * How long a staged composer markdown waits for its submission\n * ({@link stageComposerMarkdown}).\n */\nexport const STAGED_COMPOSER_MARKDOWN_TTL_MS = 10_000;\n\ninterface StagedComposerMarkdown {\n readonly markdown: string;\n readonly stagedAt: number;\n}\n\n/**\n * The staged drafts live on `globalThis`, not in module scope, because two\n * copies of this module run in one page: the SDK bundles agent-runtime into its\n * dist, so its composer stages through the SDK's copy, while the Chrome\n * extension and the Office add-in build their runtime on agent-runtime directly\n * and take the draft through their own (see `asset-attachment.ts`).\n */\nconst STAGED_COMPOSER_MARKDOWN = Symbol.for('@athenaintel/agent-runtime/staged-composer-markdown');\n\ntype StagedComposerMarkdownHost = typeof globalThis & {\n [STAGED_COMPOSER_MARKDOWN]?: Map<string, StagedComposerMarkdown>;\n};\n\nfunction stagedComposerMarkdownByThread(): Map<string, StagedComposerMarkdown> {\n const host = globalThis as StagedComposerMarkdownHost;\n return (host[STAGED_COMPOSER_MARKDOWN] ??= new Map());\n}\n\n/**\n * Stage the draft `markdown` of `text` for `threadId`'s next submission.\n *\n * statewire captures message metadata once per new user message\n * (`getMessageMetadata`, patched into `@assistant-ui/react-statewire@0.31.0`)\n * without saying which message it is for, so a composer stages its draft right\n * before it sends and the runtime's getter takes it for the submission that\n * follows ({@link withStagedComposerMarkdown}). A draft equal to its text adds\n * nothing and clears the slot. A slot nobody takes within\n * {@link STAGED_COMPOSER_MARKDOWN_TTL_MS} expires; one a submission the\n * composer did not make takes is harmless, since a composer markdown that is\n * not a draft of the message's own text is neither kept by agora nor rendered.\n */\nexport function stageComposerMarkdown({\n threadId,\n text,\n markdown,\n now = Date.now(),\n}: {\n threadId: string;\n text: string;\n markdown: string;\n /** Test seam: the time of the send. */\n now?: number;\n}): void {\n const staged = stagedComposerMarkdownByThread();\n if (markdown === text) {\n staged.delete(threadId);\n return;\n }\n staged.set(threadId, { markdown, stagedAt: now });\n}\n\n/**\n * The message metadata for `threadId`'s submission now being journaled: the\n * staged composer markdown, if one is waiting and fresh. Takes the slot.\n */\nexport function takeStagedComposerMarkdownMetadata(\n threadId: string,\n now: number = Date.now()\n): Record<string, unknown> | undefined {\n const staged = stagedComposerMarkdownByThread();\n const entry = staged.get(threadId);\n staged.delete(threadId);\n if (entry === undefined || now - entry.stagedAt > STAGED_COMPOSER_MARKDOWN_TTL_MS) {\n return undefined;\n }\n return { [DEEP_AGENT_COMPOSER_MARKDOWN_KEY]: entry.markdown };\n}\n\ntype MessageMetadataGetter = () =>\n | Record<string, unknown>\n | undefined\n | Promise<Record<string, unknown> | undefined>;\n\n/**\n * A runtime's `getMessageMetadata` that also carries the composer markdown a\n * composer staged for `threadId` ({@link stageComposerMarkdown}), on top of the\n * host's own metadata (`getMessageMetadata`, when it has one). The slot is\n * taken synchronously at submission, before the host's capture resolves, so a\n * draft never rides a later message. With nothing staged the host's own value\n * is returned as is.\n */\nexport function withStagedComposerMarkdown({\n threadId,\n getMessageMetadata,\n}: {\n threadId: string;\n getMessageMetadata?: MessageMetadataGetter | undefined;\n}): MessageMetadataGetter {\n return () => {\n const staged = takeStagedComposerMarkdownMetadata(threadId);\n const captured = getMessageMetadata?.();\n if (staged === undefined) return captured;\n const merge = (host: Record<string, unknown> | undefined): Record<string, unknown> => ({\n ...host,\n ...staged,\n });\n return captured instanceof Promise ? captured.then(merge) : merge(captured);\n };\n}\n\n/** Test seam: drop every staged composer markdown. */\nexport function resetStagedComposerMarkdown(): void {\n stagedComposerMarkdownByThread().clear();\n}\n\n/**\n * `metadata` with the composer markdown set to the draft `markdown` of `text`,\n * or without it when the draft equals the text (it adds nothing then).\n * Returns `undefined` when nothing is left.\n */\nexport function withDeepAgentComposerMarkdown({\n metadata,\n text,\n markdown,\n}: {\n metadata: Readonly<Record<string, unknown>> | undefined;\n text: string;\n markdown: string;\n}): Record<string, unknown> | undefined {\n const { [DEEP_AGENT_COMPOSER_MARKDOWN_KEY]: _previous, ...rest } = metadata ?? {};\n const next: Record<string, unknown> =\n markdown === text ? rest : { ...rest, [DEEP_AGENT_COMPOSER_MARKDOWN_KEY]: markdown };\n return Object.keys(next).length > 0 ? next : undefined;\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const ENABLED_TOOLS = "enabled_tools";
4
+ const EXCLUDED_TOOLS = "excluded_tools";
5
+ const DECLINED_TOOLKITS = "declined_toolkit_ids";
6
+ const PANEL_CONFIG = "panel_config";
7
+ const stringList = (value) => Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
8
+ const uniqueIds = (ids) => {
9
+ const seen = /* @__PURE__ */ new Set();
10
+ const out = [];
11
+ for (const id of ids) {
12
+ if (!id || seen.has(id)) continue;
13
+ seen.add(id);
14
+ out.push(id);
15
+ }
16
+ return out;
17
+ };
18
+ function applyConfigPatch(custom, patch) {
19
+ const next = { ...custom };
20
+ for (const [key, value] of Object.entries(patch)) {
21
+ if (key === PANEL_CONFIG) continue;
22
+ if (value === null || value === void 0) delete next[key];
23
+ else next[key] = value;
24
+ }
25
+ return next;
26
+ }
27
+ function applyToolsEnable(custom, toolIds) {
28
+ const ids = uniqueIds(toolIds);
29
+ if (ids.length === 0) return { ...custom };
30
+ const next = { ...custom };
31
+ const adding = new Set(ids);
32
+ next[ENABLED_TOOLS] = uniqueIds([...stringList(custom[ENABLED_TOOLS]), ...ids]);
33
+ for (const key of [EXCLUDED_TOOLS, DECLINED_TOOLKITS]) {
34
+ if (!(key in custom)) continue;
35
+ const kept = stringList(custom[key]).filter((id) => !adding.has(id));
36
+ if (kept.length > 0) next[key] = kept;
37
+ else delete next[key];
38
+ }
39
+ return next;
40
+ }
41
+ function applyToolsDisable(custom, toolIds) {
42
+ const ids = uniqueIds(toolIds);
43
+ if (ids.length === 0) return { ...custom };
44
+ const next = { ...custom };
45
+ const removing = new Set(ids);
46
+ const explicit = Array.isArray(custom[ENABLED_TOOLS]);
47
+ const enabled = stringList(custom[ENABLED_TOOLS]);
48
+ const present = new Set(enabled);
49
+ if (explicit) next[ENABLED_TOOLS] = enabled.filter((id) => !removing.has(id));
50
+ const declined = ids.filter((id) => !present.has(id));
51
+ if (declined.length > 0) {
52
+ next[DECLINED_TOOLKITS] = uniqueIds([...stringList(custom[DECLINED_TOOLKITS]), ...declined]);
53
+ }
54
+ return next;
55
+ }
56
+ exports.applyConfigPatch = applyConfigPatch;
57
+ exports.applyToolsDisable = applyToolsDisable;
58
+ exports.applyToolsEnable = applyToolsEnable;
59
+ //# sourceMappingURL=config-mutations.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config-mutations.cjs","sources":["../../../../agent-runtime/src/config-mutations.ts"],"sourcesContent":["import {\n RUN_CONFIG_PER_SEND_CUSTOM_KEY_SET,\n THREAD_CONFIG_CUSTOM_KEY_SET,\n} from './generated/runconfig-contract';\n\n/**\n * Pure transforms behind the `config/*` command family — the client-side\n * mirror of `agora/agora/graphs/athena_deep_agent/thread_config.py`\n * (`apply_config_patch` / `apply_tools_enable` / `apply_tools_disable`).\n *\n * The hook folds these over `state.config.custom` while a command is in\n * flight so the UI reflects the write immediately; the server applies the same\n * transform under the config row's lock, so the optimistic and the durable\n * result agree whenever no other device wrote in between — and compose\n * (rather than conflict) when one did.\n */\n\nexport type ThreadConfigCustom = Record<string, unknown>;\n\n/** A `config/patch` payload: `null` unsets the key. */\nexport type ThreadConfigPatch = Record<string, unknown | null>;\n\nconst ENABLED_TOOLS = 'enabled_tools';\nconst EXCLUDED_TOOLS = 'excluded_tools';\nconst DECLINED_TOOLKITS = 'declined_toolkit_ids';\nconst PANEL_CONFIG = 'panel_config';\n\nconst stringList = (value: unknown): string[] =>\n Array.isArray(value) ? value.filter((item): item is string => typeof item === 'string') : [];\n\nconst uniqueIds = (ids: Iterable<string>): string[] => {\n const seen = new Set<string>();\n const out: string[] = [];\n for (const id of ids) {\n if (!id || seen.has(id)) continue;\n seen.add(id);\n out.push(id);\n }\n return out;\n};\n\n/** Set / unset top-level keys. `panel_config` is never part of the server config. */\nexport function applyConfigPatch(\n custom: ThreadConfigCustom,\n patch: ThreadConfigPatch\n): ThreadConfigCustom {\n const next: ThreadConfigCustom = { ...custom };\n for (const [key, value] of Object.entries(patch)) {\n if (key === PANEL_CONFIG) continue;\n if (value === null || value === undefined) delete next[key];\n else next[key] = value;\n }\n return next;\n}\n\n/**\n * Append ids to `enabled_tools` (deduplicated, order preserved) and lift\n * them out of `excluded_tools` / `declined_toolkit_ids`.\n */\nexport function applyToolsEnable(\n custom: ThreadConfigCustom,\n toolIds: readonly string[]\n): ThreadConfigCustom {\n const ids = uniqueIds(toolIds);\n if (ids.length === 0) return { ...custom };\n const next: ThreadConfigCustom = { ...custom };\n const adding = new Set(ids);\n next[ENABLED_TOOLS] = uniqueIds([...stringList(custom[ENABLED_TOOLS]), ...ids]);\n for (const key of [EXCLUDED_TOOLS, DECLINED_TOOLKITS]) {\n if (!(key in custom)) continue;\n const kept = stringList(custom[key]).filter((id) => !adding.has(id));\n if (kept.length > 0) next[key] = kept;\n else delete next[key];\n }\n return next;\n}\n\n/**\n * Remove ids from an explicit `enabled_tools` list; an id that is not in the\n * list (or a thread without one — running its collab agent's compiled\n * toolkits) is recorded in `declined_toolkit_ids` so the opt-out sticks.\n */\nexport function applyToolsDisable(\n custom: ThreadConfigCustom,\n toolIds: readonly string[]\n): ThreadConfigCustom {\n const ids = uniqueIds(toolIds);\n if (ids.length === 0) return { ...custom };\n const next: ThreadConfigCustom = { ...custom };\n const removing = new Set(ids);\n const explicit = Array.isArray(custom[ENABLED_TOOLS]);\n const enabled = stringList(custom[ENABLED_TOOLS]);\n const present = new Set(enabled);\n if (explicit) next[ENABLED_TOOLS] = enabled.filter((id) => !removing.has(id));\n const declined = ids.filter((id) => !present.has(id));\n if (declined.length > 0) {\n next[DECLINED_TOOLKITS] = uniqueIds([...stringList(custom[DECLINED_TOOLKITS]), ...declined]);\n }\n return next;\n}\n\n/** A thread's currently enabled tool ids — `null` when the thread has no explicit list (defers to its agent). */\nexport function readEnabledTools(custom: ThreadConfigCustom | null | undefined): string[] | null {\n const value = custom?.[ENABLED_TOOLS];\n return Array.isArray(value) ? stringList(value) : null;\n}\n\n/** The `declined_toolkit_ids` advisory list (empty when absent). */\nexport function readDeclinedToolkitIds(custom: ThreadConfigCustom | null | undefined): string[] {\n return stringList(custom?.[DECLINED_TOOLKITS]);\n}\n\n/**\n * The subset of a `runConfig.custom` payload a client is allowed to carry per\n * send on a server-configured thread (`RUN_CONFIG_PER_SEND_CUSTOM_KEYS`).\n * Every other key is owned by the server config lane and would be dropped at\n * dispatch, so sending it only invites drift between what a client thinks it\n * asked for and what ran.\n */\nexport function pickPerSendCustom(custom: ThreadConfigCustom): ThreadConfigCustom {\n const out: ThreadConfigCustom = {};\n for (const [key, value] of Object.entries(custom)) {\n if (value === undefined) continue;\n if (RUN_CONFIG_PER_SEND_CUSTOM_KEY_SET.has(key)) out[key] = value;\n }\n return out;\n}\n\n/**\n * The subset of a `runConfig.custom` payload the thread's server config lane\n * owns (`THREAD_CONFIG_CUSTOM_KEYS`) — what a client seeds a fresh thread with\n * through `config/patch` before its first send.\n */\nexport function pickThreadConfigCustom(custom: ThreadConfigCustom): ThreadConfigCustom {\n const out: ThreadConfigCustom = {};\n for (const [key, value] of Object.entries(custom)) {\n if (value === undefined) continue;\n if (THREAD_CONFIG_CUSTOM_KEY_SET.has(key)) out[key] = value;\n }\n return out;\n}\n\n/**\n * The `config/patch` that turns `current` into `desired` over the thread-config\n * keys: changed keys carry their new value, keys absent from `desired` carry\n * `null` (unset). `null` when nothing differs.\n */\nexport function diffThreadConfig(\n current: ThreadConfigCustom,\n desired: ThreadConfigCustom\n): ThreadConfigPatch | null {\n const patch: ThreadConfigPatch = {};\n const next = pickThreadConfigCustom(desired);\n const prev = pickThreadConfigCustom(current);\n for (const [key, value] of Object.entries(next)) {\n if (!(key in prev) || !deepEqual(prev[key], value)) patch[key] = value;\n }\n for (const key of Object.keys(prev)) {\n if (!(key in next)) patch[key] = null;\n }\n return Object.keys(patch).length > 0 ? patch : null;\n}\n\nfunction deepEqual(a: unknown, b: unknown): boolean {\n if (Object.is(a, b)) return true;\n if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null) return false;\n if (Array.isArray(a) !== Array.isArray(b)) return false;\n if (Array.isArray(a) && Array.isArray(b)) {\n return a.length === b.length && a.every((item, index) => deepEqual(item, b[index]));\n }\n const ra = a as Record<string, unknown>;\n const rb = b as Record<string, unknown>;\n const ka = Object.keys(ra);\n const kb = Object.keys(rb);\n return ka.length === kb.length && ka.every((key) => key in rb && deepEqual(ra[key], rb[key]));\n}\n"],"names":[],"mappings":";;AAsBA,MAAM,gBAAgB;AACtB,MAAM,iBAAiB;AACvB,MAAM,oBAAoB;AAC1B,MAAM,eAAe;AAErB,MAAM,aAAa,CAAC,UAClB,MAAM,QAAQ,KAAK,IAAI,MAAM,OAAO,CAAC,SAAyB,OAAO,SAAS,QAAQ,IAAI,CAAA;AAE5F,MAAM,YAAY,CAAC,QAAoC;AACrD,QAAM,2BAAW,IAAA;AACjB,QAAM,MAAgB,CAAA;AACtB,aAAW,MAAM,KAAK;AACpB,QAAI,CAAC,MAAM,KAAK,IAAI,EAAE,EAAG;AACzB,SAAK,IAAI,EAAE;AACX,QAAI,KAAK,EAAE;AAAA,EACb;AACA,SAAO;AACT;AAGO,SAAS,iBACd,QACA,OACoB;AACpB,QAAM,OAA2B,EAAE,GAAG,OAAA;AACtC,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,KAAK,GAAG;AAChD,QAAI,QAAQ,aAAc;AAC1B,QAAI,UAAU,QAAQ,UAAU,OAAW,QAAO,KAAK,GAAG;AAAA,QACrD,MAAK,GAAG,IAAI;AAAA,EACnB;AACA,SAAO;AACT;AAMO,SAAS,iBACd,QACA,SACoB;AACpB,QAAM,MAAM,UAAU,OAAO;AAC7B,MAAI,IAAI,WAAW,EAAG,QAAO,EAAE,GAAG,OAAA;AAClC,QAAM,OAA2B,EAAE,GAAG,OAAA;AACtC,QAAM,SAAS,IAAI,IAAI,GAAG;AAC1B,OAAK,aAAa,IAAI,UAAU,CAAC,GAAG,WAAW,OAAO,aAAa,CAAC,GAAG,GAAG,GAAG,CAAC;AAC9E,aAAW,OAAO,CAAC,gBAAgB,iBAAiB,GAAG;AACrD,QAAI,EAAE,OAAO,QAAS;AACtB,UAAM,OAAO,WAAW,OAAO,GAAG,CAAC,EAAE,OAAO,CAAC,OAAO,CAAC,OAAO,IAAI,EAAE,CAAC;AACnE,QAAI,KAAK,SAAS,EAAG,MAAK,GAAG,IAAI;AAAA,QAC5B,QAAO,KAAK,GAAG;AAAA,EACtB;AACA,SAAO;AACT;AAOO,SAAS,kBACd,QACA,SACoB;AACpB,QAAM,MAAM,UAAU,OAAO;AAC7B,MAAI,IAAI,WAAW,EAAG,QAAO,EAAE,GAAG,OAAA;AAClC,QAAM,OAA2B,EAAE,GAAG,OAAA;AACtC,QAAM,WAAW,IAAI,IAAI,GAAG;AAC5B,QAAM,WAAW,MAAM,QAAQ,OAAO,aAAa,CAAC;AACpD,QAAM,UAAU,WAAW,OAAO,aAAa,CAAC;AAChD,QAAM,UAAU,IAAI,IAAI,OAAO;AAC/B,MAAI,SAAU,MAAK,aAAa,IAAI,QAAQ,OAAO,CAAC,OAAO,CAAC,SAAS,IAAI,EAAE,CAAC;AAC5E,QAAM,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC,QAAQ,IAAI,EAAE,CAAC;AACpD,MAAI,SAAS,SAAS,GAAG;AACvB,SAAK,iBAAiB,IAAI,UAAU,CAAC,GAAG,WAAW,OAAO,iBAAiB,CAAC,GAAG,GAAG,QAAQ,CAAC;AAAA,EAC7F;AACA,SAAO;AACT;;;;"}
@@ -0,0 +1,59 @@
1
+ const ENABLED_TOOLS = "enabled_tools";
2
+ const EXCLUDED_TOOLS = "excluded_tools";
3
+ const DECLINED_TOOLKITS = "declined_toolkit_ids";
4
+ const PANEL_CONFIG = "panel_config";
5
+ const stringList = (value) => Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
6
+ const uniqueIds = (ids) => {
7
+ const seen = /* @__PURE__ */ new Set();
8
+ const out = [];
9
+ for (const id of ids) {
10
+ if (!id || seen.has(id)) continue;
11
+ seen.add(id);
12
+ out.push(id);
13
+ }
14
+ return out;
15
+ };
16
+ function applyConfigPatch(custom, patch) {
17
+ const next = { ...custom };
18
+ for (const [key, value] of Object.entries(patch)) {
19
+ if (key === PANEL_CONFIG) continue;
20
+ if (value === null || value === void 0) delete next[key];
21
+ else next[key] = value;
22
+ }
23
+ return next;
24
+ }
25
+ function applyToolsEnable(custom, toolIds) {
26
+ const ids = uniqueIds(toolIds);
27
+ if (ids.length === 0) return { ...custom };
28
+ const next = { ...custom };
29
+ const adding = new Set(ids);
30
+ next[ENABLED_TOOLS] = uniqueIds([...stringList(custom[ENABLED_TOOLS]), ...ids]);
31
+ for (const key of [EXCLUDED_TOOLS, DECLINED_TOOLKITS]) {
32
+ if (!(key in custom)) continue;
33
+ const kept = stringList(custom[key]).filter((id) => !adding.has(id));
34
+ if (kept.length > 0) next[key] = kept;
35
+ else delete next[key];
36
+ }
37
+ return next;
38
+ }
39
+ function applyToolsDisable(custom, toolIds) {
40
+ const ids = uniqueIds(toolIds);
41
+ if (ids.length === 0) return { ...custom };
42
+ const next = { ...custom };
43
+ const removing = new Set(ids);
44
+ const explicit = Array.isArray(custom[ENABLED_TOOLS]);
45
+ const enabled = stringList(custom[ENABLED_TOOLS]);
46
+ const present = new Set(enabled);
47
+ if (explicit) next[ENABLED_TOOLS] = enabled.filter((id) => !removing.has(id));
48
+ const declined = ids.filter((id) => !present.has(id));
49
+ if (declined.length > 0) {
50
+ next[DECLINED_TOOLKITS] = uniqueIds([...stringList(custom[DECLINED_TOOLKITS]), ...declined]);
51
+ }
52
+ return next;
53
+ }
54
+ export {
55
+ applyConfigPatch,
56
+ applyToolsDisable,
57
+ applyToolsEnable
58
+ };
59
+ //# sourceMappingURL=config-mutations.js.map