@athenaintel/react 0.35.0 → 0.36.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +53 -0
  2. package/dist/agent-runtime/src/generated/runconfig-contract.cjs +4 -0
  3. package/dist/agent-runtime/src/generated/runconfig-contract.cjs.map +1 -1
  4. package/dist/agent-runtime/src/generated/runconfig-contract.js +4 -0
  5. package/dist/agent-runtime/src/generated/runconfig-contract.js.map +1 -1
  6. package/dist/agent-runtime/src/tool-metadata.cjs +11 -0
  7. package/dist/agent-runtime/src/tool-metadata.cjs.map +1 -1
  8. package/dist/agent-runtime/src/tool-metadata.js +11 -0
  9. package/dist/agent-runtime/src/tool-metadata.js.map +1 -1
  10. package/dist/agent-runtime/src/use-deep-agent-window-sync.cjs +24 -1
  11. package/dist/agent-runtime/src/use-deep-agent-window-sync.cjs.map +1 -1
  12. package/dist/agent-runtime/src/use-deep-agent-window-sync.js +24 -1
  13. package/dist/agent-runtime/src/use-deep-agent-window-sync.js.map +1 -1
  14. package/dist/catalog/agent-mode-menu.cjs +115 -0
  15. package/dist/catalog/agent-mode-menu.cjs.map +1 -0
  16. package/dist/catalog/agent-mode-menu.d.ts +101 -0
  17. package/dist/catalog/agent-mode-menu.js +115 -0
  18. package/dist/catalog/agent-mode-menu.js.map +1 -0
  19. package/dist/catalog/chat-pickers.cjs +51 -0
  20. package/dist/catalog/chat-pickers.cjs.map +1 -1
  21. package/dist/catalog/chat-pickers.d.ts +28 -0
  22. package/dist/catalog/chat-pickers.js +52 -1
  23. package/dist/catalog/chat-pickers.js.map +1 -1
  24. package/dist/catalog/runtime-catalog.cjs +4 -0
  25. package/dist/catalog/runtime-catalog.cjs.map +1 -1
  26. package/dist/catalog/runtime-catalog.d.ts +16 -0
  27. package/dist/catalog/runtime-catalog.js +5 -1
  28. package/dist/catalog/runtime-catalog.js.map +1 -1
  29. package/dist/catalog/use-athena-chat-mode.cjs +139 -0
  30. package/dist/catalog/use-athena-chat-mode.cjs.map +1 -0
  31. package/dist/catalog/use-athena-chat-mode.d.ts +43 -0
  32. package/dist/catalog/use-athena-chat-mode.js +139 -0
  33. package/dist/catalog/use-athena-chat-mode.js.map +1 -0
  34. package/dist/chat/AthenaChat.cjs +2 -0
  35. package/dist/chat/AthenaChat.cjs.map +1 -1
  36. package/dist/chat/AthenaChat.js +2 -0
  37. package/dist/chat/AthenaChat.js.map +1 -1
  38. package/dist/chat-ui/statewire.cjs +3 -0
  39. package/dist/chat-ui/statewire.cjs.map +1 -1
  40. package/dist/chat-ui/statewire.d.ts +4 -2
  41. package/dist/chat-ui/statewire.js +3 -0
  42. package/dist/chat-ui/statewire.js.map +1 -1
  43. package/dist/chat-ui/thread/statewire-history-edge.cjs +149 -26
  44. package/dist/chat-ui/thread/statewire-history-edge.cjs.map +1 -1
  45. package/dist/chat-ui/thread/statewire-history-edge.d.ts +10 -3
  46. package/dist/chat-ui/thread/statewire-history-edge.js +152 -29
  47. package/dist/chat-ui/thread/statewire-history-edge.js.map +1 -1
  48. package/dist/index.cjs +3 -0
  49. package/dist/index.cjs.map +1 -1
  50. package/dist/index.d.ts +4 -2
  51. package/dist/index.js +4 -1
  52. package/dist/index.js.map +1 -1
  53. package/dist/provider/AthenaProvider.cjs +52 -1
  54. package/dist/provider/AthenaProvider.cjs.map +1 -1
  55. package/dist/provider/AthenaProvider.d.ts +11 -1
  56. package/dist/provider/AthenaProvider.js +53 -2
  57. package/dist/provider/AthenaProvider.js.map +1 -1
  58. package/dist/provider/chat-mode-context.cjs +6 -0
  59. package/dist/provider/chat-mode-context.cjs.map +1 -0
  60. package/dist/provider/chat-mode-context.d.ts +35 -0
  61. package/dist/provider/chat-mode-context.js +6 -0
  62. package/dist/provider/chat-mode-context.js.map +1 -0
  63. package/dist/react/package.json.cjs +1 -1
  64. package/dist/react/package.json.js +1 -1
  65. package/dist/runtime/statewire-run-config.cjs +10 -6
  66. package/dist/runtime/statewire-run-config.cjs.map +1 -1
  67. package/dist/runtime/statewire-run-config.d.ts +7 -0
  68. package/dist/runtime/statewire-run-config.js +10 -6
  69. package/dist/runtime/statewire-run-config.js.map +1 -1
  70. package/dist/runtime/useAthenaStatewireRuntime.cjs +2 -0
  71. package/dist/runtime/useAthenaStatewireRuntime.cjs.map +1 -1
  72. package/dist/runtime/useAthenaStatewireRuntime.d.ts +2 -0
  73. package/dist/runtime/useAthenaStatewireRuntime.js +2 -0
  74. package/dist/runtime/useAthenaStatewireRuntime.js.map +1 -1
  75. package/package.json +1 -1
@@ -52,6 +52,7 @@ function useDeepAgentWindowSync({
52
52
  nearTop = false,
53
53
  nearTopEpoch = 0,
54
54
  isMessageMounted = everyMessageMounted,
55
+ maxConcurrentToolOutputs = Number.POSITIVE_INFINITY,
55
56
  maxChainedLoads = DEEP_AGENT_MAX_CHAINED_HISTORY_LOADS,
56
57
  maxUnseenLoads = DEEP_AGENT_MAX_UNSEEN_HISTORY_LOADS,
57
58
  onHistoryLoad,
@@ -71,6 +72,9 @@ function useDeepAgentWindowSync({
71
72
  const statedRef = react.useRef(null);
72
73
  const restatedConnectionRef = react.useRef(null);
73
74
  const answeredRef = react.useRef(/* @__PURE__ */ new Map());
75
+ const toolOutputsInFlightRef = react.useRef(0);
76
+ const toolOutputsWaitingRef = react.useRef(false);
77
+ const [toolOutputSettles, setToolOutputSettles] = react.useState(0);
74
78
  const onHistoryLoadRef = react.useRef(onHistoryLoad);
75
79
  onHistoryLoadRef.current = onHistoryLoad;
76
80
  const onErrorRef = react.useRef(onError);
@@ -164,6 +168,7 @@ function useDeepAgentWindowSync({
164
168
  react.useEffect(() => {
165
169
  var _a;
166
170
  if (window === null || toolOutputs === void 0) return;
171
+ toolOutputsWaitingRef.current = false;
167
172
  for (const message of messages) {
168
173
  if (!isMessageMounted(message.id)) continue;
169
174
  for (const part of message.content) {
@@ -172,7 +177,12 @@ function useDeepAgentWindowSync({
172
177
  if (marker === null || toolOutputs.isPending(marker.messageId)) continue;
173
178
  if (((_a = toolOutputs.entry(marker.messageId)) == null ? void 0 : _a.chars) === marker.chars) continue;
174
179
  if (answeredRef.current.get(marker.messageId) === marker.chars) continue;
180
+ if (toolOutputsInFlightRef.current >= maxConcurrentToolOutputs) {
181
+ toolOutputsWaitingRef.current = true;
182
+ return;
183
+ }
175
184
  const { messageId, chars } = marker;
185
+ toolOutputsInFlightRef.current += 1;
176
186
  toolOutputs.markPending(messageId);
177
187
  Promise.resolve(sendCommand(messageWindow.buildToolOutputCommand({ messageId }))).then((payload) => {
178
188
  answeredRef.current.set(messageId, chars);
@@ -187,10 +197,23 @@ function useDeepAgentWindowSync({
187
197
  answeredRef.current.set(messageId, chars);
188
198
  toolOutputs.clearPending(messageId);
189
199
  (_a2 = onErrorRef.current) == null ? void 0 : _a2.call(onErrorRef, error, "history/tool_output");
200
+ }).finally(() => {
201
+ toolOutputsInFlightRef.current -= 1;
202
+ if (toolOutputsWaitingRef.current && aliveRef.current) {
203
+ setToolOutputSettles((count) => count + 1);
204
+ }
190
205
  });
191
206
  }
192
207
  }
193
- }, [messages, isMessageMounted, window, sendCommand, toolOutputs]);
208
+ }, [
209
+ messages,
210
+ isMessageMounted,
211
+ window,
212
+ sendCommand,
213
+ toolOutputs,
214
+ maxConcurrentToolOutputs,
215
+ toolOutputSettles
216
+ ]);
194
217
  const loadOlder = react.useCallback(() => {
195
218
  setRequests((count) => count + 1);
196
219
  }, []);
@@ -1 +1 @@
1
- {"version":3,"file":"use-deep-agent-window-sync.cjs","sources":["../../../../agent-runtime/src/use-deep-agent-window-sync.ts"],"sourcesContent":["import type { StatewireThread } from '@assistant-ui/react-statewire';\nimport { useAuiState } from '@assistant-ui/store';\nimport { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';\nimport { DEEP_AGENT_INTEREST_PROTOCOL } from './generated/runconfig-contract';\nimport {\n buildToolOutputCommand,\n type DeepAgentInterestRequest,\n type DeepAgentMessageWindow,\n interestsCovered,\n pageInterestRequests,\n readDeepAgentMessageWindow,\n readToolOutputResult,\n readTruncatedToolOutputArtifact,\n} from './message-window';\nimport { hasStatewireThreadExtras } from './thread-extras';\nimport type { DeepAgentToolOutputStore } from './tool-outputs';\n\n/**\n * Pages of older history chained per approach to the top. A renderer lowers\n * its near-top level once prepended content has pushed the first message out\n * of its band; this bounds the case where it cannot (no viewport to scroll,\n * a list too short to overflow) so a 2,000-message thread never streams whole.\n */\nexport const DEEP_AGENT_MAX_CHAINED_HISTORY_LOADS = 3;\n\n/**\n * Pages of older history that add no converted message — every message on\n * them folds into the first loaded turn, such as one agent run's hundreds of\n * tool steps — loaded on top of a request (or an approach's chain) before it\n * stops. Such a page changes nothing the user can see, so it neither answers\n * an ask nor counts against the per-approach chain.\n */\nexport const DEEP_AGENT_MAX_UNSEEN_HISTORY_LOADS = 25;\n\n/** The converted thread messages the tool-output half scans (web and native `ThreadMessage` both satisfy it). */\nexport interface DeepAgentWindowSyncMessage {\n readonly id: string;\n /** The converted role; lets the opening fill tell a turn's prompt from its reply. */\n readonly role?: string;\n readonly content: readonly { readonly type: string; readonly artifact?: unknown }[];\n}\n\n/** One page interest round trip, for the host's telemetry. */\nexport interface DeepAgentHistoryLoadEvent {\n /** The page's ordinal (`1` extends the head). */\n page: number;\n request: DeepAgentInterestRequest;\n durationMs: number;\n}\n\nexport interface UseDeepAgentWindowSyncOptions {\n /**\n * The converted thread messages — read by the host through its platform\n * `useAuiState((s) => s.thread.messages)`, so this hook imports neither\n * `@assistant-ui/react` nor `@assistant-ui/react-native`.\n */\n messages: readonly DeepAgentWindowSyncMessage[];\n /** Messages per page; the thread's window size by default. */\n pageSize?: number;\n /**\n * Full tool outputs fetched back for cut tool messages; the same store the\n * thread converts with (`useDeepAgentThread({ toolOutputs })`). Omitted, the\n * tool-output half is inert (a surface that never shows a cut output whole).\n */\n toolOutputs?: DeepAgentToolOutputStore | undefined;\n /**\n * The renderer's report that the top of the LOADED history is near — a\n * level, not an edge: while it holds and older messages exist, pages load\n * one after another (bounded by `maxChainedLoads` per approach). Omitted for\n * hosts with only a manual affordance (`loadOlder`).\n */\n nearTop?: boolean;\n /** Bumped on every rise of `nearTop` (and every explicit request), so the chain bound is per approach. */\n nearTopEpoch?: number;\n /**\n * Whether a message's content is mounted right now, so lazy fetches (cut\n * tool outputs) target what the user can see rather than the whole loaded\n * history. Omitted: every message counts as mounted (the flat renderers).\n */\n isMessageMounted?: ((messageId: string) => boolean) | undefined;\n maxChainedLoads?: number;\n /** Pages adding no converted message loaded per request before it stops; see {@link DEEP_AGENT_MAX_UNSEEN_HISTORY_LOADS}. */\n maxUnseenLoads?: number;\n /** A page interest resolved (telemetry). */\n onHistoryLoad?: ((event: DeepAgentHistoryLoadEvent) => void) | undefined;\n /** A read failed; the sync itself recovers (the pending marks are released). */\n onError?: ((error: unknown, command: 'athena/interest' | 'history/tool_output') => void) | undefined;\n now?: () => number;\n}\n\nexport interface DeepAgentWindowSyncState {\n /** The joined window, or `null` while the head interest has not resolved (both halves are inert). */\n window: DeepAgentMessageWindow | null;\n /** Whether the host holds messages older than the loaded window. */\n hasOlder: boolean;\n /**\n * Whether the next page can be asked for: older messages exist and the\n * host's interest cap has room for them. `hasOlder && !canLoadOlder` is the\n * depth limit (`MAX_INTERESTS × MAX_MESSAGE_WINDOW` messages loaded in one\n * connection) — an edge row says so instead of offering a dead button.\n */\n canLoadOlder: boolean;\n /** A page of older history is in flight. */\n loading: boolean;\n /** Ask for the next page of older history now (an edge row's button, a keypress). A no-op before the head resolved or at the beginning. */\n loadOlder: () => void;\n}\n\nconst everyMessageMounted = (): boolean => true;\nconst defaultNow = (): number => Date.now();\nconst noopSubscribe = () => () => {};\n\n/** The statewire extras of the nearest aui thread; throws on a non-statewire thread like the library's own hooks. */\nconst useStatewireExtras = (): StatewireThread.Extras =>\n useAuiState((s) => {\n if (!hasStatewireThreadExtras(s.thread.extras)) {\n throw new Error('useDeepAgentWindowSync requires a statewire thread');\n }\n return s.thread.extras as StatewireThread.Extras;\n });\n\n/** The joined interest window of the nearest statewire thread. */\nexport function useDeepAgentMessageWindow(): DeepAgentMessageWindow | null {\n const documents = useStatewireExtras().documents;\n const mounted = useSyncExternalStore(\n documents?.subscribe ?? noopSubscribe,\n () => documents?.list(),\n () => documents?.list()\n );\n return useMemo(() => readDeepAgentMessageWindow(mounted), [mounted]);\n}\n\n/**\n * The client half of the interest window, shared by every surface — mounted\n * once per runtime, inside its aui provider.\n *\n * - **Older history.** While the host renderer reports the top of the loaded\n * history near (`nearTop`) and the window says older messages exist, this\n * re-states the interests with one more page (anchored at the window's\n * first message) — the host mounts its documents for this client alone —\n * chaining at most `maxChainedLoads` pages per approach; `loadOlder` asks\n * for one page on demand (an edge row's button). Never two loads in flight.\n * A reconnect whose hello does not carry the interests this mount last\n * stated (the host remembers a client id's view across its attaches; a new\n * host instance does not) re-states them.\n * - **Cut tool outputs.** Tool cards whose output the wire carried cut\n * (`artifact._athena_content_truncated`) get it fetched whole through\n * `history/tool_output` once their message is mounted; the converter puts\n * it back (`toolOutputs`), so the card re-renders complete. An output the\n * host replaced since (the marker's `chars` changed) is fetched again.\n */\n/**\n * Whether an older page put a new converted message ahead of the thread's\n * first one. Older history only ever merges into or lands before the first\n * converted message, so the second one is a stable landmark: it moved down iff\n * the page showed something. A new tail message does not move it. Without a\n * second message (or once it is gone) the converted count decides.\n */\nfunction pageShowedEarlierMessage(\n from: { count: number; secondId: string | undefined },\n messages: readonly DeepAgentWindowSyncMessage[]\n): boolean {\n if (from.secondId !== undefined) {\n const index = messages.findIndex((message) => message.id === from.secondId);\n if (index !== -1) return index > 1;\n }\n return messages.length > from.count;\n}\n\n/**\n * Whether the loaded thread still opens without context: it should open on a\n * prompt and hold at least one earlier turn, so the newest turn shows with the\n * one before it (a turn is a prompt followed by its reply). Queued and live\n * prompts land at the tail with no reply yet, so they never count; prompts\n * alone need nothing. Without roles, a lone converted message stands in for it.\n */\nfunction opensMidTurn(messages: readonly DeepAgentWindowSyncMessage[]): boolean {\n const first = messages[0];\n if (first === undefined) return false;\n if (messages.every((message) => message.role === undefined)) return messages.length < 2;\n if (first.role !== 'user') return true;\n if (messages.every((message) => message.role === 'user')) return false;\n let turns = 0;\n for (let index = 1; index < messages.length; index += 1) {\n if (messages[index - 1]!.role === 'user' && messages[index]!.role !== 'user') turns += 1;\n }\n return turns < 2;\n}\n\nexport function useDeepAgentWindowSync({\n messages,\n pageSize,\n toolOutputs,\n nearTop = false,\n nearTopEpoch = 0,\n isMessageMounted = everyMessageMounted,\n maxChainedLoads = DEEP_AGENT_MAX_CHAINED_HISTORY_LOADS,\n maxUnseenLoads = DEEP_AGENT_MAX_UNSEEN_HISTORY_LOADS,\n onHistoryLoad,\n onError,\n now = defaultNow,\n}: UseDeepAgentWindowSyncOptions): DeepAgentWindowSyncState {\n const { sendCommand, invoke, connection } = useStatewireExtras();\n const window = useDeepAgentMessageWindow();\n const [loading, setLoading] = useState(false);\n const [requests, setRequests] = useState(0);\n const servedRequestsRef = useRef(0);\n const inflightRef = useRef(false);\n const chainRef = useRef({ epoch: 0, loads: 0, unseen: 0, requested: false });\n // Pages loaded to show the opening turn's prompt; `null` once it shows, the\n // user took over, or a page failed — never re-armed for this mount.\n const openingFillRef = useRef<number | null>(0);\n // The converted thread when the in-flight page was stated; `null` once its\n // settle was judged (or it failed).\n const pageFromRef = useRef<{ count: number; secondId: string | undefined } | null>(null);\n const aliveRef = useRef(true);\n // The interests this mount last stated, re-stated on the next connected\n // session (a hello resets the documents) so a reconnect keeps the loaded history.\n const statedRef = useRef<DeepAgentInterestRequest[] | null>(null);\n const restatedConnectionRef = useRef<unknown>(null);\n // Outputs already asked for and answered by this mount — fetched, refused,\n // or failed — by the `chars` they were asked for. The converter re-runs on\n // every wire change, and the store is a bounded LRU: without this memory a\n // refused read would leave on every pass, and once a transcript holds more\n // cut outputs than the store retains, each fetch would evict a still-loaded\n // output and the next pass would fetch it again, forever. An evicted output\n // renders its cut text (the REST display limit) rather than re-entering the\n // loop; a marker whose `chars` moved (the host replaced the output) is asked\n // for again.\n const answeredRef = useRef(new Map<string, number>());\n const onHistoryLoadRef = useRef(onHistoryLoad);\n onHistoryLoadRef.current = onHistoryLoad;\n const onErrorRef = useRef(onError);\n onErrorRef.current = onError;\n const nowRef = useRef(now);\n nowRef.current = now;\n useEffect(() => {\n aliveRef.current = true;\n return () => {\n aliveRef.current = false;\n };\n }, []);\n\n useEffect(() => {\n if (connection.status !== 'connected' || restatedConnectionRef.current === connection) return;\n // Decided once per connected session, on the hello's interest document.\n if (window === null) return;\n restatedConnectionRef.current = connection;\n const stated = statedRef.current;\n if (stated === null || interestsCovered(window.requests, stated)) return;\n invoke(DEEP_AGENT_INTEREST_PROTOCOL, 'set', [stated]).catch((error: unknown) => {\n onErrorRef.current?.(error, 'athena/interest');\n });\n }, [connection, invoke, window]);\n\n useEffect(() => {\n if (window === null || inflightRef.current) return;\n const from = pageFromRef.current;\n pageFromRef.current = null;\n const lastPageUnseen =\n from !== null && from.count > 0 && !pageShowedEarlierMessage(from, messages);\n const pageRequests = pageInterestRequests(window, pageSize);\n if (pageRequests === null) return;\n const requested = requests !== servedRequestsRef.current;\n const chain = chainRef.current;\n let fillPage = false;\n if (\n openingFillRef.current !== null &&\n (requested || nearTop || (messages.length > 0 && !opensMidTurn(messages)))\n ) {\n openingFillRef.current = null;\n }\n if (requested) {\n // An explicit ask re-arms the chain for this approach.\n servedRequestsRef.current = requests;\n chainRef.current = { epoch: nearTopEpoch, loads: 1, unseen: 0, requested: true };\n } else if (lastPageUnseen && (chain.requested || (nearTop && chain.epoch === nearTopEpoch))) {\n // The page landed inside the first loaded turn: keep going until one shows.\n if (chain.unseen >= maxUnseenLoads) return;\n chain.unseen += 1;\n } else if (openingFillRef.current !== null && opensMidTurn(messages)) {\n if (openingFillRef.current >= maxUnseenLoads) return;\n openingFillRef.current += 1;\n fillPage = true;\n } else {\n if (!nearTop) return;\n if (chain.epoch !== nearTopEpoch) {\n chainRef.current = { epoch: nearTopEpoch, loads: 0, unseen: 0, requested: false };\n }\n if (chainRef.current.loads >= maxChainedLoads) return;\n chainRef.current.loads += 1;\n }\n pageFromRef.current = { count: messages.length, secondId: messages[1]?.id };\n const page = window.pages + 1;\n const request = pageRequests[pageRequests.length - 1] as DeepAgentInterestRequest;\n inflightRef.current = true;\n setLoading(true);\n const startedAt = nowRef.current();\n invoke(DEEP_AGENT_INTEREST_PROTOCOL, 'set', [pageRequests])\n .then(() => {\n statedRef.current = pageRequests;\n onHistoryLoadRef.current?.({\n page,\n request,\n durationMs: Math.max(0, Math.round(nowRef.current() - startedAt)),\n });\n })\n .catch((error: unknown) => {\n pageFromRef.current = null;\n if (fillPage) openingFillRef.current = null;\n onErrorRef.current?.(error, 'athena/interest');\n })\n .finally(() => {\n inflightRef.current = false;\n if (aliveRef.current) setLoading(false);\n });\n // `loading` re-arms the chain once a page settled after its documents moved the window.\n }, [\n nearTop,\n nearTopEpoch,\n requests,\n window,\n loading,\n messages,\n invoke,\n maxChainedLoads,\n maxUnseenLoads,\n pageSize,\n ]);\n\n useEffect(() => {\n if (window === null || toolOutputs === undefined) return;\n for (const message of messages) {\n if (!isMessageMounted(message.id)) continue;\n for (const part of message.content) {\n if (part.type !== 'tool-call') continue;\n const marker = readTruncatedToolOutputArtifact(part.artifact);\n if (marker === null || toolOutputs.isPending(marker.messageId)) continue;\n // Fetched for this very length already; a different length means the\n // host replaced the output under the same id since.\n if (toolOutputs.entry(marker.messageId)?.chars === marker.chars) continue;\n if (answeredRef.current.get(marker.messageId) === marker.chars) continue;\n const { messageId, chars } = marker;\n toolOutputs.markPending(messageId);\n Promise.resolve(sendCommand(buildToolOutputCommand({ messageId })))\n .then((payload) => {\n answeredRef.current.set(messageId, chars);\n const result = readToolOutputResult(payload);\n if (result === null) {\n toolOutputs.clearPending(messageId);\n return;\n }\n toolOutputs.set(result.messageId, { content: result.content, chars });\n })\n .catch((error: unknown) => {\n answeredRef.current.set(messageId, chars);\n toolOutputs.clearPending(messageId);\n onErrorRef.current?.(error, 'history/tool_output');\n });\n }\n }\n }, [messages, isMessageMounted, window, sendCommand, toolOutputs]);\n\n const loadOlder = useCallback(() => {\n setRequests((count) => count + 1);\n }, []);\n\n const canLoadOlder = useMemo(\n () => window !== null && pageInterestRequests(window, pageSize) !== null,\n [window, pageSize]\n );\n\n return { window, hasOlder: window?.hasOlder ?? false, canLoadOlder, loading, loadOlder };\n}\n"],"names":["useAuiState","hasStatewireThreadExtras","useSyncExternalStore","useMemo","readDeepAgentMessageWindow","useState","useRef","useEffect","interestsCovered","DEEP_AGENT_INTEREST_PROTOCOL","pageInterestRequests","_a","readTruncatedToolOutputArtifact","buildToolOutputCommand","readToolOutputResult","useCallback"],"mappings":"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"}
1
+ {"version":3,"file":"use-deep-agent-window-sync.cjs","sources":["../../../../agent-runtime/src/use-deep-agent-window-sync.ts"],"sourcesContent":["import type { StatewireThread } from '@assistant-ui/react-statewire';\nimport { useAuiState } from '@assistant-ui/store';\nimport { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';\nimport { DEEP_AGENT_INTEREST_PROTOCOL } from './generated/runconfig-contract';\nimport {\n buildToolOutputCommand,\n type DeepAgentInterestRequest,\n type DeepAgentMessageWindow,\n interestsCovered,\n pageInterestRequests,\n readDeepAgentMessageWindow,\n readToolOutputResult,\n readTruncatedToolOutputArtifact,\n} from './message-window';\nimport { hasStatewireThreadExtras } from './thread-extras';\nimport type { DeepAgentToolOutputStore } from './tool-outputs';\n\n/**\n * Pages of older history chained per approach to the top. A renderer lowers\n * its near-top level once prepended content has pushed the first message out\n * of its band; this bounds the case where it cannot (no viewport to scroll,\n * a list too short to overflow) so a 2,000-message thread never streams whole.\n */\nexport const DEEP_AGENT_MAX_CHAINED_HISTORY_LOADS = 3;\n\n/**\n * Pages of older history that add no converted message — every message on\n * them folds into the first loaded turn, such as one agent run's hundreds of\n * tool steps — loaded on top of a request (or an approach's chain) before it\n * stops. Such a page changes nothing the user can see, so it neither answers\n * an ask nor counts against the per-approach chain.\n */\nexport const DEEP_AGENT_MAX_UNSEEN_HISTORY_LOADS = 25;\n\n/** The converted thread messages the tool-output half scans (web and native `ThreadMessage` both satisfy it). */\nexport interface DeepAgentWindowSyncMessage {\n readonly id: string;\n /** The converted role; lets the opening fill tell a turn's prompt from its reply. */\n readonly role?: string;\n readonly content: readonly { readonly type: string; readonly artifact?: unknown }[];\n}\n\n/** One page interest round trip, for the host's telemetry. */\nexport interface DeepAgentHistoryLoadEvent {\n /** The page's ordinal (`1` extends the head). */\n page: number;\n request: DeepAgentInterestRequest;\n durationMs: number;\n}\n\nexport interface UseDeepAgentWindowSyncOptions {\n /**\n * The converted thread messages — read by the host through its platform\n * `useAuiState((s) => s.thread.messages)`, so this hook imports neither\n * `@assistant-ui/react` nor `@assistant-ui/react-native`.\n */\n messages: readonly DeepAgentWindowSyncMessage[];\n /** Messages per page; the thread's window size by default. */\n pageSize?: number;\n /**\n * Full tool outputs fetched back for cut tool messages; the same store the\n * thread converts with (`useDeepAgentThread({ toolOutputs })`). Omitted, the\n * tool-output half is inert (a surface that never shows a cut output whole).\n */\n toolOutputs?: DeepAgentToolOutputStore | undefined;\n /**\n * The renderer's report that the top of the LOADED history is near — a\n * level, not an edge: while it holds and older messages exist, pages load\n * one after another (bounded by `maxChainedLoads` per approach). Omitted for\n * hosts with only a manual affordance (`loadOlder`).\n */\n nearTop?: boolean;\n /** Bumped on every rise of `nearTop` (and every explicit request), so the chain bound is per approach. */\n nearTopEpoch?: number;\n /**\n * Whether a message's content is mounted right now, so lazy fetches (cut\n * tool outputs) target what the user can see rather than the whole loaded\n * history. Omitted: every message counts as mounted (the flat renderers).\n */\n isMessageMounted?: ((messageId: string) => boolean) | undefined;\n /**\n * Cut tool outputs this mount reads at once; the rest wait for one to\n * settle. One mounted message can hold a whole agent run — a hundred tool\n * steps — and the host rate-limits a client's frames, so a reader that sends\n * them all together gets refusals back for most of them. Omitted: every\n * mounted output is asked for together (renderers whose mounted set is\n * already small).\n */\n maxConcurrentToolOutputs?: number;\n maxChainedLoads?: number;\n /** Pages adding no converted message loaded per request before it stops; see {@link DEEP_AGENT_MAX_UNSEEN_HISTORY_LOADS}. */\n maxUnseenLoads?: number;\n /** A page interest resolved (telemetry). */\n onHistoryLoad?: ((event: DeepAgentHistoryLoadEvent) => void) | undefined;\n /** A read failed; the sync itself recovers (the pending marks are released). */\n onError?: ((error: unknown, command: 'athena/interest' | 'history/tool_output') => void) | undefined;\n now?: () => number;\n}\n\nexport interface DeepAgentWindowSyncState {\n /** The joined window, or `null` while the head interest has not resolved (both halves are inert). */\n window: DeepAgentMessageWindow | null;\n /** Whether the host holds messages older than the loaded window. */\n hasOlder: boolean;\n /**\n * Whether the next page can be asked for: older messages exist and the\n * host's interest cap has room for them. `hasOlder && !canLoadOlder` is the\n * depth limit (`MAX_INTERESTS × MAX_MESSAGE_WINDOW` messages loaded in one\n * connection) — an edge row says so instead of offering a dead button.\n */\n canLoadOlder: boolean;\n /** A page of older history is in flight. */\n loading: boolean;\n /** Ask for the next page of older history now (an edge row's button, a keypress). A no-op before the head resolved or at the beginning. */\n loadOlder: () => void;\n}\n\nconst everyMessageMounted = (): boolean => true;\nconst defaultNow = (): number => Date.now();\nconst noopSubscribe = () => () => {};\n\n/** The statewire extras of the nearest aui thread; throws on a non-statewire thread like the library's own hooks. */\nconst useStatewireExtras = (): StatewireThread.Extras =>\n useAuiState((s) => {\n if (!hasStatewireThreadExtras(s.thread.extras)) {\n throw new Error('useDeepAgentWindowSync requires a statewire thread');\n }\n return s.thread.extras as StatewireThread.Extras;\n });\n\n/** The joined interest window of the nearest statewire thread. */\nexport function useDeepAgentMessageWindow(): DeepAgentMessageWindow | null {\n const documents = useStatewireExtras().documents;\n const mounted = useSyncExternalStore(\n documents?.subscribe ?? noopSubscribe,\n () => documents?.list(),\n () => documents?.list()\n );\n return useMemo(() => readDeepAgentMessageWindow(mounted), [mounted]);\n}\n\n/**\n * The client half of the interest window, shared by every surface — mounted\n * once per runtime, inside its aui provider.\n *\n * - **Older history.** While the host renderer reports the top of the loaded\n * history near (`nearTop`) and the window says older messages exist, this\n * re-states the interests with one more page (anchored at the window's\n * first message) — the host mounts its documents for this client alone —\n * chaining at most `maxChainedLoads` pages per approach; `loadOlder` asks\n * for one page on demand (an edge row's button). Never two loads in flight.\n * A reconnect whose hello does not carry the interests this mount last\n * stated (the host remembers a client id's view across its attaches; a new\n * host instance does not) re-states them.\n * - **Cut tool outputs.** Tool cards whose output the wire carried cut\n * (`artifact._athena_content_truncated`) get it fetched whole through\n * `history/tool_output` once their message is mounted, at most\n * `maxConcurrentToolOutputs` at a time; the converter puts it back\n * (`toolOutputs`), so the card re-renders complete. An output the host\n * replaced since (the marker's `chars` changed) is fetched again.\n */\n/**\n * Whether an older page put a new converted message ahead of the thread's\n * first one. Older history only ever merges into or lands before the first\n * converted message, so the second one is a stable landmark: it moved down iff\n * the page showed something. A new tail message does not move it. Without a\n * second message (or once it is gone) the converted count decides.\n */\nfunction pageShowedEarlierMessage(\n from: { count: number; secondId: string | undefined },\n messages: readonly DeepAgentWindowSyncMessage[]\n): boolean {\n if (from.secondId !== undefined) {\n const index = messages.findIndex((message) => message.id === from.secondId);\n if (index !== -1) return index > 1;\n }\n return messages.length > from.count;\n}\n\n/**\n * Whether the loaded thread still opens without context: it should open on a\n * prompt and hold at least one earlier turn, so the newest turn shows with the\n * one before it (a turn is a prompt followed by its reply). Queued and live\n * prompts land at the tail with no reply yet, so they never count; prompts\n * alone need nothing. Without roles, a lone converted message stands in for it.\n */\nfunction opensMidTurn(messages: readonly DeepAgentWindowSyncMessage[]): boolean {\n const first = messages[0];\n if (first === undefined) return false;\n if (messages.every((message) => message.role === undefined)) return messages.length < 2;\n if (first.role !== 'user') return true;\n if (messages.every((message) => message.role === 'user')) return false;\n let turns = 0;\n for (let index = 1; index < messages.length; index += 1) {\n if (messages[index - 1]!.role === 'user' && messages[index]!.role !== 'user') turns += 1;\n }\n return turns < 2;\n}\n\nexport function useDeepAgentWindowSync({\n messages,\n pageSize,\n toolOutputs,\n nearTop = false,\n nearTopEpoch = 0,\n isMessageMounted = everyMessageMounted,\n maxConcurrentToolOutputs = Number.POSITIVE_INFINITY,\n maxChainedLoads = DEEP_AGENT_MAX_CHAINED_HISTORY_LOADS,\n maxUnseenLoads = DEEP_AGENT_MAX_UNSEEN_HISTORY_LOADS,\n onHistoryLoad,\n onError,\n now = defaultNow,\n}: UseDeepAgentWindowSyncOptions): DeepAgentWindowSyncState {\n const { sendCommand, invoke, connection } = useStatewireExtras();\n const window = useDeepAgentMessageWindow();\n const [loading, setLoading] = useState(false);\n const [requests, setRequests] = useState(0);\n const servedRequestsRef = useRef(0);\n const inflightRef = useRef(false);\n const chainRef = useRef({ epoch: 0, loads: 0, unseen: 0, requested: false });\n // Pages loaded to show the opening turn's prompt; `null` once it shows, the\n // user took over, or a page failed — never re-armed for this mount.\n const openingFillRef = useRef<number | null>(0);\n // The converted thread when the in-flight page was stated; `null` once its\n // settle was judged (or it failed).\n const pageFromRef = useRef<{ count: number; secondId: string | undefined } | null>(null);\n const aliveRef = useRef(true);\n // The interests this mount last stated, re-stated on the next connected\n // session (a hello resets the documents) so a reconnect keeps the loaded history.\n const statedRef = useRef<DeepAgentInterestRequest[] | null>(null);\n const restatedConnectionRef = useRef<unknown>(null);\n // Outputs already asked for and answered by this mount — fetched, refused,\n // or failed — by the `chars` they were asked for. The converter re-runs on\n // every wire change, and the store is a bounded LRU: without this memory a\n // refused read would leave on every pass, and once a transcript holds more\n // cut outputs than the store retains, each fetch would evict a still-loaded\n // output and the next pass would fetch it again, forever. An evicted output\n // renders its cut text (the REST display limit) rather than re-entering the\n // loop; a marker whose `chars` moved (the host replaced the output) is asked\n // for again.\n const answeredRef = useRef(new Map<string, number>());\n // Reads this mount has in flight, and whether a pass left outputs waiting\n // for one to settle (`maxConcurrentToolOutputs`): only then does a settle\n // re-run the pass, so an unbounded mount renders no more than before.\n const toolOutputsInFlightRef = useRef(0);\n const toolOutputsWaitingRef = useRef(false);\n const [toolOutputSettles, setToolOutputSettles] = useState(0);\n const onHistoryLoadRef = useRef(onHistoryLoad);\n onHistoryLoadRef.current = onHistoryLoad;\n const onErrorRef = useRef(onError);\n onErrorRef.current = onError;\n const nowRef = useRef(now);\n nowRef.current = now;\n useEffect(() => {\n aliveRef.current = true;\n return () => {\n aliveRef.current = false;\n };\n }, []);\n\n useEffect(() => {\n if (connection.status !== 'connected' || restatedConnectionRef.current === connection) return;\n // Decided once per connected session, on the hello's interest document.\n if (window === null) return;\n restatedConnectionRef.current = connection;\n const stated = statedRef.current;\n if (stated === null || interestsCovered(window.requests, stated)) return;\n invoke(DEEP_AGENT_INTEREST_PROTOCOL, 'set', [stated]).catch((error: unknown) => {\n onErrorRef.current?.(error, 'athena/interest');\n });\n }, [connection, invoke, window]);\n\n useEffect(() => {\n if (window === null || inflightRef.current) return;\n const from = pageFromRef.current;\n pageFromRef.current = null;\n const lastPageUnseen =\n from !== null && from.count > 0 && !pageShowedEarlierMessage(from, messages);\n const pageRequests = pageInterestRequests(window, pageSize);\n if (pageRequests === null) return;\n const requested = requests !== servedRequestsRef.current;\n const chain = chainRef.current;\n let fillPage = false;\n if (\n openingFillRef.current !== null &&\n (requested || nearTop || (messages.length > 0 && !opensMidTurn(messages)))\n ) {\n openingFillRef.current = null;\n }\n if (requested) {\n // An explicit ask re-arms the chain for this approach.\n servedRequestsRef.current = requests;\n chainRef.current = { epoch: nearTopEpoch, loads: 1, unseen: 0, requested: true };\n } else if (lastPageUnseen && (chain.requested || (nearTop && chain.epoch === nearTopEpoch))) {\n // The page landed inside the first loaded turn: keep going until one shows.\n if (chain.unseen >= maxUnseenLoads) return;\n chain.unseen += 1;\n } else if (openingFillRef.current !== null && opensMidTurn(messages)) {\n if (openingFillRef.current >= maxUnseenLoads) return;\n openingFillRef.current += 1;\n fillPage = true;\n } else {\n if (!nearTop) return;\n if (chain.epoch !== nearTopEpoch) {\n chainRef.current = { epoch: nearTopEpoch, loads: 0, unseen: 0, requested: false };\n }\n if (chainRef.current.loads >= maxChainedLoads) return;\n chainRef.current.loads += 1;\n }\n pageFromRef.current = { count: messages.length, secondId: messages[1]?.id };\n const page = window.pages + 1;\n const request = pageRequests[pageRequests.length - 1] as DeepAgentInterestRequest;\n inflightRef.current = true;\n setLoading(true);\n const startedAt = nowRef.current();\n invoke(DEEP_AGENT_INTEREST_PROTOCOL, 'set', [pageRequests])\n .then(() => {\n statedRef.current = pageRequests;\n onHistoryLoadRef.current?.({\n page,\n request,\n durationMs: Math.max(0, Math.round(nowRef.current() - startedAt)),\n });\n })\n .catch((error: unknown) => {\n pageFromRef.current = null;\n if (fillPage) openingFillRef.current = null;\n onErrorRef.current?.(error, 'athena/interest');\n })\n .finally(() => {\n inflightRef.current = false;\n if (aliveRef.current) setLoading(false);\n });\n // `loading` re-arms the chain once a page settled after its documents moved the window.\n }, [\n nearTop,\n nearTopEpoch,\n requests,\n window,\n loading,\n messages,\n invoke,\n maxChainedLoads,\n maxUnseenLoads,\n pageSize,\n ]);\n\n useEffect(() => {\n if (window === null || toolOutputs === undefined) return;\n toolOutputsWaitingRef.current = false;\n for (const message of messages) {\n if (!isMessageMounted(message.id)) continue;\n for (const part of message.content) {\n if (part.type !== 'tool-call') continue;\n const marker = readTruncatedToolOutputArtifact(part.artifact);\n if (marker === null || toolOutputs.isPending(marker.messageId)) continue;\n // Fetched for this very length already; a different length means the\n // host replaced the output under the same id since.\n if (toolOutputs.entry(marker.messageId)?.chars === marker.chars) continue;\n if (answeredRef.current.get(marker.messageId) === marker.chars) continue;\n if (toolOutputsInFlightRef.current >= maxConcurrentToolOutputs) {\n toolOutputsWaitingRef.current = true;\n return;\n }\n const { messageId, chars } = marker;\n toolOutputsInFlightRef.current += 1;\n toolOutputs.markPending(messageId);\n Promise.resolve(sendCommand(buildToolOutputCommand({ messageId })))\n .then((payload) => {\n answeredRef.current.set(messageId, chars);\n const result = readToolOutputResult(payload);\n if (result === null) {\n toolOutputs.clearPending(messageId);\n return;\n }\n toolOutputs.set(result.messageId, { content: result.content, chars });\n })\n .catch((error: unknown) => {\n answeredRef.current.set(messageId, chars);\n toolOutputs.clearPending(messageId);\n onErrorRef.current?.(error, 'history/tool_output');\n })\n .finally(() => {\n toolOutputsInFlightRef.current -= 1;\n if (toolOutputsWaitingRef.current && aliveRef.current) {\n setToolOutputSettles((count) => count + 1);\n }\n });\n }\n }\n }, [\n messages,\n isMessageMounted,\n window,\n sendCommand,\n toolOutputs,\n maxConcurrentToolOutputs,\n toolOutputSettles,\n ]);\n\n const loadOlder = useCallback(() => {\n setRequests((count) => count + 1);\n }, []);\n\n const canLoadOlder = useMemo(\n () => window !== null && pageInterestRequests(window, pageSize) !== null,\n [window, pageSize]\n );\n\n return { window, hasOlder: window?.hasOlder ?? false, canLoadOlder, loading, loadOlder };\n}\n"],"names":["useAuiState","hasStatewireThreadExtras","useSyncExternalStore","useMemo","readDeepAgentMessageWindow","useState","useRef","useEffect","interestsCovered","DEEP_AGENT_INTEREST_PROTOCOL","pageInterestRequests","_a","readTruncatedToolOutputArtifact","buildToolOutputCommand","readToolOutputResult","useCallback"],"mappings":"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"}
@@ -50,6 +50,7 @@ function useDeepAgentWindowSync({
50
50
  nearTop = false,
51
51
  nearTopEpoch = 0,
52
52
  isMessageMounted = everyMessageMounted,
53
+ maxConcurrentToolOutputs = Number.POSITIVE_INFINITY,
53
54
  maxChainedLoads = DEEP_AGENT_MAX_CHAINED_HISTORY_LOADS,
54
55
  maxUnseenLoads = DEEP_AGENT_MAX_UNSEEN_HISTORY_LOADS,
55
56
  onHistoryLoad,
@@ -69,6 +70,9 @@ function useDeepAgentWindowSync({
69
70
  const statedRef = useRef(null);
70
71
  const restatedConnectionRef = useRef(null);
71
72
  const answeredRef = useRef(/* @__PURE__ */ new Map());
73
+ const toolOutputsInFlightRef = useRef(0);
74
+ const toolOutputsWaitingRef = useRef(false);
75
+ const [toolOutputSettles, setToolOutputSettles] = useState(0);
72
76
  const onHistoryLoadRef = useRef(onHistoryLoad);
73
77
  onHistoryLoadRef.current = onHistoryLoad;
74
78
  const onErrorRef = useRef(onError);
@@ -162,6 +166,7 @@ function useDeepAgentWindowSync({
162
166
  useEffect(() => {
163
167
  var _a;
164
168
  if (window === null || toolOutputs === void 0) return;
169
+ toolOutputsWaitingRef.current = false;
165
170
  for (const message of messages) {
166
171
  if (!isMessageMounted(message.id)) continue;
167
172
  for (const part of message.content) {
@@ -170,7 +175,12 @@ function useDeepAgentWindowSync({
170
175
  if (marker === null || toolOutputs.isPending(marker.messageId)) continue;
171
176
  if (((_a = toolOutputs.entry(marker.messageId)) == null ? void 0 : _a.chars) === marker.chars) continue;
172
177
  if (answeredRef.current.get(marker.messageId) === marker.chars) continue;
178
+ if (toolOutputsInFlightRef.current >= maxConcurrentToolOutputs) {
179
+ toolOutputsWaitingRef.current = true;
180
+ return;
181
+ }
173
182
  const { messageId, chars } = marker;
183
+ toolOutputsInFlightRef.current += 1;
174
184
  toolOutputs.markPending(messageId);
175
185
  Promise.resolve(sendCommand(buildToolOutputCommand({ messageId }))).then((payload) => {
176
186
  answeredRef.current.set(messageId, chars);
@@ -185,10 +195,23 @@ function useDeepAgentWindowSync({
185
195
  answeredRef.current.set(messageId, chars);
186
196
  toolOutputs.clearPending(messageId);
187
197
  (_a2 = onErrorRef.current) == null ? void 0 : _a2.call(onErrorRef, error, "history/tool_output");
198
+ }).finally(() => {
199
+ toolOutputsInFlightRef.current -= 1;
200
+ if (toolOutputsWaitingRef.current && aliveRef.current) {
201
+ setToolOutputSettles((count) => count + 1);
202
+ }
188
203
  });
189
204
  }
190
205
  }
191
- }, [messages, isMessageMounted, window, sendCommand, toolOutputs]);
206
+ }, [
207
+ messages,
208
+ isMessageMounted,
209
+ window,
210
+ sendCommand,
211
+ toolOutputs,
212
+ maxConcurrentToolOutputs,
213
+ toolOutputSettles
214
+ ]);
192
215
  const loadOlder = useCallback(() => {
193
216
  setRequests((count) => count + 1);
194
217
  }, []);
@@ -1 +1 @@
1
- {"version":3,"file":"use-deep-agent-window-sync.js","sources":["../../../../agent-runtime/src/use-deep-agent-window-sync.ts"],"sourcesContent":["import type { StatewireThread } from '@assistant-ui/react-statewire';\nimport { useAuiState } from '@assistant-ui/store';\nimport { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';\nimport { DEEP_AGENT_INTEREST_PROTOCOL } from './generated/runconfig-contract';\nimport {\n buildToolOutputCommand,\n type DeepAgentInterestRequest,\n type DeepAgentMessageWindow,\n interestsCovered,\n pageInterestRequests,\n readDeepAgentMessageWindow,\n readToolOutputResult,\n readTruncatedToolOutputArtifact,\n} from './message-window';\nimport { hasStatewireThreadExtras } from './thread-extras';\nimport type { DeepAgentToolOutputStore } from './tool-outputs';\n\n/**\n * Pages of older history chained per approach to the top. A renderer lowers\n * its near-top level once prepended content has pushed the first message out\n * of its band; this bounds the case where it cannot (no viewport to scroll,\n * a list too short to overflow) so a 2,000-message thread never streams whole.\n */\nexport const DEEP_AGENT_MAX_CHAINED_HISTORY_LOADS = 3;\n\n/**\n * Pages of older history that add no converted message — every message on\n * them folds into the first loaded turn, such as one agent run's hundreds of\n * tool steps — loaded on top of a request (or an approach's chain) before it\n * stops. Such a page changes nothing the user can see, so it neither answers\n * an ask nor counts against the per-approach chain.\n */\nexport const DEEP_AGENT_MAX_UNSEEN_HISTORY_LOADS = 25;\n\n/** The converted thread messages the tool-output half scans (web and native `ThreadMessage` both satisfy it). */\nexport interface DeepAgentWindowSyncMessage {\n readonly id: string;\n /** The converted role; lets the opening fill tell a turn's prompt from its reply. */\n readonly role?: string;\n readonly content: readonly { readonly type: string; readonly artifact?: unknown }[];\n}\n\n/** One page interest round trip, for the host's telemetry. */\nexport interface DeepAgentHistoryLoadEvent {\n /** The page's ordinal (`1` extends the head). */\n page: number;\n request: DeepAgentInterestRequest;\n durationMs: number;\n}\n\nexport interface UseDeepAgentWindowSyncOptions {\n /**\n * The converted thread messages — read by the host through its platform\n * `useAuiState((s) => s.thread.messages)`, so this hook imports neither\n * `@assistant-ui/react` nor `@assistant-ui/react-native`.\n */\n messages: readonly DeepAgentWindowSyncMessage[];\n /** Messages per page; the thread's window size by default. */\n pageSize?: number;\n /**\n * Full tool outputs fetched back for cut tool messages; the same store the\n * thread converts with (`useDeepAgentThread({ toolOutputs })`). Omitted, the\n * tool-output half is inert (a surface that never shows a cut output whole).\n */\n toolOutputs?: DeepAgentToolOutputStore | undefined;\n /**\n * The renderer's report that the top of the LOADED history is near — a\n * level, not an edge: while it holds and older messages exist, pages load\n * one after another (bounded by `maxChainedLoads` per approach). Omitted for\n * hosts with only a manual affordance (`loadOlder`).\n */\n nearTop?: boolean;\n /** Bumped on every rise of `nearTop` (and every explicit request), so the chain bound is per approach. */\n nearTopEpoch?: number;\n /**\n * Whether a message's content is mounted right now, so lazy fetches (cut\n * tool outputs) target what the user can see rather than the whole loaded\n * history. Omitted: every message counts as mounted (the flat renderers).\n */\n isMessageMounted?: ((messageId: string) => boolean) | undefined;\n maxChainedLoads?: number;\n /** Pages adding no converted message loaded per request before it stops; see {@link DEEP_AGENT_MAX_UNSEEN_HISTORY_LOADS}. */\n maxUnseenLoads?: number;\n /** A page interest resolved (telemetry). */\n onHistoryLoad?: ((event: DeepAgentHistoryLoadEvent) => void) | undefined;\n /** A read failed; the sync itself recovers (the pending marks are released). */\n onError?: ((error: unknown, command: 'athena/interest' | 'history/tool_output') => void) | undefined;\n now?: () => number;\n}\n\nexport interface DeepAgentWindowSyncState {\n /** The joined window, or `null` while the head interest has not resolved (both halves are inert). */\n window: DeepAgentMessageWindow | null;\n /** Whether the host holds messages older than the loaded window. */\n hasOlder: boolean;\n /**\n * Whether the next page can be asked for: older messages exist and the\n * host's interest cap has room for them. `hasOlder && !canLoadOlder` is the\n * depth limit (`MAX_INTERESTS × MAX_MESSAGE_WINDOW` messages loaded in one\n * connection) — an edge row says so instead of offering a dead button.\n */\n canLoadOlder: boolean;\n /** A page of older history is in flight. */\n loading: boolean;\n /** Ask for the next page of older history now (an edge row's button, a keypress). A no-op before the head resolved or at the beginning. */\n loadOlder: () => void;\n}\n\nconst everyMessageMounted = (): boolean => true;\nconst defaultNow = (): number => Date.now();\nconst noopSubscribe = () => () => {};\n\n/** The statewire extras of the nearest aui thread; throws on a non-statewire thread like the library's own hooks. */\nconst useStatewireExtras = (): StatewireThread.Extras =>\n useAuiState((s) => {\n if (!hasStatewireThreadExtras(s.thread.extras)) {\n throw new Error('useDeepAgentWindowSync requires a statewire thread');\n }\n return s.thread.extras as StatewireThread.Extras;\n });\n\n/** The joined interest window of the nearest statewire thread. */\nexport function useDeepAgentMessageWindow(): DeepAgentMessageWindow | null {\n const documents = useStatewireExtras().documents;\n const mounted = useSyncExternalStore(\n documents?.subscribe ?? noopSubscribe,\n () => documents?.list(),\n () => documents?.list()\n );\n return useMemo(() => readDeepAgentMessageWindow(mounted), [mounted]);\n}\n\n/**\n * The client half of the interest window, shared by every surface — mounted\n * once per runtime, inside its aui provider.\n *\n * - **Older history.** While the host renderer reports the top of the loaded\n * history near (`nearTop`) and the window says older messages exist, this\n * re-states the interests with one more page (anchored at the window's\n * first message) — the host mounts its documents for this client alone —\n * chaining at most `maxChainedLoads` pages per approach; `loadOlder` asks\n * for one page on demand (an edge row's button). Never two loads in flight.\n * A reconnect whose hello does not carry the interests this mount last\n * stated (the host remembers a client id's view across its attaches; a new\n * host instance does not) re-states them.\n * - **Cut tool outputs.** Tool cards whose output the wire carried cut\n * (`artifact._athena_content_truncated`) get it fetched whole through\n * `history/tool_output` once their message is mounted; the converter puts\n * it back (`toolOutputs`), so the card re-renders complete. An output the\n * host replaced since (the marker's `chars` changed) is fetched again.\n */\n/**\n * Whether an older page put a new converted message ahead of the thread's\n * first one. Older history only ever merges into or lands before the first\n * converted message, so the second one is a stable landmark: it moved down iff\n * the page showed something. A new tail message does not move it. Without a\n * second message (or once it is gone) the converted count decides.\n */\nfunction pageShowedEarlierMessage(\n from: { count: number; secondId: string | undefined },\n messages: readonly DeepAgentWindowSyncMessage[]\n): boolean {\n if (from.secondId !== undefined) {\n const index = messages.findIndex((message) => message.id === from.secondId);\n if (index !== -1) return index > 1;\n }\n return messages.length > from.count;\n}\n\n/**\n * Whether the loaded thread still opens without context: it should open on a\n * prompt and hold at least one earlier turn, so the newest turn shows with the\n * one before it (a turn is a prompt followed by its reply). Queued and live\n * prompts land at the tail with no reply yet, so they never count; prompts\n * alone need nothing. Without roles, a lone converted message stands in for it.\n */\nfunction opensMidTurn(messages: readonly DeepAgentWindowSyncMessage[]): boolean {\n const first = messages[0];\n if (first === undefined) return false;\n if (messages.every((message) => message.role === undefined)) return messages.length < 2;\n if (first.role !== 'user') return true;\n if (messages.every((message) => message.role === 'user')) return false;\n let turns = 0;\n for (let index = 1; index < messages.length; index += 1) {\n if (messages[index - 1]!.role === 'user' && messages[index]!.role !== 'user') turns += 1;\n }\n return turns < 2;\n}\n\nexport function useDeepAgentWindowSync({\n messages,\n pageSize,\n toolOutputs,\n nearTop = false,\n nearTopEpoch = 0,\n isMessageMounted = everyMessageMounted,\n maxChainedLoads = DEEP_AGENT_MAX_CHAINED_HISTORY_LOADS,\n maxUnseenLoads = DEEP_AGENT_MAX_UNSEEN_HISTORY_LOADS,\n onHistoryLoad,\n onError,\n now = defaultNow,\n}: UseDeepAgentWindowSyncOptions): DeepAgentWindowSyncState {\n const { sendCommand, invoke, connection } = useStatewireExtras();\n const window = useDeepAgentMessageWindow();\n const [loading, setLoading] = useState(false);\n const [requests, setRequests] = useState(0);\n const servedRequestsRef = useRef(0);\n const inflightRef = useRef(false);\n const chainRef = useRef({ epoch: 0, loads: 0, unseen: 0, requested: false });\n // Pages loaded to show the opening turn's prompt; `null` once it shows, the\n // user took over, or a page failed — never re-armed for this mount.\n const openingFillRef = useRef<number | null>(0);\n // The converted thread when the in-flight page was stated; `null` once its\n // settle was judged (or it failed).\n const pageFromRef = useRef<{ count: number; secondId: string | undefined } | null>(null);\n const aliveRef = useRef(true);\n // The interests this mount last stated, re-stated on the next connected\n // session (a hello resets the documents) so a reconnect keeps the loaded history.\n const statedRef = useRef<DeepAgentInterestRequest[] | null>(null);\n const restatedConnectionRef = useRef<unknown>(null);\n // Outputs already asked for and answered by this mount — fetched, refused,\n // or failed — by the `chars` they were asked for. The converter re-runs on\n // every wire change, and the store is a bounded LRU: without this memory a\n // refused read would leave on every pass, and once a transcript holds more\n // cut outputs than the store retains, each fetch would evict a still-loaded\n // output and the next pass would fetch it again, forever. An evicted output\n // renders its cut text (the REST display limit) rather than re-entering the\n // loop; a marker whose `chars` moved (the host replaced the output) is asked\n // for again.\n const answeredRef = useRef(new Map<string, number>());\n const onHistoryLoadRef = useRef(onHistoryLoad);\n onHistoryLoadRef.current = onHistoryLoad;\n const onErrorRef = useRef(onError);\n onErrorRef.current = onError;\n const nowRef = useRef(now);\n nowRef.current = now;\n useEffect(() => {\n aliveRef.current = true;\n return () => {\n aliveRef.current = false;\n };\n }, []);\n\n useEffect(() => {\n if (connection.status !== 'connected' || restatedConnectionRef.current === connection) return;\n // Decided once per connected session, on the hello's interest document.\n if (window === null) return;\n restatedConnectionRef.current = connection;\n const stated = statedRef.current;\n if (stated === null || interestsCovered(window.requests, stated)) return;\n invoke(DEEP_AGENT_INTEREST_PROTOCOL, 'set', [stated]).catch((error: unknown) => {\n onErrorRef.current?.(error, 'athena/interest');\n });\n }, [connection, invoke, window]);\n\n useEffect(() => {\n if (window === null || inflightRef.current) return;\n const from = pageFromRef.current;\n pageFromRef.current = null;\n const lastPageUnseen =\n from !== null && from.count > 0 && !pageShowedEarlierMessage(from, messages);\n const pageRequests = pageInterestRequests(window, pageSize);\n if (pageRequests === null) return;\n const requested = requests !== servedRequestsRef.current;\n const chain = chainRef.current;\n let fillPage = false;\n if (\n openingFillRef.current !== null &&\n (requested || nearTop || (messages.length > 0 && !opensMidTurn(messages)))\n ) {\n openingFillRef.current = null;\n }\n if (requested) {\n // An explicit ask re-arms the chain for this approach.\n servedRequestsRef.current = requests;\n chainRef.current = { epoch: nearTopEpoch, loads: 1, unseen: 0, requested: true };\n } else if (lastPageUnseen && (chain.requested || (nearTop && chain.epoch === nearTopEpoch))) {\n // The page landed inside the first loaded turn: keep going until one shows.\n if (chain.unseen >= maxUnseenLoads) return;\n chain.unseen += 1;\n } else if (openingFillRef.current !== null && opensMidTurn(messages)) {\n if (openingFillRef.current >= maxUnseenLoads) return;\n openingFillRef.current += 1;\n fillPage = true;\n } else {\n if (!nearTop) return;\n if (chain.epoch !== nearTopEpoch) {\n chainRef.current = { epoch: nearTopEpoch, loads: 0, unseen: 0, requested: false };\n }\n if (chainRef.current.loads >= maxChainedLoads) return;\n chainRef.current.loads += 1;\n }\n pageFromRef.current = { count: messages.length, secondId: messages[1]?.id };\n const page = window.pages + 1;\n const request = pageRequests[pageRequests.length - 1] as DeepAgentInterestRequest;\n inflightRef.current = true;\n setLoading(true);\n const startedAt = nowRef.current();\n invoke(DEEP_AGENT_INTEREST_PROTOCOL, 'set', [pageRequests])\n .then(() => {\n statedRef.current = pageRequests;\n onHistoryLoadRef.current?.({\n page,\n request,\n durationMs: Math.max(0, Math.round(nowRef.current() - startedAt)),\n });\n })\n .catch((error: unknown) => {\n pageFromRef.current = null;\n if (fillPage) openingFillRef.current = null;\n onErrorRef.current?.(error, 'athena/interest');\n })\n .finally(() => {\n inflightRef.current = false;\n if (aliveRef.current) setLoading(false);\n });\n // `loading` re-arms the chain once a page settled after its documents moved the window.\n }, [\n nearTop,\n nearTopEpoch,\n requests,\n window,\n loading,\n messages,\n invoke,\n maxChainedLoads,\n maxUnseenLoads,\n pageSize,\n ]);\n\n useEffect(() => {\n if (window === null || toolOutputs === undefined) return;\n for (const message of messages) {\n if (!isMessageMounted(message.id)) continue;\n for (const part of message.content) {\n if (part.type !== 'tool-call') continue;\n const marker = readTruncatedToolOutputArtifact(part.artifact);\n if (marker === null || toolOutputs.isPending(marker.messageId)) continue;\n // Fetched for this very length already; a different length means the\n // host replaced the output under the same id since.\n if (toolOutputs.entry(marker.messageId)?.chars === marker.chars) continue;\n if (answeredRef.current.get(marker.messageId) === marker.chars) continue;\n const { messageId, chars } = marker;\n toolOutputs.markPending(messageId);\n Promise.resolve(sendCommand(buildToolOutputCommand({ messageId })))\n .then((payload) => {\n answeredRef.current.set(messageId, chars);\n const result = readToolOutputResult(payload);\n if (result === null) {\n toolOutputs.clearPending(messageId);\n return;\n }\n toolOutputs.set(result.messageId, { content: result.content, chars });\n })\n .catch((error: unknown) => {\n answeredRef.current.set(messageId, chars);\n toolOutputs.clearPending(messageId);\n onErrorRef.current?.(error, 'history/tool_output');\n });\n }\n }\n }, [messages, isMessageMounted, window, sendCommand, toolOutputs]);\n\n const loadOlder = useCallback(() => {\n setRequests((count) => count + 1);\n }, []);\n\n const canLoadOlder = useMemo(\n () => window !== null && pageInterestRequests(window, pageSize) !== null,\n [window, pageSize]\n );\n\n return { window, hasOlder: window?.hasOlder ?? false, canLoadOlder, loading, loadOlder };\n}\n"],"names":["_a"],"mappings":"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"}
1
+ {"version":3,"file":"use-deep-agent-window-sync.js","sources":["../../../../agent-runtime/src/use-deep-agent-window-sync.ts"],"sourcesContent":["import type { StatewireThread } from '@assistant-ui/react-statewire';\nimport { useAuiState } from '@assistant-ui/store';\nimport { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';\nimport { DEEP_AGENT_INTEREST_PROTOCOL } from './generated/runconfig-contract';\nimport {\n buildToolOutputCommand,\n type DeepAgentInterestRequest,\n type DeepAgentMessageWindow,\n interestsCovered,\n pageInterestRequests,\n readDeepAgentMessageWindow,\n readToolOutputResult,\n readTruncatedToolOutputArtifact,\n} from './message-window';\nimport { hasStatewireThreadExtras } from './thread-extras';\nimport type { DeepAgentToolOutputStore } from './tool-outputs';\n\n/**\n * Pages of older history chained per approach to the top. A renderer lowers\n * its near-top level once prepended content has pushed the first message out\n * of its band; this bounds the case where it cannot (no viewport to scroll,\n * a list too short to overflow) so a 2,000-message thread never streams whole.\n */\nexport const DEEP_AGENT_MAX_CHAINED_HISTORY_LOADS = 3;\n\n/**\n * Pages of older history that add no converted message — every message on\n * them folds into the first loaded turn, such as one agent run's hundreds of\n * tool steps — loaded on top of a request (or an approach's chain) before it\n * stops. Such a page changes nothing the user can see, so it neither answers\n * an ask nor counts against the per-approach chain.\n */\nexport const DEEP_AGENT_MAX_UNSEEN_HISTORY_LOADS = 25;\n\n/** The converted thread messages the tool-output half scans (web and native `ThreadMessage` both satisfy it). */\nexport interface DeepAgentWindowSyncMessage {\n readonly id: string;\n /** The converted role; lets the opening fill tell a turn's prompt from its reply. */\n readonly role?: string;\n readonly content: readonly { readonly type: string; readonly artifact?: unknown }[];\n}\n\n/** One page interest round trip, for the host's telemetry. */\nexport interface DeepAgentHistoryLoadEvent {\n /** The page's ordinal (`1` extends the head). */\n page: number;\n request: DeepAgentInterestRequest;\n durationMs: number;\n}\n\nexport interface UseDeepAgentWindowSyncOptions {\n /**\n * The converted thread messages — read by the host through its platform\n * `useAuiState((s) => s.thread.messages)`, so this hook imports neither\n * `@assistant-ui/react` nor `@assistant-ui/react-native`.\n */\n messages: readonly DeepAgentWindowSyncMessage[];\n /** Messages per page; the thread's window size by default. */\n pageSize?: number;\n /**\n * Full tool outputs fetched back for cut tool messages; the same store the\n * thread converts with (`useDeepAgentThread({ toolOutputs })`). Omitted, the\n * tool-output half is inert (a surface that never shows a cut output whole).\n */\n toolOutputs?: DeepAgentToolOutputStore | undefined;\n /**\n * The renderer's report that the top of the LOADED history is near — a\n * level, not an edge: while it holds and older messages exist, pages load\n * one after another (bounded by `maxChainedLoads` per approach). Omitted for\n * hosts with only a manual affordance (`loadOlder`).\n */\n nearTop?: boolean;\n /** Bumped on every rise of `nearTop` (and every explicit request), so the chain bound is per approach. */\n nearTopEpoch?: number;\n /**\n * Whether a message's content is mounted right now, so lazy fetches (cut\n * tool outputs) target what the user can see rather than the whole loaded\n * history. Omitted: every message counts as mounted (the flat renderers).\n */\n isMessageMounted?: ((messageId: string) => boolean) | undefined;\n /**\n * Cut tool outputs this mount reads at once; the rest wait for one to\n * settle. One mounted message can hold a whole agent run — a hundred tool\n * steps — and the host rate-limits a client's frames, so a reader that sends\n * them all together gets refusals back for most of them. Omitted: every\n * mounted output is asked for together (renderers whose mounted set is\n * already small).\n */\n maxConcurrentToolOutputs?: number;\n maxChainedLoads?: number;\n /** Pages adding no converted message loaded per request before it stops; see {@link DEEP_AGENT_MAX_UNSEEN_HISTORY_LOADS}. */\n maxUnseenLoads?: number;\n /** A page interest resolved (telemetry). */\n onHistoryLoad?: ((event: DeepAgentHistoryLoadEvent) => void) | undefined;\n /** A read failed; the sync itself recovers (the pending marks are released). */\n onError?: ((error: unknown, command: 'athena/interest' | 'history/tool_output') => void) | undefined;\n now?: () => number;\n}\n\nexport interface DeepAgentWindowSyncState {\n /** The joined window, or `null` while the head interest has not resolved (both halves are inert). */\n window: DeepAgentMessageWindow | null;\n /** Whether the host holds messages older than the loaded window. */\n hasOlder: boolean;\n /**\n * Whether the next page can be asked for: older messages exist and the\n * host's interest cap has room for them. `hasOlder && !canLoadOlder` is the\n * depth limit (`MAX_INTERESTS × MAX_MESSAGE_WINDOW` messages loaded in one\n * connection) — an edge row says so instead of offering a dead button.\n */\n canLoadOlder: boolean;\n /** A page of older history is in flight. */\n loading: boolean;\n /** Ask for the next page of older history now (an edge row's button, a keypress). A no-op before the head resolved or at the beginning. */\n loadOlder: () => void;\n}\n\nconst everyMessageMounted = (): boolean => true;\nconst defaultNow = (): number => Date.now();\nconst noopSubscribe = () => () => {};\n\n/** The statewire extras of the nearest aui thread; throws on a non-statewire thread like the library's own hooks. */\nconst useStatewireExtras = (): StatewireThread.Extras =>\n useAuiState((s) => {\n if (!hasStatewireThreadExtras(s.thread.extras)) {\n throw new Error('useDeepAgentWindowSync requires a statewire thread');\n }\n return s.thread.extras as StatewireThread.Extras;\n });\n\n/** The joined interest window of the nearest statewire thread. */\nexport function useDeepAgentMessageWindow(): DeepAgentMessageWindow | null {\n const documents = useStatewireExtras().documents;\n const mounted = useSyncExternalStore(\n documents?.subscribe ?? noopSubscribe,\n () => documents?.list(),\n () => documents?.list()\n );\n return useMemo(() => readDeepAgentMessageWindow(mounted), [mounted]);\n}\n\n/**\n * The client half of the interest window, shared by every surface — mounted\n * once per runtime, inside its aui provider.\n *\n * - **Older history.** While the host renderer reports the top of the loaded\n * history near (`nearTop`) and the window says older messages exist, this\n * re-states the interests with one more page (anchored at the window's\n * first message) — the host mounts its documents for this client alone —\n * chaining at most `maxChainedLoads` pages per approach; `loadOlder` asks\n * for one page on demand (an edge row's button). Never two loads in flight.\n * A reconnect whose hello does not carry the interests this mount last\n * stated (the host remembers a client id's view across its attaches; a new\n * host instance does not) re-states them.\n * - **Cut tool outputs.** Tool cards whose output the wire carried cut\n * (`artifact._athena_content_truncated`) get it fetched whole through\n * `history/tool_output` once their message is mounted, at most\n * `maxConcurrentToolOutputs` at a time; the converter puts it back\n * (`toolOutputs`), so the card re-renders complete. An output the host\n * replaced since (the marker's `chars` changed) is fetched again.\n */\n/**\n * Whether an older page put a new converted message ahead of the thread's\n * first one. Older history only ever merges into or lands before the first\n * converted message, so the second one is a stable landmark: it moved down iff\n * the page showed something. A new tail message does not move it. Without a\n * second message (or once it is gone) the converted count decides.\n */\nfunction pageShowedEarlierMessage(\n from: { count: number; secondId: string | undefined },\n messages: readonly DeepAgentWindowSyncMessage[]\n): boolean {\n if (from.secondId !== undefined) {\n const index = messages.findIndex((message) => message.id === from.secondId);\n if (index !== -1) return index > 1;\n }\n return messages.length > from.count;\n}\n\n/**\n * Whether the loaded thread still opens without context: it should open on a\n * prompt and hold at least one earlier turn, so the newest turn shows with the\n * one before it (a turn is a prompt followed by its reply). Queued and live\n * prompts land at the tail with no reply yet, so they never count; prompts\n * alone need nothing. Without roles, a lone converted message stands in for it.\n */\nfunction opensMidTurn(messages: readonly DeepAgentWindowSyncMessage[]): boolean {\n const first = messages[0];\n if (first === undefined) return false;\n if (messages.every((message) => message.role === undefined)) return messages.length < 2;\n if (first.role !== 'user') return true;\n if (messages.every((message) => message.role === 'user')) return false;\n let turns = 0;\n for (let index = 1; index < messages.length; index += 1) {\n if (messages[index - 1]!.role === 'user' && messages[index]!.role !== 'user') turns += 1;\n }\n return turns < 2;\n}\n\nexport function useDeepAgentWindowSync({\n messages,\n pageSize,\n toolOutputs,\n nearTop = false,\n nearTopEpoch = 0,\n isMessageMounted = everyMessageMounted,\n maxConcurrentToolOutputs = Number.POSITIVE_INFINITY,\n maxChainedLoads = DEEP_AGENT_MAX_CHAINED_HISTORY_LOADS,\n maxUnseenLoads = DEEP_AGENT_MAX_UNSEEN_HISTORY_LOADS,\n onHistoryLoad,\n onError,\n now = defaultNow,\n}: UseDeepAgentWindowSyncOptions): DeepAgentWindowSyncState {\n const { sendCommand, invoke, connection } = useStatewireExtras();\n const window = useDeepAgentMessageWindow();\n const [loading, setLoading] = useState(false);\n const [requests, setRequests] = useState(0);\n const servedRequestsRef = useRef(0);\n const inflightRef = useRef(false);\n const chainRef = useRef({ epoch: 0, loads: 0, unseen: 0, requested: false });\n // Pages loaded to show the opening turn's prompt; `null` once it shows, the\n // user took over, or a page failed — never re-armed for this mount.\n const openingFillRef = useRef<number | null>(0);\n // The converted thread when the in-flight page was stated; `null` once its\n // settle was judged (or it failed).\n const pageFromRef = useRef<{ count: number; secondId: string | undefined } | null>(null);\n const aliveRef = useRef(true);\n // The interests this mount last stated, re-stated on the next connected\n // session (a hello resets the documents) so a reconnect keeps the loaded history.\n const statedRef = useRef<DeepAgentInterestRequest[] | null>(null);\n const restatedConnectionRef = useRef<unknown>(null);\n // Outputs already asked for and answered by this mount — fetched, refused,\n // or failed — by the `chars` they were asked for. The converter re-runs on\n // every wire change, and the store is a bounded LRU: without this memory a\n // refused read would leave on every pass, and once a transcript holds more\n // cut outputs than the store retains, each fetch would evict a still-loaded\n // output and the next pass would fetch it again, forever. An evicted output\n // renders its cut text (the REST display limit) rather than re-entering the\n // loop; a marker whose `chars` moved (the host replaced the output) is asked\n // for again.\n const answeredRef = useRef(new Map<string, number>());\n // Reads this mount has in flight, and whether a pass left outputs waiting\n // for one to settle (`maxConcurrentToolOutputs`): only then does a settle\n // re-run the pass, so an unbounded mount renders no more than before.\n const toolOutputsInFlightRef = useRef(0);\n const toolOutputsWaitingRef = useRef(false);\n const [toolOutputSettles, setToolOutputSettles] = useState(0);\n const onHistoryLoadRef = useRef(onHistoryLoad);\n onHistoryLoadRef.current = onHistoryLoad;\n const onErrorRef = useRef(onError);\n onErrorRef.current = onError;\n const nowRef = useRef(now);\n nowRef.current = now;\n useEffect(() => {\n aliveRef.current = true;\n return () => {\n aliveRef.current = false;\n };\n }, []);\n\n useEffect(() => {\n if (connection.status !== 'connected' || restatedConnectionRef.current === connection) return;\n // Decided once per connected session, on the hello's interest document.\n if (window === null) return;\n restatedConnectionRef.current = connection;\n const stated = statedRef.current;\n if (stated === null || interestsCovered(window.requests, stated)) return;\n invoke(DEEP_AGENT_INTEREST_PROTOCOL, 'set', [stated]).catch((error: unknown) => {\n onErrorRef.current?.(error, 'athena/interest');\n });\n }, [connection, invoke, window]);\n\n useEffect(() => {\n if (window === null || inflightRef.current) return;\n const from = pageFromRef.current;\n pageFromRef.current = null;\n const lastPageUnseen =\n from !== null && from.count > 0 && !pageShowedEarlierMessage(from, messages);\n const pageRequests = pageInterestRequests(window, pageSize);\n if (pageRequests === null) return;\n const requested = requests !== servedRequestsRef.current;\n const chain = chainRef.current;\n let fillPage = false;\n if (\n openingFillRef.current !== null &&\n (requested || nearTop || (messages.length > 0 && !opensMidTurn(messages)))\n ) {\n openingFillRef.current = null;\n }\n if (requested) {\n // An explicit ask re-arms the chain for this approach.\n servedRequestsRef.current = requests;\n chainRef.current = { epoch: nearTopEpoch, loads: 1, unseen: 0, requested: true };\n } else if (lastPageUnseen && (chain.requested || (nearTop && chain.epoch === nearTopEpoch))) {\n // The page landed inside the first loaded turn: keep going until one shows.\n if (chain.unseen >= maxUnseenLoads) return;\n chain.unseen += 1;\n } else if (openingFillRef.current !== null && opensMidTurn(messages)) {\n if (openingFillRef.current >= maxUnseenLoads) return;\n openingFillRef.current += 1;\n fillPage = true;\n } else {\n if (!nearTop) return;\n if (chain.epoch !== nearTopEpoch) {\n chainRef.current = { epoch: nearTopEpoch, loads: 0, unseen: 0, requested: false };\n }\n if (chainRef.current.loads >= maxChainedLoads) return;\n chainRef.current.loads += 1;\n }\n pageFromRef.current = { count: messages.length, secondId: messages[1]?.id };\n const page = window.pages + 1;\n const request = pageRequests[pageRequests.length - 1] as DeepAgentInterestRequest;\n inflightRef.current = true;\n setLoading(true);\n const startedAt = nowRef.current();\n invoke(DEEP_AGENT_INTEREST_PROTOCOL, 'set', [pageRequests])\n .then(() => {\n statedRef.current = pageRequests;\n onHistoryLoadRef.current?.({\n page,\n request,\n durationMs: Math.max(0, Math.round(nowRef.current() - startedAt)),\n });\n })\n .catch((error: unknown) => {\n pageFromRef.current = null;\n if (fillPage) openingFillRef.current = null;\n onErrorRef.current?.(error, 'athena/interest');\n })\n .finally(() => {\n inflightRef.current = false;\n if (aliveRef.current) setLoading(false);\n });\n // `loading` re-arms the chain once a page settled after its documents moved the window.\n }, [\n nearTop,\n nearTopEpoch,\n requests,\n window,\n loading,\n messages,\n invoke,\n maxChainedLoads,\n maxUnseenLoads,\n pageSize,\n ]);\n\n useEffect(() => {\n if (window === null || toolOutputs === undefined) return;\n toolOutputsWaitingRef.current = false;\n for (const message of messages) {\n if (!isMessageMounted(message.id)) continue;\n for (const part of message.content) {\n if (part.type !== 'tool-call') continue;\n const marker = readTruncatedToolOutputArtifact(part.artifact);\n if (marker === null || toolOutputs.isPending(marker.messageId)) continue;\n // Fetched for this very length already; a different length means the\n // host replaced the output under the same id since.\n if (toolOutputs.entry(marker.messageId)?.chars === marker.chars) continue;\n if (answeredRef.current.get(marker.messageId) === marker.chars) continue;\n if (toolOutputsInFlightRef.current >= maxConcurrentToolOutputs) {\n toolOutputsWaitingRef.current = true;\n return;\n }\n const { messageId, chars } = marker;\n toolOutputsInFlightRef.current += 1;\n toolOutputs.markPending(messageId);\n Promise.resolve(sendCommand(buildToolOutputCommand({ messageId })))\n .then((payload) => {\n answeredRef.current.set(messageId, chars);\n const result = readToolOutputResult(payload);\n if (result === null) {\n toolOutputs.clearPending(messageId);\n return;\n }\n toolOutputs.set(result.messageId, { content: result.content, chars });\n })\n .catch((error: unknown) => {\n answeredRef.current.set(messageId, chars);\n toolOutputs.clearPending(messageId);\n onErrorRef.current?.(error, 'history/tool_output');\n })\n .finally(() => {\n toolOutputsInFlightRef.current -= 1;\n if (toolOutputsWaitingRef.current && aliveRef.current) {\n setToolOutputSettles((count) => count + 1);\n }\n });\n }\n }\n }, [\n messages,\n isMessageMounted,\n window,\n sendCommand,\n toolOutputs,\n maxConcurrentToolOutputs,\n toolOutputSettles,\n ]);\n\n const loadOlder = useCallback(() => {\n setRequests((count) => count + 1);\n }, []);\n\n const canLoadOlder = useMemo(\n () => window !== null && pageInterestRequests(window, pageSize) !== null,\n [window, pageSize]\n );\n\n return { window, hasOlder: window?.hasOlder ?? false, canLoadOlder, loading, loadOlder };\n}\n"],"names":["_a"],"mappings":"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"}
@@ -0,0 +1,115 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const BASE_MODE_ID = "base";
4
+ const WEB_ENTRY = "web";
5
+ const MAX_MODE_KEY_LENGTH = 64;
6
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
7
+ const isModeKey = (value) => typeof value === "string" && value.length > 0 && value.length <= MAX_MODE_KEY_LENGTH;
8
+ const decoded = (value) => {
9
+ if (typeof value !== "string") return value;
10
+ try {
11
+ return JSON.parse(value);
12
+ } catch {
13
+ return value;
14
+ }
15
+ };
16
+ const uniqueKeys = (value) => {
17
+ const kept = [];
18
+ if (Array.isArray(value)) {
19
+ for (const entry of value) {
20
+ if (isModeKey(entry) && !kept.includes(entry)) kept.push(entry);
21
+ }
22
+ }
23
+ return kept;
24
+ };
25
+ function publishedAgentModeSource(athenaMetadata) {
26
+ if (!isRecord(athenaMetadata) || !athenaMetadata.published_version_id) return null;
27
+ const config = athenaMetadata.published_config;
28
+ if (!isRecord(config)) return null;
29
+ const agentModes = decoded(config.agentModes);
30
+ const stored = isRecord(agentModes) ? agentModes : {};
31
+ const startModes = {};
32
+ if (isRecord(stored.startModes)) {
33
+ for (const [channel, mode] of Object.entries(stored.startModes)) {
34
+ if (isModeKey(channel) && isModeKey(mode)) startModes[channel] = mode;
35
+ }
36
+ }
37
+ const switchModes = {};
38
+ if (isRecord(stored.switchModes)) {
39
+ for (const [channel, targets] of Object.entries(stored.switchModes)) {
40
+ const kept = uniqueKeys(targets);
41
+ if (isModeKey(channel) && kept.length > 0) switchModes[channel] = kept;
42
+ }
43
+ }
44
+ const channels = {};
45
+ const customChannels = decoded(config.customChannels);
46
+ if (Array.isArray(customChannels)) {
47
+ for (const entry of customChannels) {
48
+ if (!isRecord(entry) || !isModeKey(entry.id) || entry.id in channels) continue;
49
+ const name = typeof entry.name === "string" ? entry.name.trim() : "";
50
+ channels[entry.id] = { label: name || entry.id, enabled: entry.enabled !== false };
51
+ }
52
+ }
53
+ return {
54
+ modes: uniqueKeys(stored.modes).filter((id) => id !== BASE_MODE_ID),
55
+ startModes,
56
+ switchModes,
57
+ lockedChannels: uniqueKeys(stored.lockedChannels),
58
+ channels
59
+ };
60
+ }
61
+ function agentModeEntry({
62
+ source,
63
+ channel
64
+ }) {
65
+ return channel && isCustomEntry({ source, channel }) ? channel : WEB_ENTRY;
66
+ }
67
+ const isCustomEntry = ({ source, channel }) => channel !== BASE_MODE_ID && !source.modes.includes(channel) && channel in source.channels;
68
+ function agentModeOfChannel({
69
+ source,
70
+ channel
71
+ }) {
72
+ return isCustomEntry({ source, channel }) ? source.startModes[channel] ?? channel : channel;
73
+ }
74
+ const optionLabel = ({ source, id }) => {
75
+ var _a;
76
+ return id === BASE_MODE_ID ? "Default" : ((_a = source.channels[id]) == null ? void 0 : _a.label) ?? id;
77
+ };
78
+ function agentModeMenu({
79
+ source,
80
+ channel,
81
+ hostModes
82
+ }) {
83
+ var _a;
84
+ const entry = agentModeEntry({ source, channel });
85
+ const startModeId = source.startModes[entry] ?? (entry === WEB_ENTRY ? BASE_MODE_ID : entry);
86
+ const ownMode = channel && agentModeOfChannel({ source, channel }) === channel ? [channel] : [];
87
+ const offered = /* @__PURE__ */ new Set([startModeId, ...ownMode, ...source.switchModes[entry] ?? []]);
88
+ const options = [];
89
+ for (const id of hostModes) {
90
+ if (!offered.has(id) || options.some((option) => option.id === id)) continue;
91
+ if (id !== BASE_MODE_ID && ((_a = source.channels[id]) == null ? void 0 : _a.enabled) === false) continue;
92
+ options.push({ id, label: optionLabel({ source, id }) });
93
+ }
94
+ return {
95
+ entry,
96
+ startModeId,
97
+ firstSendModeId: channel ? agentModeOfChannel({ source, channel }) : startModeId,
98
+ options,
99
+ locked: source.lockedChannels.includes(entry)
100
+ };
101
+ }
102
+ function sessionChannelFromMetadata(athenaMetadata) {
103
+ if (!isRecord(athenaMetadata)) return null;
104
+ const current = athenaMetadata.collab_current_channel_id;
105
+ if (isModeKey(current)) return current;
106
+ const stamp = athenaMetadata.collab_channel_id;
107
+ return isModeKey(stamp) ? stamp : null;
108
+ }
109
+ exports.BASE_MODE_ID = BASE_MODE_ID;
110
+ exports.agentModeEntry = agentModeEntry;
111
+ exports.agentModeMenu = agentModeMenu;
112
+ exports.agentModeOfChannel = agentModeOfChannel;
113
+ exports.publishedAgentModeSource = publishedAgentModeSource;
114
+ exports.sessionChannelFromMetadata = sessionChannelFromMetadata;
115
+ //# sourceMappingURL=agent-mode-menu.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-mode-menu.cjs","sources":["../../src/catalog/agent-mode-menu.ts"],"sourcesContent":["/**\n * A collab agent's mode menu for an SDK chat, from its published definition.\n *\n * A chat offers the modes a session entered through its channel may run in,\n * by the rule agora checks every switch with (`resolve_mode_switch`): the\n * channel's start mode, the channel itself, and the modes the agent's author\n * lists for it (`agentModes.switchModes[<entry>]`). A channel the author\n * locks (`agentModes.lockedChannels`) keeps each session in the mode it\n * starts in: people pick the mode before a session's first message, and it\n * stays. A custom channel that is not itself a mode is its own entry; no\n * channel, a mode or a built-in channel is the Default (Text) entry (`web`).\n *\n * The host lists which of those modes its menu shows (`AthenaProvider`'s\n * `modes`); a mode the agent does not offer there is left out, never sent.\n *\n * Pure, for `useAthenaChatMode` and the tests.\n */\n\n/** The id of the agent as configured, with no mode's overrides (agora's `base`). */\nexport const BASE_MODE_ID = 'base';\n\n/** The Default (Text) entry: chats that name no custom channel of their own. */\nconst WEB_ENTRY = 'web';\n\n/** agora's limit on a mode key's length (`_MAX_MODE_KEY_LENGTH`). */\nconst MAX_MODE_KEY_LENGTH = 64;\n\n/** What an agent's published definition says about its modes. */\nexport interface AgentModeSource {\n /** Custom channels the author marked as modes (`agentModes.modes`). */\n modes: readonly string[];\n /** Channel key → the mode its sessions start in (`agentModes.startModes`). */\n startModes: Readonly<Record<string, string>>;\n /** Entry channel key → the modes its sessions may switch to (`agentModes.switchModes`). */\n switchModes: Readonly<Record<string, readonly string[]>>;\n /** Entry channels that keep each session in its start mode (`agentModes.lockedChannels`). */\n lockedChannels: readonly string[];\n /** The agent's custom channels by id (`customChannels`). */\n channels: Readonly<Record<string, { label: string; enabled: boolean }>>;\n}\n\n/** One mode a chat may run in. */\nexport interface AthenaChatModeOption {\n /** What a send names as `collab_channel_id`: `base` or a mode's id. */\n id: string;\n /** The author's name for it (`Default` for `base`). */\n label: string;\n}\n\n/** The mode menu of a chat on one channel of an agent. */\nexport interface AgentModeMenu {\n /** The entry channel its sessions are judged by (`web` for the Default (Text) entry). */\n entry: string;\n /** The mode a session of the entry starts in. */\n startModeId: string;\n /** The mode a new chat's first send runs in when nobody picks one: the\n * entry's start mode, or the channel itself when it names a mode. */\n firstSendModeId: string;\n /** The modes the host listed that the agent offers there, in the host's order. */\n options: readonly AthenaChatModeOption[];\n /** The channel keeps each session in the mode it starts in. */\n locked: boolean;\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value);\n\nconst isModeKey = (value: unknown): value is string =>\n typeof value === 'string' && value.length > 0 && value.length <= MAX_MODE_KEY_LENGTH;\n\n/** A stored JSON field: the live doc keeps it as a string, the caches decoded. */\nconst decoded = (value: unknown): unknown => {\n if (typeof value !== 'string') return value;\n try {\n return JSON.parse(value) as unknown;\n } catch {\n return value;\n }\n};\n\nconst uniqueKeys = (value: unknown): string[] => {\n const kept: string[] = [];\n if (Array.isArray(value)) {\n for (const entry of value) {\n if (isModeKey(entry) && !kept.includes(entry)) kept.push(entry);\n }\n }\n return kept;\n};\n\n/**\n * What an agent's published definition (`athenaMetadata.published_config`)\n * says about its modes, read as leniently as agora reads it\n * (`parse_agent_modes`, `parse_custom_channels`): malformed parts are dropped.\n * `null` for an agent never published.\n */\nexport function publishedAgentModeSource(athenaMetadata: unknown): AgentModeSource | null {\n if (!isRecord(athenaMetadata) || !athenaMetadata.published_version_id) return null;\n const config = athenaMetadata.published_config;\n if (!isRecord(config)) return null;\n const agentModes = decoded(config.agentModes);\n const stored = isRecord(agentModes) ? agentModes : {};\n const startModes: Record<string, string> = {};\n if (isRecord(stored.startModes)) {\n for (const [channel, mode] of Object.entries(stored.startModes)) {\n if (isModeKey(channel) && isModeKey(mode)) startModes[channel] = mode;\n }\n }\n const switchModes: Record<string, string[]> = {};\n if (isRecord(stored.switchModes)) {\n for (const [channel, targets] of Object.entries(stored.switchModes)) {\n const kept = uniqueKeys(targets);\n if (isModeKey(channel) && kept.length > 0) switchModes[channel] = kept;\n }\n }\n const channels: Record<string, { label: string; enabled: boolean }> = {};\n const customChannels = decoded(config.customChannels);\n if (Array.isArray(customChannels)) {\n for (const entry of customChannels) {\n if (!isRecord(entry) || !isModeKey(entry.id) || entry.id in channels) continue;\n const name = typeof entry.name === 'string' ? entry.name.trim() : '';\n channels[entry.id] = { label: name || entry.id, enabled: entry.enabled !== false };\n }\n }\n return {\n modes: uniqueKeys(stored.modes).filter((id) => id !== BASE_MODE_ID),\n startModes,\n switchModes,\n lockedChannels: uniqueKeys(stored.lockedChannels),\n channels,\n };\n}\n\n/**\n * The entry a session on `channel` is judged by (agora `session_entry_channel`):\n * one of the agent's custom channels that is not a mode, else the Default\n * (Text) entry.\n */\nexport function agentModeEntry({\n source,\n channel,\n}: {\n source: AgentModeSource;\n channel: string | null;\n}): string {\n return channel && isCustomEntry({ source, channel }) ? channel : WEB_ENTRY;\n}\n\nconst isCustomEntry = ({ source, channel }: { source: AgentModeSource; channel: string }) =>\n channel !== BASE_MODE_ID && !source.modes.includes(channel) && channel in source.channels;\n\n/**\n * The mode a run on `channel` runs in, as agora compiles it: a custom entry\n * channel follows its start mode even when a send names it, while `base`, a\n * mode or a built-in channel runs its own layer.\n */\nexport function agentModeOfChannel({\n source,\n channel,\n}: {\n source: AgentModeSource;\n channel: string;\n}): string {\n return isCustomEntry({ source, channel }) ? (source.startModes[channel] ?? channel) : channel;\n}\n\nconst optionLabel = ({ source, id }: { source: AgentModeSource; id: string }): string =>\n id === BASE_MODE_ID ? 'Default' : (source.channels[id]?.label ?? id);\n\n/**\n * The menu of a chat on `channel` (the provider's `channel`, `null` for none)\n * that lists `hostModes`: the modes the agent offers its entry — its start\n * mode, the channel itself (unless naming it runs that start mode), its switch\n * list — that the host listed, in the host's order, without a mode that is\n * switched off.\n */\nexport function agentModeMenu({\n source,\n channel,\n hostModes,\n}: {\n source: AgentModeSource;\n channel: string | null;\n hostModes: readonly string[];\n}): AgentModeMenu {\n const entry = agentModeEntry({ source, channel });\n const startModeId = source.startModes[entry] ?? (entry === WEB_ENTRY ? BASE_MODE_ID : entry);\n // A custom entry channel that starts in another mode runs that mode when\n // named, so it is no option of its own.\n const ownMode = channel && agentModeOfChannel({ source, channel }) === channel ? [channel] : [];\n const offered = new Set([startModeId, ...ownMode, ...(source.switchModes[entry] ?? [])]);\n const options: AthenaChatModeOption[] = [];\n for (const id of hostModes) {\n if (!offered.has(id) || options.some((option) => option.id === id)) continue;\n if (id !== BASE_MODE_ID && source.channels[id]?.enabled === false) continue;\n options.push({ id, label: optionLabel({ source, id }) });\n }\n return {\n entry,\n startModeId,\n firstSendModeId: channel ? agentModeOfChannel({ source, channel }) : startModeId,\n options,\n locked: source.lockedChannels.includes(entry),\n };\n}\n\n/**\n * The channel agora reads as a session's current one, from its session\n * asset's metadata: the mode it last switched to or started in\n * (`collab_current_channel_id`), else its channel stamp (`collab_channel_id`).\n * A send that names it continues the session without switching it; the mode\n * it runs in is {@link agentModeOfChannel} of it. `null` when neither is set.\n */\nexport function sessionChannelFromMetadata(athenaMetadata: unknown): string | null {\n if (!isRecord(athenaMetadata)) return null;\n const current = athenaMetadata.collab_current_channel_id;\n if (isModeKey(current)) return current;\n const stamp = athenaMetadata.collab_channel_id;\n return isModeKey(stamp) ? stamp : null;\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,101 @@
1
+ /**
2
+ * A collab agent's mode menu for an SDK chat, from its published definition.
3
+ *
4
+ * A chat offers the modes a session entered through its channel may run in,
5
+ * by the rule agora checks every switch with (`resolve_mode_switch`): the
6
+ * channel's start mode, the channel itself, and the modes the agent's author
7
+ * lists for it (`agentModes.switchModes[<entry>]`). A channel the author
8
+ * locks (`agentModes.lockedChannels`) keeps each session in the mode it
9
+ * starts in: people pick the mode before a session's first message, and it
10
+ * stays. A custom channel that is not itself a mode is its own entry; no
11
+ * channel, a mode or a built-in channel is the Default (Text) entry (`web`).
12
+ *
13
+ * The host lists which of those modes its menu shows (`AthenaProvider`'s
14
+ * `modes`); a mode the agent does not offer there is left out, never sent.
15
+ *
16
+ * Pure, for `useAthenaChatMode` and the tests.
17
+ */
18
+ /** The id of the agent as configured, with no mode's overrides (agora's `base`). */
19
+ export declare const BASE_MODE_ID = "base";
20
+ /** What an agent's published definition says about its modes. */
21
+ export interface AgentModeSource {
22
+ /** Custom channels the author marked as modes (`agentModes.modes`). */
23
+ modes: readonly string[];
24
+ /** Channel key → the mode its sessions start in (`agentModes.startModes`). */
25
+ startModes: Readonly<Record<string, string>>;
26
+ /** Entry channel key → the modes its sessions may switch to (`agentModes.switchModes`). */
27
+ switchModes: Readonly<Record<string, readonly string[]>>;
28
+ /** Entry channels that keep each session in its start mode (`agentModes.lockedChannels`). */
29
+ lockedChannels: readonly string[];
30
+ /** The agent's custom channels by id (`customChannels`). */
31
+ channels: Readonly<Record<string, {
32
+ label: string;
33
+ enabled: boolean;
34
+ }>>;
35
+ }
36
+ /** One mode a chat may run in. */
37
+ export interface AthenaChatModeOption {
38
+ /** What a send names as `collab_channel_id`: `base` or a mode's id. */
39
+ id: string;
40
+ /** The author's name for it (`Default` for `base`). */
41
+ label: string;
42
+ }
43
+ /** The mode menu of a chat on one channel of an agent. */
44
+ export interface AgentModeMenu {
45
+ /** The entry channel its sessions are judged by (`web` for the Default (Text) entry). */
46
+ entry: string;
47
+ /** The mode a session of the entry starts in. */
48
+ startModeId: string;
49
+ /** The mode a new chat's first send runs in when nobody picks one: the
50
+ * entry's start mode, or the channel itself when it names a mode. */
51
+ firstSendModeId: string;
52
+ /** The modes the host listed that the agent offers there, in the host's order. */
53
+ options: readonly AthenaChatModeOption[];
54
+ /** The channel keeps each session in the mode it starts in. */
55
+ locked: boolean;
56
+ }
57
+ /**
58
+ * What an agent's published definition (`athenaMetadata.published_config`)
59
+ * says about its modes, read as leniently as agora reads it
60
+ * (`parse_agent_modes`, `parse_custom_channels`): malformed parts are dropped.
61
+ * `null` for an agent never published.
62
+ */
63
+ export declare function publishedAgentModeSource(athenaMetadata: unknown): AgentModeSource | null;
64
+ /**
65
+ * The entry a session on `channel` is judged by (agora `session_entry_channel`):
66
+ * one of the agent's custom channels that is not a mode, else the Default
67
+ * (Text) entry.
68
+ */
69
+ export declare function agentModeEntry({ source, channel, }: {
70
+ source: AgentModeSource;
71
+ channel: string | null;
72
+ }): string;
73
+ /**
74
+ * The mode a run on `channel` runs in, as agora compiles it: a custom entry
75
+ * channel follows its start mode even when a send names it, while `base`, a
76
+ * mode or a built-in channel runs its own layer.
77
+ */
78
+ export declare function agentModeOfChannel({ source, channel, }: {
79
+ source: AgentModeSource;
80
+ channel: string;
81
+ }): string;
82
+ /**
83
+ * The menu of a chat on `channel` (the provider's `channel`, `null` for none)
84
+ * that lists `hostModes`: the modes the agent offers its entry — its start
85
+ * mode, the channel itself (unless naming it runs that start mode), its switch
86
+ * list — that the host listed, in the host's order, without a mode that is
87
+ * switched off.
88
+ */
89
+ export declare function agentModeMenu({ source, channel, hostModes, }: {
90
+ source: AgentModeSource;
91
+ channel: string | null;
92
+ hostModes: readonly string[];
93
+ }): AgentModeMenu;
94
+ /**
95
+ * The channel agora reads as a session's current one, from its session
96
+ * asset's metadata: the mode it last switched to or started in
97
+ * (`collab_current_channel_id`), else its channel stamp (`collab_channel_id`).
98
+ * A send that names it continues the session without switching it; the mode
99
+ * it runs in is {@link agentModeOfChannel} of it. `null` when neither is set.
100
+ */
101
+ export declare function sessionChannelFromMetadata(athenaMetadata: unknown): string | null;