@athenaintel/react 0.53.1 → 0.55.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -1
- package/dist/agent-runtime/src/call-recap.cjs +2 -3
- package/dist/agent-runtime/src/call-recap.cjs.map +1 -1
- package/dist/agent-runtime/src/call-recap.d.ts +1 -2
- package/dist/agent-runtime/src/call-recap.js +1 -2
- package/dist/agent-runtime/src/call-recap.js.map +1 -1
- package/dist/agent-runtime/src/deep-agent-transport-recipe.cjs +7 -3
- package/dist/agent-runtime/src/deep-agent-transport-recipe.cjs.map +1 -1
- package/dist/agent-runtime/src/deep-agent-transport-recipe.d.ts +10 -2
- package/dist/agent-runtime/src/deep-agent-transport-recipe.js +8 -4
- package/dist/agent-runtime/src/deep-agent-transport-recipe.js.map +1 -1
- package/dist/agent-runtime/src/dictation-session.d.ts +158 -0
- package/dist/agent-runtime/src/edit-command.cjs +15 -3
- package/dist/agent-runtime/src/edit-command.cjs.map +1 -1
- package/dist/agent-runtime/src/edit-command.d.ts +8 -0
- package/dist/agent-runtime/src/edit-command.js +15 -3
- package/dist/agent-runtime/src/edit-command.js.map +1 -1
- package/dist/agent-runtime/src/generated/runconfig-contract.cjs +10 -0
- package/dist/agent-runtime/src/generated/runconfig-contract.cjs.map +1 -1
- package/dist/agent-runtime/src/generated/runconfig-contract.d.ts +25 -1
- package/dist/agent-runtime/src/generated/runconfig-contract.js +10 -0
- package/dist/agent-runtime/src/generated/runconfig-contract.js.map +1 -1
- package/dist/agent-runtime/src/history-pages.cjs +279 -0
- package/dist/agent-runtime/src/history-pages.cjs.map +1 -0
- package/dist/agent-runtime/src/history-pages.d.ts +252 -0
- package/dist/agent-runtime/src/history-pages.js +279 -0
- package/dist/agent-runtime/src/history-pages.js.map +1 -0
- package/dist/agent-runtime/src/index.d.ts +8 -7
- package/dist/agent-runtime/src/message-window.cjs +111 -7
- package/dist/agent-runtime/src/message-window.cjs.map +1 -1
- package/dist/agent-runtime/src/message-window.d.ts +66 -6
- package/dist/agent-runtime/src/message-window.js +112 -8
- package/dist/agent-runtime/src/message-window.js.map +1 -1
- package/dist/agent-runtime/src/offline-status.cjs.map +1 -1
- package/dist/agent-runtime/src/offline-status.d.ts +64 -2
- package/dist/agent-runtime/src/offline-status.js.map +1 -1
- package/dist/agent-runtime/src/runtime.d.ts +4 -3
- package/dist/agent-runtime/src/subagent-transcripts.cjs +23 -1
- package/dist/agent-runtime/src/subagent-transcripts.cjs.map +1 -1
- package/dist/agent-runtime/src/subagent-transcripts.js +25 -3
- package/dist/agent-runtime/src/subagent-transcripts.js.map +1 -1
- package/dist/agent-runtime/src/subagents.cjs +112 -0
- package/dist/agent-runtime/src/subagents.cjs.map +1 -1
- package/dist/agent-runtime/src/subagents.d.ts +103 -0
- package/dist/agent-runtime/src/subagents.js +113 -1
- package/dist/agent-runtime/src/subagents.js.map +1 -1
- package/dist/agent-runtime/src/thread-handoff-protocol.d.ts +37 -9
- package/dist/agent-runtime/src/thread-handoff.d.ts +1 -1
- package/dist/agent-runtime/src/tool-metadata.cjs +8 -0
- package/dist/agent-runtime/src/tool-metadata.cjs.map +1 -1
- package/dist/agent-runtime/src/tool-metadata.js +8 -0
- package/dist/agent-runtime/src/tool-metadata.js.map +1 -1
- package/dist/agent-runtime/src/use-deep-agent-thread.cjs +22 -2
- package/dist/agent-runtime/src/use-deep-agent-thread.cjs.map +1 -1
- package/dist/agent-runtime/src/use-deep-agent-thread.d.ts +9 -1
- package/dist/agent-runtime/src/use-deep-agent-thread.js +23 -3
- package/dist/agent-runtime/src/use-deep-agent-thread.js.map +1 -1
- package/dist/agent-runtime/src/use-deep-agent-window-sync.cjs +106 -25
- package/dist/agent-runtime/src/use-deep-agent-window-sync.cjs.map +1 -1
- package/dist/agent-runtime/src/use-deep-agent-window-sync.d.ts +41 -6
- package/dist/agent-runtime/src/use-deep-agent-window-sync.js +109 -28
- package/dist/agent-runtime/src/use-deep-agent-window-sync.js.map +1 -1
- package/dist/agent-runtime/src/use-thread-handoff-flow.d.ts +5 -9
- package/dist/agent-runtime/src/voice-presentation.cjs +34 -0
- package/dist/agent-runtime/src/voice-presentation.cjs.map +1 -1
- package/dist/agent-runtime/src/voice-presentation.d.ts +35 -0
- package/dist/agent-runtime/src/voice-presentation.js +34 -0
- package/dist/agent-runtime/src/voice-presentation.js.map +1 -1
- package/dist/assets/AthenaAssetEmbed.cjs +4 -2
- package/dist/assets/AthenaAssetEmbed.cjs.map +1 -1
- package/dist/assets/AthenaAssetEmbed.d.ts +17 -8
- package/dist/assets/AthenaAssetEmbed.js +4 -2
- package/dist/assets/AthenaAssetEmbed.js.map +1 -1
- package/dist/assets/use-asset-embed.cjs.map +1 -1
- package/dist/assets/use-asset-embed.d.ts +7 -5
- package/dist/assets/use-asset-embed.js.map +1 -1
- package/dist/auth/account-identity.cjs +69 -0
- package/dist/auth/account-identity.cjs.map +1 -0
- package/dist/auth/account-identity.d.ts +57 -0
- package/dist/auth/account-identity.js +69 -0
- package/dist/auth/account-identity.js.map +1 -0
- package/dist/chat/AthenaChat.cjs +1 -1
- package/dist/chat/AthenaChat.cjs.map +1 -1
- package/dist/chat/AthenaChat.js +1 -1
- package/dist/chat/AthenaChat.js.map +1 -1
- package/dist/chat/super-grouping/SuperGroupingCard.cjs +51 -43
- package/dist/chat/super-grouping/SuperGroupingCard.cjs.map +1 -1
- package/dist/chat/super-grouping/SuperGroupingCard.js +51 -43
- package/dist/chat/super-grouping/SuperGroupingCard.js.map +1 -1
- package/dist/chat/super-grouping/SuperGroupingTranscript.cjs +9 -4
- package/dist/chat/super-grouping/SuperGroupingTranscript.cjs.map +1 -1
- package/dist/chat/super-grouping/SuperGroupingTranscript.js +9 -4
- package/dist/chat/super-grouping/SuperGroupingTranscript.js.map +1 -1
- package/dist/chat/super-grouping/activity-timeline.cjs +7 -4
- package/dist/chat/super-grouping/activity-timeline.cjs.map +1 -1
- package/dist/chat/super-grouping/activity-timeline.d.ts +10 -1
- package/dist/chat/super-grouping/activity-timeline.js +7 -4
- package/dist/chat/super-grouping/activity-timeline.js.map +1 -1
- package/dist/chat/super-grouping/earlier-steps-button.cjs +50 -0
- package/dist/chat/super-grouping/earlier-steps-button.cjs.map +1 -0
- package/dist/chat/super-grouping/earlier-steps-button.d.ts +11 -0
- package/dist/chat/super-grouping/earlier-steps-button.js +50 -0
- package/dist/chat/super-grouping/earlier-steps-button.js.map +1 -0
- package/dist/chat/super-grouping/toolkit-map.cjs +1 -0
- package/dist/chat/super-grouping/toolkit-map.cjs.map +1 -1
- package/dist/chat/super-grouping/toolkit-map.js +1 -0
- package/dist/chat/super-grouping/toolkit-map.js.map +1 -1
- package/dist/chat/super-grouping/use-turn-steps.cjs +28 -0
- package/dist/chat/super-grouping/use-turn-steps.cjs.map +1 -0
- package/dist/chat/super-grouping/use-turn-steps.d.ts +23 -0
- package/dist/chat/super-grouping/use-turn-steps.js +28 -0
- package/dist/chat/super-grouping/use-turn-steps.js.map +1 -0
- package/dist/chat/use-statewire-approval-request.cjs +5 -5
- package/dist/chat/use-statewire-approval-request.cjs.map +1 -1
- package/dist/chat/use-statewire-approval-request.d.ts +7 -0
- package/dist/chat/use-statewire-approval-request.js +5 -5
- package/dist/chat/use-statewire-approval-request.js.map +1 -1
- package/dist/chat-ui/mentions/use-account-workspace-id.cjs +18 -58
- package/dist/chat-ui/mentions/use-account-workspace-id.cjs.map +1 -1
- package/dist/chat-ui/mentions/use-account-workspace-id.js +18 -58
- package/dist/chat-ui/mentions/use-account-workspace-id.js.map +1 -1
- package/dist/chat-ui/messages/voice-delegation-card.cjs +93 -25
- package/dist/chat-ui/messages/voice-delegation-card.cjs.map +1 -1
- package/dist/chat-ui/messages/voice-delegation-card.d.ts +16 -7
- package/dist/chat-ui/messages/voice-delegation-card.js +95 -27
- package/dist/chat-ui/messages/voice-delegation-card.js.map +1 -1
- package/dist/chat-ui/statewire.cjs +6 -0
- package/dist/chat-ui/statewire.cjs.map +1 -1
- package/dist/chat-ui/statewire.d.ts +3 -1
- package/dist/chat-ui/statewire.js +7 -1
- package/dist/chat-ui/statewire.js.map +1 -1
- package/dist/chat-ui/thread/handoff-cursor.cjs +2 -2
- package/dist/chat-ui/thread/handoff-cursor.cjs.map +1 -1
- package/dist/chat-ui/thread/handoff-cursor.js +3 -3
- package/dist/chat-ui/thread/handoff-cursor.js.map +1 -1
- package/dist/chat-ui/thread/statewire-history-edge.cjs +4 -0
- package/dist/chat-ui/thread/statewire-history-edge.cjs.map +1 -1
- package/dist/chat-ui/thread/statewire-history-edge.d.ts +11 -1
- package/dist/chat-ui/thread/statewire-history-edge.js +5 -1
- package/dist/chat-ui/thread/statewire-history-edge.js.map +1 -1
- package/dist/collab/client.cjs +134 -7
- package/dist/collab/client.cjs.map +1 -1
- package/dist/collab/client.d.ts +44 -0
- package/dist/collab/client.js +134 -7
- package/dist/collab/client.js.map +1 -1
- package/dist/collab/index.d.ts +1 -1
- package/dist/composer/QuoteContext.cjs +3 -3
- package/dist/composer/QuoteContext.cjs.map +1 -1
- package/dist/composer/QuoteContext.js +3 -3
- package/dist/composer/QuoteContext.js.map +1 -1
- package/dist/keryx-http/src/initial-sync.cjs +121 -0
- package/dist/keryx-http/src/initial-sync.cjs.map +1 -0
- package/dist/keryx-http/src/initial-sync.d.ts +77 -0
- package/dist/keryx-http/src/initial-sync.js +104 -0
- package/dist/keryx-http/src/initial-sync.js.map +1 -0
- package/dist/keryx-http/src/unconfirmed-update-attributions.cjs +8 -0
- package/dist/keryx-http/src/unconfirmed-update-attributions.cjs.map +1 -1
- package/dist/keryx-http/src/unconfirmed-update-attributions.d.ts +6 -0
- package/dist/keryx-http/src/unconfirmed-update-attributions.js +8 -0
- package/dist/keryx-http/src/unconfirmed-update-attributions.js.map +1 -1
- package/dist/provider/AthenaProvider.cjs +11 -3
- package/dist/provider/AthenaProvider.cjs.map +1 -1
- package/dist/provider/AthenaProvider.js +12 -4
- package/dist/provider/AthenaProvider.js.map +1 -1
- package/dist/provider/statewire-window-context.cjs +6 -0
- package/dist/provider/statewire-window-context.cjs.map +1 -1
- package/dist/provider/statewire-window-context.d.ts +10 -1
- package/dist/provider/statewire-window-context.js +6 -0
- package/dist/provider/statewire-window-context.js.map +1 -1
- package/dist/react/package.json.cjs +1 -1
- package/dist/react/package.json.js +1 -1
- package/dist/runtime/useAthenaStatewireRuntime.cjs +2 -0
- package/dist/runtime/useAthenaStatewireRuntime.cjs.map +1 -1
- package/dist/runtime/useAthenaStatewireRuntime.d.ts +7 -1
- package/dist/runtime/useAthenaStatewireRuntime.js +2 -0
- package/dist/runtime/useAthenaStatewireRuntime.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/threads/api.cjs +1 -2
- package/dist/threads/api.cjs.map +1 -1
- package/dist/threads/api.d.ts +3 -2
- package/dist/threads/api.js +1 -2
- package/dist/threads/api.js.map +1 -1
- package/dist/threads/sessions-query.cjs +2 -0
- package/dist/threads/sessions-query.cjs.map +1 -1
- package/dist/threads/sessions-query.js +2 -0
- package/dist/threads/sessions-query.js.map +1 -1
- package/dist/threads/statewire-thread-list.cjs +5 -7
- package/dist/threads/statewire-thread-list.cjs.map +1 -1
- package/dist/threads/statewire-thread-list.js +7 -9
- package/dist/threads/statewire-thread-list.js.map +1 -1
- package/dist/tools/subagent-task-tool-ui.cjs +15 -5
- package/dist/tools/subagent-task-tool-ui.cjs.map +1 -1
- package/dist/tools/subagent-task-tool-ui.d.ts +5 -4
- package/dist/tools/subagent-task-tool-ui.js +16 -6
- package/dist/tools/subagent-task-tool-ui.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"offline-status.js","sources":["../../../../agent-runtime/src/offline-status.ts"],"sourcesContent":["/**\n * Athena's offline language: the one set of states and words every app uses to\n * say where the person stands when the network, or Athena, is out of reach —\n * Olympus, the desktop app, the phone app, the Chrome extension and the Office\n * add-in.\n *\n * The rule the words follow: say what still works before what doesn't. Work\n * done while Athena can't be reached is kept on the device and syncs when\n * Athena is back, so every message leads with that promise. Nothing here is\n * shown while Athena answers and nothing waits; the \"back\" messages are brief.\n *\n * Pure strings and small functions, no UI library: each app draws them its own\n * way (a banner, a header chip, a tooltip, a sheet). Reachability itself is\n * each app's own (the browser's or OS's online flag AND agora answering its\n * health read); this module only names the result.\n */\n\n/** Where the app stands with Athena. */\nexport type ConnectionState =\n /** Athena answers. Nothing is shown. */\n | 'online'\n /** The device has no network. */\n | 'offline'\n /** The network is up but Athena doesn't answer. */\n | 'unreachable'\n /** Athena answers again and what waited is being sent. */\n | 'reconnecting'\n /** Just back, and everything that waited was sent. Shown briefly. */\n | 'synced';\n\n/** Where one document's edits stand, for the chip in its header. */\nexport type DocumentSyncState =\n /** In Athena. */\n | 'saved'\n /** Being sent to Athena. */\n | 'saving'\n /** Kept on this device while Athena can't be reached; syncs when it is back. */\n | 'on-device'\n /** Kept only while this tab or app stays open (no device storage); syncs when Athena is back. */\n | 'in-memory'\n /** Sending what was kept on this device. */\n | 'syncing'\n /** Some changes overlap with Athena's version: the person chooses. */\n | 'needs-review'\n /** The document can no longer take these edits (access or state changed): kept, not sent. */\n | 'view-only'\n /**\n * Athena answers but isn't taking this document's changes for now: it holds the document while it\n * repairs it, or hasn't confirmed the latest changes for longer than usual (`SavingDelayCause`\n * says which). Kept, and saved once Athena takes them. Never said while Athena can't be reached:\n * that is `on-device` or `in-memory`.\n */\n | 'delayed';\n\n/** Why a document's saving is delayed while Athena answers (`DocumentSyncState` `delayed`). */\nexport type SavingDelayCause =\n /** Athena holds the document while it repairs it, and takes no changes to it until then. */\n | 'repairing'\n /** Athena hasn't confirmed the latest changes for longer than usual. */\n | 'unconfirmed';\n\n/** How a sentence names the device the person is on. */\nexport type DeviceNoun =\n | 'this browser'\n | 'this Mac'\n | 'this computer'\n | 'this phone'\n | 'this tablet'\n | 'this device';\n\n/**\n * What the app really keeps of work done while Athena can't be reached, so the\n * words never promise more: `device` (stored on the device, survives closing),\n * `memory` (kept only while this tab or app stays open: a tenant that turns\n * device copies off, or storage the app can't open), `none` (a surface that\n * keeps no work offline at all, like the Chrome side panel).\n */\nexport type OfflineKeeping = 'device' | 'memory' | 'none';\n\n/** A title and the one line under it (a banner, a sheet, a tooltip's heading). */\nexport interface StatusCopy {\n title: string;\n detail: string;\n}\n\n/** A short label for a chip or a disabled control, and its one-line explanation. */\nexport interface LabelCopy {\n label: string;\n detail: string;\n}\n\nfunction plural(count: number, one: string, many: string): string {\n return `${count} ${count === 1 ? one : many}`;\n}\n\n/** What a tab or an app is called when work lasts only while it stays open. */\nfunction openContainer(device: DeviceNoun): string {\n return device === 'this browser' ? 'this tab' : 'the app';\n}\n\n/** The promise after \"You're offline\" / \"Can't reach Athena\", honest to what is kept. */\nfunction keepingPromise({\n keeping,\n device,\n when,\n}: {\n keeping: OfflineKeeping;\n device: DeviceNoun;\n when: string;\n}): string {\n switch (keeping) {\n case 'device':\n return `You can keep working. Changes are saved on ${device} and sync ${when}.`;\n case 'memory':\n return `You can keep working, but changes are kept only until you close ${openContainer(device)}. They sync ${when}.`;\n case 'none':\n return `You’re still signed in. This reconnects ${when}.`;\n }\n}\n\n/**\n * The app-wide status line, or `null` while Athena answers and nothing waits.\n * `waiting` counts changes not in Athena yet: while `reconnecting` they are\n * being sent; while `online` they wait for something else (a copy that sends\n * when it is next opened), and the line stays until they are gone.\n * `keeping` is what the app really keeps offline (default `device`). A surface\n * that keeps nothing (`none`) has nothing to sync, so its way back is only\n * \"Back online\", never \"Syncing…\" or \"All changes synced\".\n */\nexport function connectionCopy({\n state,\n device,\n waiting = 0,\n keeping = 'device',\n}: {\n state: ConnectionState;\n device: DeviceNoun;\n waiting?: number;\n keeping?: OfflineKeeping;\n}): StatusCopy | null {\n switch (state) {\n case 'online':\n return waiting > 0\n ? {\n title: 'Not synced yet',\n detail: `${plural(waiting, 'change', 'changes')} on ${device} ${waiting === 1 ? 'hasn’t' : 'haven’t'} synced yet.`,\n }\n : null;\n case 'offline':\n return {\n title: 'You’re offline',\n detail: keepingPromise({ keeping, device, when: 'when you’re back online' }),\n };\n case 'unreachable':\n return {\n title: 'Can’t reach Athena',\n detail: keepingPromise({ keeping, device, when: 'when Athena is back' }),\n };\n case 'reconnecting':\n if (keeping === 'none') return { title: 'Back online', detail: '' };\n return {\n title: 'Back online',\n detail:\n waiting > 0 ? `Syncing ${plural(waiting, 'change', 'changes')}…` : 'Syncing…',\n };\n case 'synced':\n return keeping === 'none'\n ? { title: 'Back online', detail: '' }\n : { title: 'All changes synced', detail: '' };\n }\n}\n\n/**\n * The line under \"Saving delayed\": why saving waits, and what is kept meanwhile, never promising\n * more than the app keeps.\n */\nfunction savingDelayedDetail({\n delay,\n keeping,\n device,\n}: {\n delay: SavingDelayCause;\n keeping: Exclude<OfflineKeeping, 'none'>;\n device: DeviceNoun;\n}): string {\n if (delay === 'repairing') {\n return keeping === 'memory'\n ? `You can keep working, but your changes are kept only until you close ${openContainer(device)}. They save once Athena finishes repairing this document.`\n : `You can keep working. Your changes are kept on ${device} and save once Athena finishes repairing this document.`;\n }\n return keeping === 'memory'\n ? `Athena is taking longer than usual to save your latest changes. They’re kept only until you close ${openContainer(device)}, so keep it open.`\n : `Athena is taking longer than usual to save your latest changes. They’re kept on ${device} in the meantime.`;\n}\n\n/**\n * The chip in a document's header, and the line it explains itself with. For `delayed`, `delay`\n * says why (default `unconfirmed`) and `keeping` whether the changes are kept on the device or only\n * while the tab or app stays open (default `device`); every other state ignores both.\n */\nexport function documentSyncCopy({\n state,\n device,\n delay = 'unconfirmed',\n keeping = 'device',\n}: {\n state: DocumentSyncState;\n device: DeviceNoun;\n delay?: SavingDelayCause;\n keeping?: Exclude<OfflineKeeping, 'none'>;\n}): LabelCopy {\n switch (state) {\n case 'saved':\n return { label: 'Saved', detail: 'Saved to Athena.' };\n case 'saving':\n return { label: 'Saving…', detail: 'Saving to Athena.' };\n case 'on-device':\n return {\n label: `Saved on ${device}`,\n detail: `Saved on ${device}. Your changes sync when Athena is back.`,\n };\n case 'in-memory':\n return {\n label: 'Not saved yet',\n detail: `Your changes are kept only until you close ${openContainer(device)}, and save when Athena is back.`,\n };\n case 'syncing':\n return { label: 'Syncing…', detail: `Sending the changes saved on ${device}.` };\n case 'needs-review':\n return {\n label: 'Review changes',\n detail: 'Some of your changes overlap with changes made in Athena. Choose what to keep.',\n };\n case 'view-only':\n return {\n label: 'View only',\n detail: `You can’t edit this anymore. Your changes stay on ${device}.`,\n };\n case 'delayed':\n return { label: 'Saving delayed', detail: savingDelayedDetail({ delay, keeping, device }) };\n }\n}\n\n/**\n * How long a document's edits may wait unconfirmed on a connection that is up before its saving\n * reads as delayed (`delayed`, `unconfirmed`), and before its room asks Keryx why: the stall window\n * every editor shares (`ACK_STALL_MS` in `@athenaintel/asset-host/local-updates`, which Olympus's\n * `room-sync-store.test.ts` holds equal).\n */\nexport const UNCONFIRMED_EDITS_STALL_MS = 15_000;\n\n/**\n * Whether a document's saving is delayed, and why (`DocumentSyncState` `delayed`): only while Athena\n * answers and the document's room is connected; `repairing` when Keryx refuses the room's writes\n * (quarantined while it is repaired: said at once, before anything waits), `unconfirmed` once its\n * edits have stayed unconfirmed past {@link UNCONFIRMED_EDITS_STALL_MS}; `null` otherwise. Every\n * app's chip asks it after a hold and after edits kept without a connection, and ahead of syncing\n * and saving, so a room that refuses writes never spins and being offline is never a delay.\n */\nexport function savingDelayCause({\n reachable,\n connected,\n writesRefused,\n unconfirmedStalled,\n}: {\n /** Athena answers (the app's reachability, never the socket alone). */\n reachable: boolean;\n /** The document's room is connected. */\n connected: boolean;\n /** Keryx's document status says the room takes no writes for now. */\n writesRefused: boolean;\n /** Local edits have waited unconfirmed past the stall window on this connection. */\n unconfirmedStalled: boolean;\n}): SavingDelayCause | null {\n if (!reachable || !connected) return null;\n if (writesRefused) return 'repairing';\n return unconfirmedStalled ? 'unconfirmed' : null;\n}\n\n/** When something that needs Athena works again (the second half of `NEEDS_CONNECTION`'s line). */\nconst AVAILABLE_AGAIN = 'It’s available again when you’re back online.';\n\n/**\n * The reason on a control that needs Athena (share, run, upload, invite…),\n * shown disabled while Athena can't be reached rather than failing when used.\n */\nexport const NEEDS_CONNECTION: LabelCopy = {\n label: 'Needs a connection',\n detail: `This needs Athena. ${AVAILABLE_AGAIN}`,\n};\n\n/** What a composer was given to attach: a photo (from the phone's library or camera) or any file. */\nexport type AttachmentKind = 'photo' | 'file';\n\n/**\n * What a composer says when a photo or file is picked, pasted or dropped while\n * Athena can't be reached. It isn't attached: only Athena can take the upload.\n * In a chat whose messages wait to be sent meanwhile (`messagesWait`: the chat\n * keeps an outbox), the message itself still goes, so that comes first. Where\n * nothing waits (the Chrome side panel, a launcher that keeps its draft), it\n * says only when attaching works again, as `NEEDS_CONNECTION` does.\n */\nexport function attachmentNeedsConnection({\n kind,\n messagesWait,\n}: {\n kind: AttachmentKind;\n messagesWait: boolean;\n}): string {\n const attaching = `Attaching ${kind === 'photo' ? 'a photo' : 'a file'} needs Athena. ${AVAILABLE_AGAIN}`;\n return messagesWait\n ? `You can still send your message: it goes when Athena is back. ${attaching}`\n : attaching;\n}\n\n/** What a sent message carries besides its text that can't wait for Athena. */\nfunction heldExtras(withQuote: boolean): string {\n return withQuote ? 'an attachment or a quote' : 'an attachment';\n}\n\n/**\n * What a composer says when a message is sent while Athena can't be reached\n * with an attachment that never reached Athena (or, `withQuote`, a quote the\n * chat can't keep waiting). Its text could wait in the chat outbox; the\n * attachment can't, so the draft stays as it is.\n */\nexport function attachedMessageNeedsConnection({\n withQuote = false,\n}: {\n withQuote?: boolean;\n} = {}): string {\n return `Your text can wait and go when Athena is back, but ${heldExtras(withQuote)} can’t. Remove ${\n withQuote ? 'it' : 'the attachment'\n } to send the text.`;\n}\n\n/**\n * What a composer says when a message with an attachment (or, `withQuote`, a\n * quote) is sent while messages written when Athena couldn't be reached still\n * wait to go: those go first, and this one can't wait behind them.\n */\nexport function attachedMessageBehindWaiting({\n withQuote = false,\n}: {\n withQuote?: boolean;\n} = {}): string {\n return `Messages you wrote while Athena couldn’t be reached go first. A message with ${heldExtras(\n withQuote\n )} can’t wait behind them, so send it once they’ve gone.`;\n}\n\n/**\n * The reason on New Presentation while Athena can't be reached, in a workspace\n * whose new presentations start from its design system's template: a\n * presentation made offline starts blank, and only Athena can copy the\n * template, so none is made until it can.\n */\nexport const NEW_PRESENTATION_NEEDS_TEMPLATE: LabelCopy = {\n label: 'Needs a connection',\n detail:\n 'New presentations in this workspace start from its design template, which needs Athena. You can make one when you’re back online.',\n};\n\n/**\n * Whether an item opens without a connection on this device, for lists.\n * `canKeep` says whether opening it online would keep a copy here (the\n * deployment allows device copies and the item's type is one that is kept):\n * only then is the person told to open it once.\n */\nexport function offlineAvailabilityCopy({\n available,\n canKeep,\n device,\n}: {\n available: boolean;\n canKeep: boolean;\n device: DeviceNoun;\n}): LabelCopy {\n if (available) {\n return { label: 'Available offline', detail: `Opens without a connection on ${device}.` };\n }\n return canKeep\n ? {\n label: 'Not available offline',\n detail: `Open it once while online to keep it on ${device}.`,\n }\n : { label: 'Not available offline', detail: 'This opens only with a connection.' };\n}\n\n/**\n * The one-time explainer shown the first time the person goes offline, on a\n * surface that keeps work there (`keeping` `device`). A surface that keeps\n * nothing (`none`: the Chrome side panel, the Office task pane) shows no\n * explainer; its status line already says all that holds.\n */\nexport function firstOfflineExplainer({ device }: { device: DeviceNoun }): StatusCopy & {\n action: string;\n} {\n return {\n title: 'You can keep working offline',\n detail: `Documents you’ve opened recently keep working on ${device}. Your changes sync when you’re back online.`,\n action: 'Got it',\n };\n}\n\n/**\n * The summary heading of the one \"Waiting to sync\" list, from what waits on\n * the device; `null` when nothing does.\n */\nexport function waitingSummary({\n changes = 0,\n messages = 0,\n recordings = 0,\n}: {\n changes?: number;\n messages?: number;\n recordings?: number;\n}): string | null {\n const parts = [\n changes > 0 ? plural(changes, 'change', 'changes') : null,\n messages > 0 ? plural(messages, 'message', 'messages') : null,\n recordings > 0 ? plural(recordings, 'recording', 'recordings') : null,\n ].filter((part): part is string => part !== null);\n return parts.length === 0 ? null : `Waiting to sync: ${parts.join(', ')}`;\n}\n\n/**\n * Where one item in the \"Waiting to sync\" list stands: a change, a message or a\n * recording made while Athena couldn't be reached.\n */\nexport type WaitingItemState =\n /** Saved on the device; it syncs when Athena is back. */\n | 'waiting'\n /** Being sent to Athena now. */\n | 'syncing'\n /**\n * Kept only while this tab or app stays open: it couldn't be saved on the\n * device (the organization keeps nothing on devices, or storage failed).\n */\n | 'in-memory'\n /**\n * Saved on the device in something that isn't open (a document closed before\n * its edits synced, one made offline): it syncs when it is next opened with a\n * connection (opening it offline sends nothing).\n */\n | 'when-opened'\n /** Made in another workspace: it syncs once that workspace is open again, with a connection. */\n | 'other-workspace'\n /** It doesn't go on its own: the person chooses (a message that waited more than a day). */\n | 'needs-you'\n /** Athena didn't take it: the app adds why, and what the person can do about it. */\n | 'not-synced';\n\n/** The state of one item in the \"Waiting to sync\" list, and the line that explains it. */\nexport function waitingItemCopy({\n state,\n device,\n}: {\n state: WaitingItemState;\n device: DeviceNoun;\n}): LabelCopy {\n switch (state) {\n case 'waiting':\n return {\n label: 'Waiting to sync',\n detail: `Saved on ${device}. It syncs when Athena is back.`,\n };\n case 'syncing':\n return { label: 'Syncing…', detail: `Sending what was saved on ${device}.` };\n case 'in-memory':\n return {\n label: 'Not saved yet',\n detail: `Kept only until you close ${openContainer(device)}. It syncs when Athena is back.`,\n };\n case 'when-opened':\n return {\n label: 'Not synced yet',\n detail: `Saved on ${device}. It syncs when you next open it with a connection.`,\n };\n case 'other-workspace':\n return {\n label: 'Waiting for its workspace',\n detail: 'It syncs once you’re in the workspace it was made in, with a connection.',\n };\n case 'needs-you':\n return { label: 'Waiting for you', detail: 'Open it to choose what happens to it.' };\n case 'not-synced':\n return { label: 'Couldn’t sync', detail: 'Athena didn’t take it.' };\n }\n}\n\n/** The headings of the \"Waiting to sync\" list, one per kind of item. */\nexport const WAITING_SECTION_TITLES = {\n changes: 'Changes',\n messages: 'Messages',\n recordings: 'Recordings',\n} as const;\n\n/**\n * What the person can do with an item Athena didn't take (`not-synced`) in the\n * \"Waiting to sync\" list: send it again, or delete the copy on the device. That\n * copy is the item's only one, so the app asks first (`deleteFromDeviceConfirm`)\n * and never deletes it as a side effect of anything else.\n */\nexport function notSyncedActions({ device }: { device: DeviceNoun }): {\n retry: string;\n remove: string;\n} {\n return { retry: 'Try again', remove: `Delete from ${device}` };\n}\n\n/** One of `notSyncedActions`, or showing where the item is kept on the device. */\nexport type NotSyncedAction = 'retry' | 'reveal' | 'remove';\n\n/**\n * What the person hears when the device couldn't do what they asked for an\n * item Athena didn't take (its files, not Athena, failed).\n */\nexport function notSyncedActionFailed({\n action,\n device,\n}: {\n action: NotSyncedAction;\n device: DeviceNoun;\n}): string {\n switch (action) {\n case 'retry':\n return 'Couldn’t send it again. Try again in a moment.';\n case 'reveal':\n return `Couldn’t show where it’s kept on ${device}.`;\n case 'remove':\n return `Couldn’t delete it from ${device}.`;\n }\n}\n\n/**\n * The question before deleting such an item's copy on the device, naming the\n * item: the destructive choice is never the default.\n */\nexport function deleteFromDeviceConfirm({\n name,\n device,\n}: {\n name: string;\n device: DeviceNoun;\n}): StatusCopy & { confirm: string; cancel: string } {\n return {\n title: `Delete “${name}” from ${device}?`,\n detail: 'Athena didn’t take it, so this is its only copy. Deleting it can’t be undone.',\n confirm: 'Delete',\n cancel: 'Cancel',\n };\n}\n"],"names":[],"mappings":"AAyPO,MAAM,6BAA6B;AA+B1C,MAAM,kBAAkB;AAMjB,MAAM,mBAA8B;AAAA,EACzC,OAAO;AAAA,EACP,QAAQ,sBAAsB,eAAe;AAC/C;AAaO,SAAS,0BAA0B;AAAA,EACxC;AAAA,EACA;AACF,GAGW;AACT,QAAM,YAAY,aAA4C,QAAQ,kBAAkB,eAAe;AACvG,SAAO,eACH,iEAAiE,SAAS,KAC1E;AACN;AAGA,SAAS,WAAW,WAA4B;AAC9C,SAAO,YAAY,6BAA6B;AAClD;AAQO,SAAS,+BAA+B;AAAA,EAC7C,YAAY;AACd,IAEI,IAAY;AACd,SAAO,sDAAsD,WAAW,SAAS,CAAC,kBAChF,YAAY,OAAO,gBACrB;AACF;AAOO,SAAS,6BAA6B;AAAA,EAC3C,YAAY;AACd,IAEI,IAAY;AACd,SAAO,gFAAgF;AAAA,IACrF;AAAA,EAAA,CACD;AACH;"}
|
|
1
|
+
{"version":3,"file":"offline-status.js","sources":["../../../../agent-runtime/src/offline-status.ts"],"sourcesContent":["/**\n * Athena's offline language: the one set of states and words every app uses to\n * say where the person stands when the network, or Athena, is out of reach —\n * Olympus, the desktop app, the phone app, the Chrome extension and the Office\n * add-in.\n *\n * The rule the words follow: say what still works before what doesn't. Work\n * done while Athena can't be reached is kept on the device and syncs when\n * Athena is back, so every message leads with that promise. Nothing here is\n * shown while Athena answers and nothing waits; the \"back\" messages are brief.\n *\n * Pure strings and small functions, no UI library: each app draws them its own\n * way (a banner, a header chip, a tooltip, a sheet). Reachability itself is\n * each app's own (the browser's or OS's online flag AND agora answering its\n * health read); this module only names the result.\n */\n\n/** Where the app stands with Athena. */\nexport type ConnectionState =\n /** Athena answers. Nothing is shown. */\n | 'online'\n /** The device has no network. */\n | 'offline'\n /** The network is up but Athena doesn't answer. */\n | 'unreachable'\n /** Athena answers again and what waited is being sent. */\n | 'reconnecting'\n /** Just back, and everything that waited was sent. Shown briefly. */\n | 'synced';\n\n/** Where one document's edits stand, for the chip in its header. */\nexport type DocumentSyncState =\n /** In Athena. */\n | 'saved'\n /** Being sent to Athena. */\n | 'saving'\n /** Kept on this device while Athena can't be reached; syncs when it is back. */\n | 'on-device'\n /** Kept only while this tab or app stays open (no device storage); syncs when Athena is back. */\n | 'in-memory'\n /** Sending what was kept on this device. */\n | 'syncing'\n /** Some changes overlap with Athena's version: the person chooses. */\n | 'needs-review'\n /** The document can no longer take these edits (access or state changed): kept, not sent. */\n | 'view-only'\n /**\n * Athena answers but isn't taking this document's changes for now: it holds the document while it\n * repairs it, or hasn't confirmed the latest changes for longer than usual (`SavingDelayCause`\n * says which). Kept, and saved once Athena takes them. Never said while Athena can't be reached:\n * that is `on-device` or `in-memory`.\n */\n | 'delayed';\n\n/** Why a document's saving is delayed while Athena answers (`DocumentSyncState` `delayed`). */\nexport type SavingDelayCause =\n /** Athena holds the document while it repairs it, and takes no changes to it until then. */\n | 'repairing'\n /** Athena hasn't confirmed the latest changes for longer than usual. */\n | 'unconfirmed';\n\n/** How a sentence names the device the person is on. */\nexport type DeviceNoun =\n | 'this browser'\n | 'this Mac'\n | 'this computer'\n | 'this phone'\n | 'this tablet'\n | 'this device';\n\n/**\n * What the app really keeps of work done while Athena can't be reached, so the\n * words never promise more: `device` (stored on the device, survives closing),\n * `memory` (kept only while this tab or app stays open: a tenant that turns\n * device copies off, or storage the app can't open), `none` (a surface that\n * keeps no work offline at all, like the Chrome side panel).\n */\nexport type OfflineKeeping = 'device' | 'memory' | 'none';\n\n/** A title and the one line under it (a banner, a sheet, a tooltip's heading). */\nexport interface StatusCopy {\n title: string;\n detail: string;\n}\n\n/** A short label for a chip or a disabled control, and its one-line explanation. */\nexport interface LabelCopy {\n label: string;\n detail: string;\n}\n\nfunction plural(count: number, one: string, many: string): string {\n return `${count} ${count === 1 ? one : many}`;\n}\n\n/** What a tab or an app is called when work lasts only while it stays open. */\nfunction openContainer(device: DeviceNoun): string {\n return device === 'this browser' ? 'this tab' : 'the app';\n}\n\n/** The promise after \"You're offline\" / \"Can't reach Athena\", honest to what is kept. */\nfunction keepingPromise({\n keeping,\n device,\n when,\n}: {\n keeping: OfflineKeeping;\n device: DeviceNoun;\n when: string;\n}): string {\n switch (keeping) {\n case 'device':\n return `You can keep working. Changes are saved on ${device} and sync ${when}.`;\n case 'memory':\n return `You can keep working, but changes are kept only until you close ${openContainer(device)}. They sync ${when}.`;\n case 'none':\n return `You’re still signed in. This reconnects ${when}.`;\n }\n}\n\n/**\n * The app-wide status line, or `null` while Athena answers and nothing waits.\n * `waiting` counts changes not in Athena yet: while `reconnecting` they are\n * being sent; while `online` they wait for something else (a copy that sends\n * when it is next opened), and the line stays until they are gone.\n * `keeping` is what the app really keeps offline (default `device`). A surface\n * that keeps nothing (`none`) has nothing to sync, so its way back is only\n * \"Back online\", never \"Syncing…\" or \"All changes synced\".\n */\nexport function connectionCopy({\n state,\n device,\n waiting = 0,\n keeping = 'device',\n}: {\n state: ConnectionState;\n device: DeviceNoun;\n waiting?: number;\n keeping?: OfflineKeeping;\n}): StatusCopy | null {\n switch (state) {\n case 'online':\n return waiting > 0\n ? {\n title: 'Not synced yet',\n detail: `${plural(waiting, 'change', 'changes')} on ${device} ${waiting === 1 ? 'hasn’t' : 'haven’t'} synced yet.`,\n }\n : null;\n case 'offline':\n return {\n title: 'You’re offline',\n detail: keepingPromise({ keeping, device, when: 'when you’re back online' }),\n };\n case 'unreachable':\n return {\n title: 'Can’t reach Athena',\n detail: keepingPromise({ keeping, device, when: 'when Athena is back' }),\n };\n case 'reconnecting':\n if (keeping === 'none') return { title: 'Back online', detail: '' };\n return {\n title: 'Back online',\n detail:\n waiting > 0 ? `Syncing ${plural(waiting, 'change', 'changes')}…` : 'Syncing…',\n };\n case 'synced':\n return keeping === 'none'\n ? { title: 'Back online', detail: '' }\n : { title: 'All changes synced', detail: '' };\n }\n}\n\n/**\n * The line under \"Saving delayed\": why saving waits, and what is kept meanwhile, never promising\n * more than the app keeps.\n */\nfunction savingDelayedDetail({\n delay,\n keeping,\n device,\n}: {\n delay: SavingDelayCause;\n keeping: Exclude<OfflineKeeping, 'none'>;\n device: DeviceNoun;\n}): string {\n if (delay === 'repairing') {\n return keeping === 'memory'\n ? `You can keep working, but your changes are kept only until you close ${openContainer(device)}. They save once Athena finishes repairing this document.`\n : `You can keep working. Your changes are kept on ${device} and save once Athena finishes repairing this document.`;\n }\n return keeping === 'memory'\n ? `Athena is taking longer than usual to save your latest changes. They’re kept only until you close ${openContainer(device)}, so keep it open.`\n : `Athena is taking longer than usual to save your latest changes. They’re kept on ${device} in the meantime.`;\n}\n\n/**\n * What `on-device` says. The desktop app on a Mac speaks as Word, Excel and PowerPoint for Mac do\n * of a cloud document whose changes wait on the Mac (Office's own title-bar words): \"Upload\n * Pending\", and \"Upload Pending (Offline)\" while Athena can't be reached. Every other app says the\n * changes are saved on its device: a browser has no such words (Word for the web can't edit\n * offline), nor does the phone.\n */\nfunction onDeviceCopy({ device, reachable }: { device: DeviceNoun; reachable: boolean }): LabelCopy {\n if (device === 'this Mac') {\n return {\n label: reachable ? 'Upload Pending' : 'Upload Pending (Offline)',\n detail: 'Upload pending. Your changes sync when Athena is back.',\n };\n }\n return {\n label: `Saved on ${device}`,\n detail: `Saved on ${device}. Your changes sync when Athena is back.`,\n };\n}\n\n/**\n * The chip in a document's header, and the line it explains itself with. For `delayed`, `delay`\n * says why (default `unconfirmed`) and `keeping` whether the changes are kept on the device or only\n * while the tab or app stays open (default `device`); every other state ignores both. `reachable`\n * (default `true`) is whether Athena can be reached, which only the Mac's `on-device` words read.\n */\nexport function documentSyncCopy({\n state,\n device,\n delay = 'unconfirmed',\n keeping = 'device',\n reachable = true,\n}: {\n state: DocumentSyncState;\n device: DeviceNoun;\n delay?: SavingDelayCause;\n keeping?: Exclude<OfflineKeeping, 'none'>;\n reachable?: boolean;\n}): LabelCopy {\n switch (state) {\n case 'saved':\n return { label: 'Saved', detail: 'Saved to Athena.' };\n case 'saving':\n return { label: 'Saving…', detail: 'Saving to Athena.' };\n case 'on-device':\n return onDeviceCopy({ device, reachable });\n case 'in-memory':\n return {\n label: 'Not saved yet',\n detail: `Your changes are kept only until you close ${openContainer(device)}, and save when Athena is back.`,\n };\n case 'syncing':\n return { label: 'Syncing…', detail: `Sending the changes saved on ${device}.` };\n case 'needs-review':\n return {\n label: 'Review changes',\n detail: 'Some of your changes overlap with changes made in Athena. Choose what to keep.',\n };\n case 'view-only':\n return {\n label: 'View only',\n detail: `You can’t edit this anymore. Your changes stay on ${device}.`,\n };\n case 'delayed':\n return { label: 'Saving delayed', detail: savingDelayedDetail({ delay, keeping, device }) };\n }\n}\n\n/**\n * How long a document's edits may wait unconfirmed on a connection that is up before its saving\n * reads as delayed (`delayed`, `unconfirmed`), and before its room asks Keryx why: the stall window\n * every editor shares (`ACK_STALL_MS` in `@athenaintel/asset-host/local-updates`, which Olympus's\n * `room-sync-store.test.ts` holds equal).\n */\nexport const UNCONFIRMED_EDITS_STALL_MS = 15_000;\n\n/**\n * Whether a document's saving is delayed, and why (`DocumentSyncState` `delayed`): only while Athena\n * answers and the document's room is connected; `repairing` when Keryx refuses the room's writes\n * (quarantined while it is repaired: said at once, before anything waits), `unconfirmed` once its\n * edits have stayed unconfirmed past {@link UNCONFIRMED_EDITS_STALL_MS}; `null` otherwise. Every\n * app's chip asks it after a hold and after edits kept without a connection, and ahead of syncing\n * and saving, so a room that refuses writes never spins and being offline is never a delay.\n */\nexport function savingDelayCause({\n reachable,\n connected,\n writesRefused,\n unconfirmedStalled,\n}: {\n /** Athena answers (the app's reachability, never the socket alone). */\n reachable: boolean;\n /** The document's room is connected. */\n connected: boolean;\n /** Keryx's document status says the room takes no writes for now. */\n writesRefused: boolean;\n /** Local edits have waited unconfirmed past the stall window on this connection. */\n unconfirmedStalled: boolean;\n}): SavingDelayCause | null {\n if (!reachable || !connected) return null;\n if (writesRefused) return 'repairing';\n return unconfirmedStalled ? 'unconfirmed' : null;\n}\n\n/**\n * What the person lost of a document while it was open, when the server checked its room's grant\n * again (Keryx's 4401, after a share changed): `edit`, they may still view it but no longer edit it;\n * `view`, they may no longer open it at all.\n */\nexport type AccessLoss = 'edit' | 'view';\n\n/** What a document says once the person's access changed while it was open. */\nexport interface AccessChangedCopy extends StatusCopy {\n /**\n * The label of the button that saves the person's version as a file of their own, with the\n * changes Athena never took, or `null` when nothing is offered: nothing of theirs waits, or the\n * device no longer serves the document (`view`).\n */\n saveCopy: string | null;\n}\n\n/**\n * What a document says once the person's access to it changed while it was open (its editor turns\n * read-only at once), in Word's own words (Word for Mac's message bar, `strman_a6d4`, and its\n * \"Permissions Have Changed\" notice, `strman_0a3e`). `keptChanges`: changes Athena never took wait\n * on the device, never sent; Word offers to save a copy of the file so nothing typed is lost. A\n * `view` loss offers nothing to save: the device stops serving a document the person may no longer\n * read, and their changes stay where they are kept.\n */\nexport function accessChangedCopy({\n loss,\n keptChanges,\n}: {\n loss: AccessLoss;\n keptChanges: boolean;\n}): AccessChangedCopy {\n if (loss === 'view') {\n return {\n title: 'Permissions Have Changed',\n detail:\n 'The permissions on this file were updated and you no longer have rights to view this file. If you previously made changes to this file, they may not be saved.',\n saveCopy: null,\n };\n }\n if (keptChanges) {\n return {\n title: 'Can’t save',\n detail:\n 'You no longer have the permission to edit this file. To keep your changes, save a copy of the file.',\n saveCopy: 'Save a Copy',\n };\n }\n return {\n title: 'Read Only',\n detail: 'You no longer have the permission to edit this file.',\n saveCopy: null,\n };\n}\n\n/** When something that needs Athena works again (the second half of `NEEDS_CONNECTION`'s line). */\nconst AVAILABLE_AGAIN = 'It’s available again when you’re back online.';\n\n/**\n * The reason on a control that needs Athena (share, run, upload, invite…),\n * shown disabled while Athena can't be reached rather than failing when used.\n */\nexport const NEEDS_CONNECTION: LabelCopy = {\n label: 'Needs a connection',\n detail: `This needs Athena. ${AVAILABLE_AGAIN}`,\n};\n\n/** What a composer was given to attach: a photo (from the phone's library or camera) or any file. */\nexport type AttachmentKind = 'photo' | 'file';\n\n/**\n * What a composer says when a photo or file is picked, pasted or dropped while\n * Athena can't be reached. It isn't attached: only Athena can take the upload.\n * In a chat whose messages wait to be sent meanwhile (`messagesWait`: the chat\n * keeps an outbox), the message itself still goes, so that comes first. Where\n * nothing waits (the Chrome side panel, a launcher that keeps its draft), it\n * says only when attaching works again, as `NEEDS_CONNECTION` does.\n */\nexport function attachmentNeedsConnection({\n kind,\n messagesWait,\n}: {\n kind: AttachmentKind;\n messagesWait: boolean;\n}): string {\n const attaching = `Attaching ${kind === 'photo' ? 'a photo' : 'a file'} needs Athena. ${AVAILABLE_AGAIN}`;\n return messagesWait\n ? `You can still send your message: it goes when Athena is back. ${attaching}`\n : attaching;\n}\n\n/** What a sent message carries besides its text that can't wait for Athena. */\nfunction heldExtras(withQuote: boolean): string {\n return withQuote ? 'an attachment or a quote' : 'an attachment';\n}\n\n/**\n * What a composer says when a message is sent while Athena can't be reached\n * with an attachment that never reached Athena (or, `withQuote`, a quote the\n * chat can't keep waiting). Its text could wait in the chat outbox; the\n * attachment can't, so the draft stays as it is.\n */\nexport function attachedMessageNeedsConnection({\n withQuote = false,\n}: {\n withQuote?: boolean;\n} = {}): string {\n return `Your text can wait and go when Athena is back, but ${heldExtras(withQuote)} can’t. Remove ${\n withQuote ? 'it' : 'the attachment'\n } to send the text.`;\n}\n\n/**\n * What a composer says when a message with an attachment (or, `withQuote`, a\n * quote) is sent while messages written when Athena couldn't be reached still\n * wait to go: those go first, and this one can't wait behind them.\n */\nexport function attachedMessageBehindWaiting({\n withQuote = false,\n}: {\n withQuote?: boolean;\n} = {}): string {\n return `Messages you wrote while Athena couldn’t be reached go first. A message with ${heldExtras(\n withQuote\n )} can’t wait behind them, so send it once they’ve gone.`;\n}\n\n/**\n * The reason on New Presentation while Athena can't be reached, in a workspace\n * whose new presentations start from its design system's template: a\n * presentation made offline starts blank, and only Athena can copy the\n * template, so none is made until it can.\n */\nexport const NEW_PRESENTATION_NEEDS_TEMPLATE: LabelCopy = {\n label: 'Needs a connection',\n detail:\n 'New presentations in this workspace start from its design template, which needs Athena. You can make one when you’re back online.',\n};\n\n/**\n * What a download of a spreadsheet made from an Excel Macro-Enabled Workbook\n * (`.xlsm`) says when it went without the macros because they come from Athena,\n * which can't be reached, and the device hasn't kept them (they travel with the\n * workbook and never run). The file is the workbook without them; downloading\n * it again with a connection keeps them.\n */\nexport function downloadedWithoutMacros({ device }: { device: DeviceNoun }): string {\n return `Downloaded without the spreadsheet’s macros: they come from Athena, which can’t be reached, and ${device} hasn’t kept them. Download it again when you’re back online to keep them.`;\n}\n\n/**\n * Save's question for a spreadsheet whose file is an Excel Macro-Enabled\n * Workbook (`.xlsm`) while its macros come from Athena, which can't be reached,\n * and the device hasn't kept them: the `.xlsm` is never written without them,\n * so the person may save an Excel Workbook (`.xlsx`) beside it, and Save writes\n * the `.xlsm` again once Athena answers.\n */\nexport function macrosNeedConnectionToSave({\n name,\n device,\n}: {\n name: string;\n device: DeviceNoun;\n}): StatusCopy {\n return {\n title: `Athena can’t save the macros in “${name}” right now.`,\n detail: `They come from Athena, which can’t be reached, and ${device} hasn’t kept them. Save it as an Excel Workbook (.xlsx) instead. “${name}” stays as it is, macros included, and Save writes it again when you’re back online.`,\n };\n}\n\n/**\n * What saving a version kept on the device as a new spreadsheet says when the\n * spreadsheet was made from an Excel Macro-Enabled Workbook (`.xlsm`) and\n * Athena can't be reached: the new spreadsheet is made all the same (on the\n * device, and it syncs when Athena is back), but without the macros, which only\n * Athena can keep with a new spreadsheet (they travel with the workbook and\n * never run). The kept version stays on the device, so saving it again with a\n * connection keeps them.\n */\nexport function savedAsNewWithoutMacros({ device }: { device: DeviceNoun }): string {\n return `The new spreadsheet was made without the macros: only Athena can keep them in a new spreadsheet, and it can’t be reached. The kept version stays on ${device}: save it as a new spreadsheet again when you’re back online to keep them.`;\n}\n\n/**\n * Whether an item opens without a connection on this device, for lists.\n * `canKeep` says whether opening it online would keep a copy here (the\n * deployment allows device copies and the item's type is one that is kept):\n * only then is the person told to open it once.\n */\nexport function offlineAvailabilityCopy({\n available,\n canKeep,\n device,\n}: {\n available: boolean;\n canKeep: boolean;\n device: DeviceNoun;\n}): LabelCopy {\n if (available) {\n return { label: 'Available offline', detail: `Opens without a connection on ${device}.` };\n }\n return canKeep\n ? {\n label: 'Not available offline',\n detail: `Open it once while online to keep it on ${device}.`,\n }\n : { label: 'Not available offline', detail: 'This opens only with a connection.' };\n}\n\n/**\n * The one-time explainer shown the first time the person goes offline, on a\n * surface that keeps work there (`keeping` `device`). A surface that keeps\n * nothing (`none`: the Chrome side panel, the Office task pane) shows no\n * explainer; its status line already says all that holds.\n */\nexport function firstOfflineExplainer({ device }: { device: DeviceNoun }): StatusCopy & {\n action: string;\n} {\n return {\n title: 'You can keep working offline',\n detail: `Documents you’ve opened recently keep working on ${device}. Your changes sync when you’re back online.`,\n action: 'Got it',\n };\n}\n\n/**\n * The summary heading of the one \"Waiting to sync\" list, from what waits on\n * the device; `null` when nothing does.\n */\nexport function waitingSummary({\n changes = 0,\n messages = 0,\n recordings = 0,\n}: {\n changes?: number;\n messages?: number;\n recordings?: number;\n}): string | null {\n const parts = [\n changes > 0 ? plural(changes, 'change', 'changes') : null,\n messages > 0 ? plural(messages, 'message', 'messages') : null,\n recordings > 0 ? plural(recordings, 'recording', 'recordings') : null,\n ].filter((part): part is string => part !== null);\n return parts.length === 0 ? null : `Waiting to sync: ${parts.join(', ')}`;\n}\n\n/**\n * Where one item in the \"Waiting to sync\" list stands: a change, a message or a\n * recording made while Athena couldn't be reached.\n */\nexport type WaitingItemState =\n /** Saved on the device; it syncs when Athena is back. */\n | 'waiting'\n /** Being sent to Athena now. */\n | 'syncing'\n /**\n * Kept only while this tab or app stays open: it couldn't be saved on the\n * device (the organization keeps nothing on devices, or storage failed).\n */\n | 'in-memory'\n /**\n * Saved on the device in something that isn't open (a document closed before\n * its edits synced, one made offline): it syncs when it is next opened with a\n * connection (opening it offline sends nothing).\n */\n | 'when-opened'\n /** Made in another workspace: it syncs once that workspace is open again, with a connection. */\n | 'other-workspace'\n /** It doesn't go on its own: the person chooses (a message that waited more than a day). */\n | 'needs-you'\n /** Athena didn't take it: the app adds why, and what the person can do about it. */\n | 'not-synced';\n\n/** The state of one item in the \"Waiting to sync\" list, and the line that explains it. */\nexport function waitingItemCopy({\n state,\n device,\n}: {\n state: WaitingItemState;\n device: DeviceNoun;\n}): LabelCopy {\n switch (state) {\n case 'waiting':\n return {\n label: 'Waiting to sync',\n detail: `Saved on ${device}. It syncs when Athena is back.`,\n };\n case 'syncing':\n return { label: 'Syncing…', detail: `Sending what was saved on ${device}.` };\n case 'in-memory':\n return {\n label: 'Not saved yet',\n detail: `Kept only until you close ${openContainer(device)}. It syncs when Athena is back.`,\n };\n case 'when-opened':\n return {\n label: 'Not synced yet',\n detail: `Saved on ${device}. It syncs when you next open it with a connection.`,\n };\n case 'other-workspace':\n return {\n label: 'Waiting for its workspace',\n detail: 'It syncs once you’re in the workspace it was made in, with a connection.',\n };\n case 'needs-you':\n return { label: 'Waiting for you', detail: 'Open it to choose what happens to it.' };\n case 'not-synced':\n return { label: 'Couldn’t sync', detail: 'Athena didn’t take it.' };\n }\n}\n\n/** The headings of the \"Waiting to sync\" list, one per kind of item. */\nexport const WAITING_SECTION_TITLES = {\n changes: 'Changes',\n messages: 'Messages',\n recordings: 'Recordings',\n} as const;\n\n/**\n * What the person can do with an item Athena didn't take (`not-synced`) in the\n * \"Waiting to sync\" list: send it again, or delete the copy on the device. That\n * copy is the item's only one, so the app asks first (`deleteFromDeviceConfirm`)\n * and never deletes it as a side effect of anything else.\n */\nexport function notSyncedActions({ device }: { device: DeviceNoun }): {\n retry: string;\n remove: string;\n} {\n return { retry: 'Try again', remove: `Delete from ${device}` };\n}\n\n/** One of `notSyncedActions`, or showing where the item is kept on the device. */\nexport type NotSyncedAction = 'retry' | 'reveal' | 'remove';\n\n/**\n * What the person hears when the device couldn't do what they asked for an\n * item Athena didn't take (its files, not Athena, failed).\n */\nexport function notSyncedActionFailed({\n action,\n device,\n}: {\n action: NotSyncedAction;\n device: DeviceNoun;\n}): string {\n switch (action) {\n case 'retry':\n return 'Couldn’t send it again. Try again in a moment.';\n case 'reveal':\n return `Couldn’t show where it’s kept on ${device}.`;\n case 'remove':\n return `Couldn’t delete it from ${device}.`;\n }\n}\n\n/**\n * The question before deleting such an item's copy on the device, naming the\n * item: the destructive choice is never the default.\n */\nexport function deleteFromDeviceConfirm({\n name,\n device,\n}: {\n name: string;\n device: DeviceNoun;\n}): StatusCopy & { confirm: string; cancel: string } {\n return {\n title: `Delete “${name}” from ${device}?`,\n detail: 'Athena didn’t take it, so this is its only copy. Deleting it can’t be undone.',\n confirm: 'Delete',\n cancel: 'Cancel',\n };\n}\n"],"names":[],"mappings":"AA6QO,MAAM,6BAA6B;AAsF1C,MAAM,kBAAkB;AAMjB,MAAM,mBAA8B;AAAA,EACzC,OAAO;AAAA,EACP,QAAQ,sBAAsB,eAAe;AAC/C;AAaO,SAAS,0BAA0B;AAAA,EACxC;AAAA,EACA;AACF,GAGW;AACT,QAAM,YAAY,aAA4C,QAAQ,kBAAkB,eAAe;AACvG,SAAO,eACH,iEAAiE,SAAS,KAC1E;AACN;AAGA,SAAS,WAAW,WAA4B;AAC9C,SAAO,YAAY,6BAA6B;AAClD;AAQO,SAAS,+BAA+B;AAAA,EAC7C,YAAY;AACd,IAEI,IAAY;AACd,SAAO,sDAAsD,WAAW,SAAS,CAAC,kBAChF,YAAY,OAAO,gBACrB;AACF;AAOO,SAAS,6BAA6B;AAAA,EAC3C,YAAY;AACd,IAEI,IAAY;AACd,SAAO,gFAAgF;AAAA,IACrF;AAAA,EAAA,CACD;AACH;"}
|
|
@@ -10,7 +10,7 @@ export { pickThreadConfigCustom, type ThreadConfigPatch, } from "./config-mutati
|
|
|
10
10
|
export { type ConversationIndexEntry, conversationIndexFingerprint, useConversationIndex, } from "./conversation-index";
|
|
11
11
|
export { type ConversationListPage, type ConversationSummary, mergeConversationIndexIntoPage, } from "./conversation-list";
|
|
12
12
|
export { isLegacyThreadReadOnlyRejection, isStatewireChannelTeardown, isStatewireRewindAnchorRejection, statewireRejectionDetailMessage, statewireRejectionPayload, } from "./errors";
|
|
13
|
-
export { asHitlApproval, findAbandonedInterrupt, HITL_PENDING_RESUME_GRACE_MS, type HitlApprovalPayload, hitlResumeApprove, hitlResumeReject, interruptResumeDecline, isApprovalCardInterrupt, pendingActionRequests, readAbandonedInputRequests, readInterruptMessage, toolCallIdsAwaitingApproval, useApprovalResumeLock, } from "./interrupts";
|
|
13
|
+
export { asHitlApproval, findAbandonedInterrupt, HITL_PENDING_RESUME_GRACE_MS, type HitlApprovalPayload, hitlResumeApprove, hitlResumeReject, interruptMirrorPayload, interruptResumeDecline, isApprovalCardInterrupt, pendingActionRequests, readAbandonedInputRequests, readInterruptMessage, readInterruptMirror, readQueuedLaneDecisions, toolCallIdsAwaitingApproval, useApprovalResumeLock, } from "./interrupts";
|
|
14
14
|
export { type DeepAgentMessageWindowDeclaration } from "./message-window";
|
|
15
15
|
export { ATHENA_RUN_MARKER_KEY, isQueuedLaneMessage, } from "./messages";
|
|
16
16
|
export { isDeepAgentHostState } from "./protocol";
|
|
@@ -20,8 +20,9 @@ export { createStatewireDiagnosticsRecorder } from "./statewire-diagnostics";
|
|
|
20
20
|
export { isDeepAgentStateHydrated } from "./statewire-state";
|
|
21
21
|
export { DEEP_AGENT_STATEWIRE_PROTOCOL } from "./statewire-transport";
|
|
22
22
|
export { hideUnterminatedMarkdownLink } from "./streaming-markdown";
|
|
23
|
-
export { nestedMessagesFromArtifact } from "./subagents";
|
|
23
|
+
export { nestedMessagesFromArtifact, SUBAGENT_AWAITING_APPROVAL_LABEL, subagentApprovalNoteLabel, type SubagentLaneApprovalWait, turnLaneApprovalWait, } from "./subagents";
|
|
24
24
|
export { hasStatewireThreadExtras } from "./thread-extras";
|
|
25
|
+
export { salientToolCallDetail } from "./tool-names";
|
|
25
26
|
export { createDeepAgentThreadSnapshotCache, type DeepAgentThreadSnapshotCache, } from "./thread-snapshot-cache";
|
|
26
27
|
export { useDeepAgentThreadStatus } from "./thread-status";
|
|
27
28
|
export { describeToolArgs, formatToolName, getCategoryEmoji, getToolDisplayMeta, knownToolNames, } from "./tool-metadata";
|
|
@@ -34,7 +35,7 @@ export { useDeepAgentMessageEditSubmit } from "./use-deep-agent-edit-submit";
|
|
|
34
35
|
export { type DeepAgentThreadElement, logUnhandledStatewireError, useDeepAgentRuntime, } from "./use-deep-agent-runtime";
|
|
35
36
|
export { describeVoiceFailure, describeVoiceLiveElsewhere, type DeepAgentVoiceCallMove, type DeepAgentVoiceSurface, playVoiceMoveEarcon, useDeepAgentVoice, type UseDeepAgentVoiceReturn, useVoiceElapsed, useVoiceMovedNotice, VOICE_CONTINUE_BY_VOICE_LABEL, VOICE_MOVE_CALL_HERE_LABEL, VOICE_MOVE_EARCON_MS, VOICE_MOVE_HERE_ACTION, voiceMoveEarconDataUri, } from "./use-deep-agent-voice";
|
|
36
37
|
export { useDeepAgentWindowSync } from "./use-deep-agent-window-sync";
|
|
37
|
-
export { describeVoiceUpdateToolCall, isSpokenTurnTextPart, isVoiceDelegationMessage, VOICE_LIVE_TYPED_HINT, VOICE_SOURCE_SPOKEN, VOICE_UPDATE_TOOL_NAME, voiceProvenanceLabel, voiceUpdateStatusLabel, } from "./voice-presentation";
|
|
38
|
+
export { describeVoiceUpdateToolCall, isSpokenTurnTextPart, isVoiceDelegationMessage, VOICE_LIVE_TYPED_HINT, VOICE_SOURCE_SPOKEN, VOICE_UPDATE_TOOL_NAME, voiceDelegationEchoesRequest, voiceProvenanceLabel, voiceUpdateStatusLabel, } from "./voice-presentation";
|
|
38
39
|
export { CALL_RECAP_APPROVAL_LABEL, CALL_RECAP_TITLE, type CallRecap, type CallRecapApproval, type CallRecapAsset, type CallRecapItem, type CallRecapLink, type CallRecapMessage, type CallRecapOpenToolCall, type CallRecapPart, callRecapAfter, callRecapHasItems, callRecapItems, isCallUserMessage, mayHaveLiveCall, VOICE_TYPED_ID_PREFIX, } from "./call-recap";
|
|
39
40
|
export { type ResumeExtras } from "./interrupts";
|
|
40
41
|
export { resumeValueApproves } from "./interrupts";
|
|
@@ -201,7 +201,29 @@ function createSubagentTranscriptConversionCache(options = {}) {
|
|
|
201
201
|
}
|
|
202
202
|
};
|
|
203
203
|
}
|
|
204
|
-
react.createExternalMessageConversionCache();
|
|
204
|
+
const inEvalTranscriptCache = react.createExternalMessageConversionCache();
|
|
205
|
+
const IN_EVAL_TRANSCRIPT_METADATA = {};
|
|
206
|
+
const inEvalTranscripts = /* @__PURE__ */ new WeakMap();
|
|
207
|
+
const NO_MESSAGES = [];
|
|
208
|
+
function convertSubagentTranscript(transcript, running) {
|
|
209
|
+
const cached = inEvalTranscripts.get(transcript);
|
|
210
|
+
if (cached !== void 0 && cached.running === running) return cached.messages;
|
|
211
|
+
let messages$1 = NO_MESSAGES;
|
|
212
|
+
try {
|
|
213
|
+
messages$1 = react.convertExternalMessages(
|
|
214
|
+
transcriptSources(transcript),
|
|
215
|
+
messages.convertDeepAgentLangChainMessage,
|
|
216
|
+
running,
|
|
217
|
+
IN_EVAL_TRANSCRIPT_METADATA,
|
|
218
|
+
inEvalTranscriptCache
|
|
219
|
+
).map(messages.markPreliminaryToolResults);
|
|
220
|
+
} catch (error) {
|
|
221
|
+
reportUnconvertibleTranscript(transcript, error);
|
|
222
|
+
}
|
|
223
|
+
inEvalTranscripts.set(transcript, { running, messages: messages$1 });
|
|
224
|
+
return messages$1;
|
|
225
|
+
}
|
|
226
|
+
exports.convertSubagentTranscript = convertSubagentTranscript;
|
|
205
227
|
exports.createSubagentLaneConversion = createSubagentLaneConversion;
|
|
206
228
|
exports.createSubagentTranscriptConversionCache = createSubagentTranscriptConversionCache;
|
|
207
229
|
exports.withLaneReport = withLaneReport;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"subagent-transcripts.cjs","sources":["../../../../agent-runtime/src/subagent-transcripts.ts"],"sourcesContent":["/**\n * A subagent's transcript as assistant-ui's native nested thread.\n *\n * The backend nests a `task` subagent's transcript in its card's ToolMessage,\n * at `artifact.subgraph_state.messages`: the newest steps on the running\n * placeholder, the whole transcript on the real result. The conversion cache\n * built here converts that transcript through the thread's own pipeline and\n * sets it as the `task` tool-call part's `messages`, assistant-ui's channel for\n * a tool call's nested conversation. `ReadonlyThreadProvider` (the provider\n * under `MessagePartPrimitive.Messages`) renders it as a read-only thread under\n * the parent's tool UIs, and `thread.tasks` derives child work from it.\n *\n * Hosts opt in (`useDeepAgentThread({ createConversionCache })`). It builds on\n * the shared conversion memory from outside, so the shared converter, which\n * ships in every surface's startup bundle (the Office pane's startup and total\n * budgets have no room to spare), stays as it is, and a surface that draws no\n * subagent transcript pays nothing for it.\n */\nimport type {\n ThreadAssistantMessagePart,\n ThreadMessage,\n ToolCallMessagePart,\n} from '@assistant-ui/core';\nimport {\n convertExternalMessages,\n createExternalMessageConversionCache,\n} from '@assistant-ui/core/react';\nimport type { LangChainMessage } from '@assistant-ui/react-langgraph';\nimport { DEEP_AGENT_REPEATS_RESULT_KEY } from './generated/runconfig-contract';\nimport { getMessageText } from './message-text';\nimport {\n convertDeepAgentLangChainMessage,\n createDeepAgentMessageConversionCache,\n type DeepAgentConversionMemory,\n type DeepAgentMessageConversionOptions,\n markPreliminaryToolResults,\n sanitizeLangChainMessages,\n} from './messages';\nimport { TASK_TOOL_NAME } from './tool-names';\n\n/** The transcript nested on a `task` card's artifact, or `undefined` when it carries none. */\nfunction laneTranscriptOf(artifact: unknown): readonly LangChainMessage[] | undefined {\n if (typeof artifact !== 'object' || artifact === null) return undefined;\n const state: unknown = (artifact as { subgraph_state?: unknown }).subgraph_state;\n if (typeof state !== 'object' || state === null) return undefined;\n const messages: unknown = (state as { messages?: unknown }).messages;\n return Array.isArray(messages) ? (messages as LangChainMessage[]) : undefined;\n}\n\nconst restoredTranscripts = new WeakMap<\n readonly LangChainMessage[],\n { readonly content: unknown; readonly transcript: readonly LangChainMessage[] }\n>();\n\n/**\n * A card's nested transcript with its report restored. On a cut finished card\n * the last step can ride with empty content and `DEEP_AGENT_REPEATS_RESULT_KEY`:\n * its text is the subagent's report, the card's own `content`, which the card\n * carries anyway. The step gets that content back. Any other transcript comes\n * back as it is. Memoized per transcript, so a lane's conversion sees one array.\n */\nexport function withRepeatedResult(\n transcript: readonly LangChainMessage[],\n content: unknown\n): readonly LangChainMessage[] {\n const last = transcript[transcript.length - 1];\n if (\n last === undefined ||\n (last as Record<string, unknown>)[DEEP_AGENT_REPEATS_RESULT_KEY] !== true ||\n (typeof content !== 'string' && !Array.isArray(content))\n ) {\n return transcript;\n }\n const cached = restoredTranscripts.get(transcript);\n if (cached !== undefined && cached.content === content) return cached.transcript;\n const restored = [...transcript.slice(0, -1), { ...last, content } as LangChainMessage];\n restoredTranscripts.set(transcript, { content, transcript: restored });\n return restored;\n}\n\ntype AssistantLangChainMessage = Extract<LangChainMessage, { type: 'ai' }>;\n\nconst convertibleChunkTurns = new WeakMap<LangChainMessage, LangChainMessage>();\n\n/**\n * A nested assistant turn stored under the chunk class name (the legacy\n * assistant-stream fold keeps `AIMessageChunk`) as the `ai` message it is: the\n * shipped converter maps only `ai`, and would drop the turn.\n */\nfunction asConvertibleTurn(message: LangChainMessage): LangChainMessage {\n const type: string = message.type;\n if (type !== 'AIMessageChunk') return message;\n const cached = convertibleChunkTurns.get(message);\n if (cached !== undefined) return cached;\n const turn: LangChainMessage = { ...(message as AssistantLangChainMessage), type: 'ai' };\n convertibleChunkTurns.set(message, turn);\n return turn;\n}\n\n/** A lane step as the assistant turn it is when it calls nothing; `null` for any other step. */\nfunction answeringTurn(step: LangChainMessage): AssistantLangChainMessage | null {\n const turn = asConvertibleTurn(step);\n return turn.type === 'ai' && (turn.tool_calls?.length ?? 0) === 0 ? turn : null;\n}\n\nconst isTextBlock = (block: unknown): block is { type: 'text'; text?: unknown } =>\n typeof block === 'object' && block !== null && 'type' in block && block.type === 'text';\n\n/** The text a step's content shows: the string, or its text blocks joined. */\nfunction shownText(content: unknown): string {\n if (typeof content === 'string') return content;\n if (!Array.isArray(content)) return '';\n let text = '';\n for (const block of content) {\n if (isTextBlock(block) && typeof block.text === 'string') text += block.text;\n }\n return text;\n}\n\n/** A turn that calls nothing and shows nothing: no text, and no block of another kind. */\nfunction blankTurn(step: LangChainMessage): AssistantLangChainMessage | null {\n const turn = answeringTurn(step);\n if (turn === null || shownText(turn.content).trim() !== '') return null;\n return typeof turn.content === 'string' || turn.content.every(isTextBlock) ? turn : null;\n}\n\nconst reportedLanes = new WeakMap<\n readonly LangChainMessage[],\n { readonly report: string; readonly transcript: readonly LangChainMessage[] }\n>();\n\n/**\n * A finished lane's transcript, closed by its report. A `task` card's content\n * is its subagent's report, the text of the lane's last turn. A transcript\n * that ends on that turn (an assistant step that calls nothing and says\n * something, which {@link withRepeatedResult} restores when it rides marked)\n * comes back as it is, even when blank steps follow it. A lane stored without\n * it shows the report as its closing answer: a lane whose last step is a blank\n * assistant step (lanes that finished before the host marked a repeated\n * report) gets the report in that step, its id and stamps kept, and a lane\n * that ends on a tool result or a turn that calls tools gets a closing step\n * after its last one. An empty report or transcript changes nothing. For\n * display only: the wire and the stored lane are untouched. Memoized per\n * transcript, so a lane's conversion sees one array.\n */\nexport function withLaneReport(\n transcript: readonly LangChainMessage[],\n report: string | undefined\n): readonly LangChainMessage[] {\n if (report === undefined || report.trim() === '' || transcript.length === 0) return transcript;\n // The lane's closing turn: its last step past any blank ones.\n let at = transcript.length - 1;\n let closing = transcript[at];\n while (closing !== undefined && blankTurn(closing) !== null) {\n at -= 1;\n closing = transcript[at];\n }\n const answered =\n closing !== undefined &&\n answeringTurn(closing) !== null &&\n shownText(closing.content).trim() !== '';\n if (answered) return transcript;\n const cached = reportedLanes.get(transcript);\n if (cached !== undefined && cached.report === report) return cached.transcript;\n const last = transcript[transcript.length - 1];\n const blank = last === undefined ? null : blankTurn(last);\n const closed: readonly LangChainMessage[] =\n blank !== null\n ? [...transcript.slice(0, -1), { ...blank, content: report }]\n : [...transcript, { id: `${last?.id ?? 'lane'}:report`, type: 'ai', content: report }];\n reportedLanes.set(transcript, { report, transcript: closed });\n return closed;\n}\n\n/** A nested transcript as the thread's own pipeline reads it (see {@link sanitizeLangChainMessages}). */\nfunction transcriptSources(transcript: readonly LangChainMessage[]): LangChainMessage[] {\n return sanitizeLangChainMessages(transcript.map(asConvertibleTurn));\n}\n\nconst reportedTranscripts = new WeakSet<readonly LangChainMessage[]>();\n\n/**\n * A nested transcript the shipped converter refused (it throws on, e.g., a\n * result whose tool name differs from its call's): reported once, and its card\n * goes without a nested thread instead of taking the whole thread down.\n */\nfunction reportUnconvertibleTranscript(\n transcript: readonly LangChainMessage[],\n error: unknown\n): void {\n if (reportedTranscripts.has(transcript)) return;\n reportedTranscripts.add(transcript);\n console.warn('[agent-runtime] a subagent transcript could not be converted', error);\n}\n\n/** Whether two values the wire carries (JSON: no functions, no cycles) are equal. */\nfunction sameJson(a: unknown, b: unknown): boolean {\n if (Object.is(a, b)) return true;\n if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null) return false;\n if (Array.isArray(a) || Array.isArray(b)) {\n return (\n Array.isArray(a) &&\n Array.isArray(b) &&\n a.length === b.length &&\n a.every((item, index) => sameJson(item, b[index]))\n );\n }\n const left = a as Record<string, unknown>;\n const right = b as Record<string, unknown>;\n const keys = Object.keys(left);\n return (\n keys.length === Object.keys(right).length &&\n keys.every(\n (key) =>\n Object.prototype.hasOwnProperty.call(right, key) && sameJson(left[key], right[key])\n )\n );\n}\n\n/**\n * Keeps a lane's unchanged steps as the objects it already converted. The host\n * replaces a card's whole document whenever the card changes, so every step it\n * carries arrives as a new object, and the conversion memory, which tells the\n * steps a turn continues by object, would see a new turn each time the card's\n * cut drops its oldest step and the turn's first step changes. A step whose id\n * and content are unchanged is the object held for that id. O(the steps' size).\n */\nfunction steadyStepObjects(): (\n transcript: readonly LangChainMessage[]\n) => readonly LangChainMessage[] {\n let held = new Map<string, LangChainMessage>();\n return (transcript) => {\n const next = new Map<string, LangChainMessage>();\n let kept = false;\n const steadied = transcript.map((entry) => {\n const id = entry.id;\n if (typeof id !== 'string') return entry;\n const before = held.get(id);\n const step = before !== undefined && sameJson(before, entry) ? before : entry;\n next.set(id, step);\n if (step !== entry) kept = true;\n return step;\n });\n held = next;\n return kept ? steadied : transcript;\n };\n}\n\n/** One lane's transcript, converted: `undefined` when the converter refused it. */\nexport type SubagentLaneConversion = (\n transcript: readonly LangChainMessage[],\n running: boolean\n) => readonly ThreadMessage[] | undefined;\n\n/**\n * One `task` lane's own conversion memory, for whatever converts that lane:\n * its unchanged steps keep their converted objects while it streams (a step the\n * wire sends again unchanged included, see {@link steadyStepObjects}), and its\n * turn keeps its row key (`deepAgentRowKey`) and part keys when the turn's head\n * moves — the oldest steps of a cut card falling off as the lane runs, or older\n * steps a transcript drawer pages in. A surface that keeps its disclosures under\n * that key keeps them open through both. A transcript array converted last time\n * is not converted again.\n */\nexport function createSubagentLaneConversion(): SubagentLaneConversion {\n const memory = createSubagentTranscriptConversionCache();\n const steady = steadyStepObjects();\n let last: {\n readonly transcript: readonly LangChainMessage[];\n readonly running: boolean;\n readonly messages: readonly ThreadMessage[] | undefined;\n } | null = null;\n return (transcript, running) => {\n if (last !== null && last.transcript === transcript && last.running === running) {\n return last.messages;\n }\n let messages: readonly ThreadMessage[] | undefined;\n try {\n messages = memory.convert(transcriptSources(steady(transcript)), running);\n } catch (error) {\n reportUnconvertibleTranscript(transcript, error);\n }\n last = { transcript, running, messages };\n return messages;\n };\n}\n\n/**\n * Sets each `task` part of a pass's converted rows to carry its nested\n * transcript as `messages`, for one attach. Each lane has its own conversion\n * memory, so its unchanged steps keep their converted objects while it\n * streams, as the thread's own messages do, and a lane whose transcript array\n * did not change is not converted again. A row, and a part whose lane did not\n * change, keep their identity: the same row always comes back as the same\n * object. A lane's memory lives only while a row of the pass holds its `task`\n * call: a lane a rewind removes, a branch switch hides or the window leaves\n * behind is let go with its transcript, and converted afresh if it returns.\n */\nfunction subagentTranscriptAttacher(): (rows: readonly ThreadMessage[]) => ThreadMessage[] {\n const lanes = new Map<string, SubagentLaneConversion>();\n const attachedRows = new WeakMap<ThreadMessage, ThreadMessage>();\n const attachedParts = new WeakMap<\n ToolCallMessagePart,\n { readonly messages: readonly ThreadMessage[]; readonly part: ToolCallMessagePart }\n >();\n\n const laneMessages = (\n toolCallId: string,\n transcript: readonly LangChainMessage[],\n running: boolean\n ): readonly ThreadMessage[] | undefined => {\n let lane = lanes.get(toolCallId);\n if (lane === undefined) {\n lane = createSubagentLaneConversion();\n lanes.set(toolCallId, lane);\n }\n return lane(transcript, running);\n };\n\n const attach = (row: ThreadMessage): ThreadMessage => {\n if (row.role !== 'assistant') return row;\n const cached = attachedRows.get(row);\n if (cached !== undefined) return cached;\n let content: ThreadAssistantMessagePart[] | null = null;\n for (const [index, part] of row.content.entries()) {\n if (part.type !== 'tool-call' || part.toolName !== TASK_TOOL_NAME) continue;\n const nested = laneTranscriptOf(part.artifact);\n if (nested === undefined) continue;\n // A running lane's placeholder is preliminary: its newest nested turn streams.\n const running = part.isPreliminary === true;\n const restored = withRepeatedResult(nested, part.result);\n const transcript =\n running || part.isError === true\n ? restored\n : withLaneReport(restored, getMessageText({ content: part.result }));\n const messages = laneMessages(part.toolCallId, transcript, running);\n if (messages === undefined) continue;\n const kept = attachedParts.get(part);\n const attached =\n kept !== undefined && kept.messages === messages ? kept.part : { ...part, messages };\n attachedParts.set(part, { messages, part: attached });\n content ??= [...row.content];\n content[index] = attached;\n }\n const attachedRow: ThreadMessage = content === null ? row : { ...row, content };\n attachedRows.set(row, attachedRow);\n return attachedRow;\n };\n\n return (rows) => {\n const attached = rows.map(attach);\n if (lanes.size === 0) return attached;\n const held = new Set<string>();\n for (const row of rows) {\n if (row.role !== 'assistant') continue;\n for (const part of row.content) {\n if (part.type !== 'tool-call' || part.toolName !== TASK_TOOL_NAME) continue;\n held.add(part.toolCallId);\n }\n }\n for (const toolCallId of lanes.keys()) {\n if (!held.has(toolCallId)) lanes.delete(toolCallId);\n }\n return attached;\n };\n}\n\n/**\n * A conversion memory for one attach (see\n * `useDeepAgentThread({ createConversionCache })`): the shared one's rows, with\n * each `task` tool-call part carrying its subagent's converted transcript as\n * `messages`. A pass that changes no row hands back the previous list itself.\n */\nexport function createSubagentTranscriptConversionCache(\n options: DeepAgentMessageConversionOptions = {}\n): DeepAgentConversionMemory {\n const thread = createDeepAgentMessageConversionCache(options);\n const attach = subagentTranscriptAttacher();\n let previousRows: ThreadMessage[] | null = null;\n let previous: ThreadMessage[] = [];\n return {\n convert(messages, isRunning) {\n const rows = thread.convert(messages, isRunning);\n if (rows === previousRows) return previous;\n previousRows = rows;\n const next = attach(rows);\n const unchanged =\n next.length === previous.length && next.every((row, index) => row === previous[index]);\n if (!unchanged) previous = next;\n return previous;\n },\n };\n}\n\n/**\n * Shared by every in-eval transcript: entries are keyed by source message, so\n * none outlives the wire state holding it and threads cannot collide.\n */\nconst inEvalTranscriptCache = createExternalMessageConversionCache();\nconst IN_EVAL_TRANSCRIPT_METADATA = {};\nconst inEvalTranscripts = new WeakMap<\n readonly LangChainMessage[],\n { readonly running: boolean; readonly messages: readonly ThreadMessage[] }\n>();\n\nconst NO_MESSAGES: readonly ThreadMessage[] = [];\n\n/**\n * An in-eval dispatch's live transcript (a `ptc_subagents` entry's `messages`)\n * as a nested thread, converted like a `task` lane's. Memoized per transcript\n * array; a step the lane did not change keeps its converted object. A\n * transcript the converter refuses reads as empty.\n */\nexport function convertSubagentTranscript(\n transcript: readonly LangChainMessage[],\n running: boolean\n): readonly ThreadMessage[] {\n const cached = inEvalTranscripts.get(transcript);\n if (cached !== undefined && cached.running === running) return cached.messages;\n let messages: readonly ThreadMessage[] = NO_MESSAGES;\n try {\n messages = convertExternalMessages(\n transcriptSources(transcript),\n convertDeepAgentLangChainMessage,\n running,\n IN_EVAL_TRANSCRIPT_METADATA,\n inEvalTranscriptCache\n ).map(markPreliminaryToolResults);\n } catch (error) {\n reportUnconvertibleTranscript(transcript, error);\n }\n inEvalTranscripts.set(transcript, { running, messages });\n return messages;\n}\n"],"names":["messages","DEEP_AGENT_REPEATS_RESULT_KEY","sanitizeLangChainMessages","TASK_TOOL_NAME","getMessageText","createDeepAgentMessageConversionCache","createExternalMessageConversionCache"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"subagent-transcripts.cjs","sources":["../../../../agent-runtime/src/subagent-transcripts.ts"],"sourcesContent":["/**\n * A subagent's transcript as assistant-ui's native nested thread.\n *\n * The backend nests a `task` subagent's transcript in its card's ToolMessage,\n * at `artifact.subgraph_state.messages`: the newest steps on the running\n * placeholder, the whole transcript on the real result. The conversion cache\n * built here converts that transcript through the thread's own pipeline and\n * sets it as the `task` tool-call part's `messages`, assistant-ui's channel for\n * a tool call's nested conversation. `ReadonlyThreadProvider` (the provider\n * under `MessagePartPrimitive.Messages`) renders it as a read-only thread under\n * the parent's tool UIs, and `thread.tasks` derives child work from it.\n *\n * Hosts opt in (`useDeepAgentThread({ createConversionCache })`). It builds on\n * the shared conversion memory from outside, so the shared converter, which\n * ships in every surface's startup bundle (the Office pane's startup and total\n * budgets have no room to spare), stays as it is, and a surface that draws no\n * subagent transcript pays nothing for it.\n */\nimport type {\n ThreadAssistantMessagePart,\n ThreadMessage,\n ToolCallMessagePart,\n} from '@assistant-ui/core';\nimport {\n convertExternalMessages,\n createExternalMessageConversionCache,\n} from '@assistant-ui/core/react';\nimport type { LangChainMessage } from '@assistant-ui/react-langgraph';\nimport { DEEP_AGENT_REPEATS_RESULT_KEY } from './generated/runconfig-contract';\nimport { getMessageText } from './message-text';\nimport {\n convertDeepAgentLangChainMessage,\n createDeepAgentMessageConversionCache,\n type DeepAgentConversionMemory,\n type DeepAgentMessageConversionOptions,\n markPreliminaryToolResults,\n sanitizeLangChainMessages,\n} from './messages';\nimport { TASK_TOOL_NAME } from './tool-names';\n\n/** The transcript nested on a `task` card's artifact, or `undefined` when it carries none. */\nfunction laneTranscriptOf(artifact: unknown): readonly LangChainMessage[] | undefined {\n if (typeof artifact !== 'object' || artifact === null) return undefined;\n const state: unknown = (artifact as { subgraph_state?: unknown }).subgraph_state;\n if (typeof state !== 'object' || state === null) return undefined;\n const messages: unknown = (state as { messages?: unknown }).messages;\n return Array.isArray(messages) ? (messages as LangChainMessage[]) : undefined;\n}\n\nconst restoredTranscripts = new WeakMap<\n readonly LangChainMessage[],\n { readonly content: unknown; readonly transcript: readonly LangChainMessage[] }\n>();\n\n/**\n * A card's nested transcript with its report restored. On a cut finished card\n * the last step can ride with empty content and `DEEP_AGENT_REPEATS_RESULT_KEY`:\n * its text is the subagent's report, the card's own `content`, which the card\n * carries anyway. The step gets that content back. Any other transcript comes\n * back as it is. Memoized per transcript, so a lane's conversion sees one array.\n */\nexport function withRepeatedResult(\n transcript: readonly LangChainMessage[],\n content: unknown\n): readonly LangChainMessage[] {\n const last = transcript[transcript.length - 1];\n if (\n last === undefined ||\n (last as Record<string, unknown>)[DEEP_AGENT_REPEATS_RESULT_KEY] !== true ||\n (typeof content !== 'string' && !Array.isArray(content))\n ) {\n return transcript;\n }\n const cached = restoredTranscripts.get(transcript);\n if (cached !== undefined && cached.content === content) return cached.transcript;\n const restored = [...transcript.slice(0, -1), { ...last, content } as LangChainMessage];\n restoredTranscripts.set(transcript, { content, transcript: restored });\n return restored;\n}\n\ntype AssistantLangChainMessage = Extract<LangChainMessage, { type: 'ai' }>;\n\nconst convertibleChunkTurns = new WeakMap<LangChainMessage, LangChainMessage>();\n\n/**\n * A nested assistant turn stored under the chunk class name (the legacy\n * assistant-stream fold keeps `AIMessageChunk`) as the `ai` message it is: the\n * shipped converter maps only `ai`, and would drop the turn.\n */\nfunction asConvertibleTurn(message: LangChainMessage): LangChainMessage {\n const type: string = message.type;\n if (type !== 'AIMessageChunk') return message;\n const cached = convertibleChunkTurns.get(message);\n if (cached !== undefined) return cached;\n const turn: LangChainMessage = { ...(message as AssistantLangChainMessage), type: 'ai' };\n convertibleChunkTurns.set(message, turn);\n return turn;\n}\n\n/** A lane step as the assistant turn it is when it calls nothing; `null` for any other step. */\nfunction answeringTurn(step: LangChainMessage): AssistantLangChainMessage | null {\n const turn = asConvertibleTurn(step);\n return turn.type === 'ai' && (turn.tool_calls?.length ?? 0) === 0 ? turn : null;\n}\n\nconst isTextBlock = (block: unknown): block is { type: 'text'; text?: unknown } =>\n typeof block === 'object' && block !== null && 'type' in block && block.type === 'text';\n\n/** The text a step's content shows: the string, or its text blocks joined. */\nfunction shownText(content: unknown): string {\n if (typeof content === 'string') return content;\n if (!Array.isArray(content)) return '';\n let text = '';\n for (const block of content) {\n if (isTextBlock(block) && typeof block.text === 'string') text += block.text;\n }\n return text;\n}\n\n/** A turn that calls nothing and shows nothing: no text, and no block of another kind. */\nfunction blankTurn(step: LangChainMessage): AssistantLangChainMessage | null {\n const turn = answeringTurn(step);\n if (turn === null || shownText(turn.content).trim() !== '') return null;\n return typeof turn.content === 'string' || turn.content.every(isTextBlock) ? turn : null;\n}\n\nconst reportedLanes = new WeakMap<\n readonly LangChainMessage[],\n { readonly report: string; readonly transcript: readonly LangChainMessage[] }\n>();\n\n/**\n * A finished lane's transcript, closed by its report. A `task` card's content\n * is its subagent's report, the text of the lane's last turn. A transcript\n * that ends on that turn (an assistant step that calls nothing and says\n * something, which {@link withRepeatedResult} restores when it rides marked)\n * comes back as it is, even when blank steps follow it. A lane stored without\n * it shows the report as its closing answer: a lane whose last step is a blank\n * assistant step (lanes that finished before the host marked a repeated\n * report) gets the report in that step, its id and stamps kept, and a lane\n * that ends on a tool result or a turn that calls tools gets a closing step\n * after its last one. An empty report or transcript changes nothing. For\n * display only: the wire and the stored lane are untouched. Memoized per\n * transcript, so a lane's conversion sees one array.\n */\nexport function withLaneReport(\n transcript: readonly LangChainMessage[],\n report: string | undefined\n): readonly LangChainMessage[] {\n if (report === undefined || report.trim() === '' || transcript.length === 0) return transcript;\n // The lane's closing turn: its last step past any blank ones.\n let at = transcript.length - 1;\n let closing = transcript[at];\n while (closing !== undefined && blankTurn(closing) !== null) {\n at -= 1;\n closing = transcript[at];\n }\n const answered =\n closing !== undefined &&\n answeringTurn(closing) !== null &&\n shownText(closing.content).trim() !== '';\n if (answered) return transcript;\n const cached = reportedLanes.get(transcript);\n if (cached !== undefined && cached.report === report) return cached.transcript;\n const last = transcript[transcript.length - 1];\n const blank = last === undefined ? null : blankTurn(last);\n const closed: readonly LangChainMessage[] =\n blank !== null\n ? [...transcript.slice(0, -1), { ...blank, content: report }]\n : [...transcript, { id: `${last?.id ?? 'lane'}:report`, type: 'ai', content: report }];\n reportedLanes.set(transcript, { report, transcript: closed });\n return closed;\n}\n\n/** A nested transcript as the thread's own pipeline reads it (see {@link sanitizeLangChainMessages}). */\nfunction transcriptSources(transcript: readonly LangChainMessage[]): LangChainMessage[] {\n return sanitizeLangChainMessages(transcript.map(asConvertibleTurn));\n}\n\nconst reportedTranscripts = new WeakSet<readonly LangChainMessage[]>();\n\n/**\n * A nested transcript the shipped converter refused (it throws on, e.g., a\n * result whose tool name differs from its call's): reported once, and its card\n * goes without a nested thread instead of taking the whole thread down.\n */\nfunction reportUnconvertibleTranscript(\n transcript: readonly LangChainMessage[],\n error: unknown\n): void {\n if (reportedTranscripts.has(transcript)) return;\n reportedTranscripts.add(transcript);\n console.warn('[agent-runtime] a subagent transcript could not be converted', error);\n}\n\n/** Whether two values the wire carries (JSON: no functions, no cycles) are equal. */\nfunction sameJson(a: unknown, b: unknown): boolean {\n if (Object.is(a, b)) return true;\n if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null) return false;\n if (Array.isArray(a) || Array.isArray(b)) {\n return (\n Array.isArray(a) &&\n Array.isArray(b) &&\n a.length === b.length &&\n a.every((item, index) => sameJson(item, b[index]))\n );\n }\n const left = a as Record<string, unknown>;\n const right = b as Record<string, unknown>;\n const keys = Object.keys(left);\n return (\n keys.length === Object.keys(right).length &&\n keys.every(\n (key) =>\n Object.prototype.hasOwnProperty.call(right, key) && sameJson(left[key], right[key])\n )\n );\n}\n\n/**\n * Keeps a lane's unchanged steps as the objects it already converted. The host\n * replaces a card's whole document whenever the card changes, so every step it\n * carries arrives as a new object, and the conversion memory, which tells the\n * steps a turn continues by object, would see a new turn each time the card's\n * cut drops its oldest step and the turn's first step changes. A step whose id\n * and content are unchanged is the object held for that id. O(the steps' size).\n */\nfunction steadyStepObjects(): (\n transcript: readonly LangChainMessage[]\n) => readonly LangChainMessage[] {\n let held = new Map<string, LangChainMessage>();\n return (transcript) => {\n const next = new Map<string, LangChainMessage>();\n let kept = false;\n const steadied = transcript.map((entry) => {\n const id = entry.id;\n if (typeof id !== 'string') return entry;\n const before = held.get(id);\n const step = before !== undefined && sameJson(before, entry) ? before : entry;\n next.set(id, step);\n if (step !== entry) kept = true;\n return step;\n });\n held = next;\n return kept ? steadied : transcript;\n };\n}\n\n/** One lane's transcript, converted: `undefined` when the converter refused it. */\nexport type SubagentLaneConversion = (\n transcript: readonly LangChainMessage[],\n running: boolean\n) => readonly ThreadMessage[] | undefined;\n\n/**\n * One `task` lane's own conversion memory, for whatever converts that lane:\n * its unchanged steps keep their converted objects while it streams (a step the\n * wire sends again unchanged included, see {@link steadyStepObjects}), and its\n * turn keeps its row key (`deepAgentRowKey`) and part keys when the turn's head\n * moves — the oldest steps of a cut card falling off as the lane runs, or older\n * steps a transcript drawer pages in. A surface that keeps its disclosures under\n * that key keeps them open through both. A transcript array converted last time\n * is not converted again.\n */\nexport function createSubagentLaneConversion(): SubagentLaneConversion {\n const memory = createSubagentTranscriptConversionCache();\n const steady = steadyStepObjects();\n let last: {\n readonly transcript: readonly LangChainMessage[];\n readonly running: boolean;\n readonly messages: readonly ThreadMessage[] | undefined;\n } | null = null;\n return (transcript, running) => {\n if (last !== null && last.transcript === transcript && last.running === running) {\n return last.messages;\n }\n let messages: readonly ThreadMessage[] | undefined;\n try {\n messages = memory.convert(transcriptSources(steady(transcript)), running);\n } catch (error) {\n reportUnconvertibleTranscript(transcript, error);\n }\n last = { transcript, running, messages };\n return messages;\n };\n}\n\n/**\n * Sets each `task` part of a pass's converted rows to carry its nested\n * transcript as `messages`, for one attach. Each lane has its own conversion\n * memory, so its unchanged steps keep their converted objects while it\n * streams, as the thread's own messages do, and a lane whose transcript array\n * did not change is not converted again. A row, and a part whose lane did not\n * change, keep their identity: the same row always comes back as the same\n * object. A lane's memory lives only while a row of the pass holds its `task`\n * call: a lane a rewind removes, a branch switch hides or the window leaves\n * behind is let go with its transcript, and converted afresh if it returns.\n */\nfunction subagentTranscriptAttacher(): (rows: readonly ThreadMessage[]) => ThreadMessage[] {\n const lanes = new Map<string, SubagentLaneConversion>();\n const attachedRows = new WeakMap<ThreadMessage, ThreadMessage>();\n const attachedParts = new WeakMap<\n ToolCallMessagePart,\n { readonly messages: readonly ThreadMessage[]; readonly part: ToolCallMessagePart }\n >();\n\n const laneMessages = (\n toolCallId: string,\n transcript: readonly LangChainMessage[],\n running: boolean\n ): readonly ThreadMessage[] | undefined => {\n let lane = lanes.get(toolCallId);\n if (lane === undefined) {\n lane = createSubagentLaneConversion();\n lanes.set(toolCallId, lane);\n }\n return lane(transcript, running);\n };\n\n const attach = (row: ThreadMessage): ThreadMessage => {\n if (row.role !== 'assistant') return row;\n const cached = attachedRows.get(row);\n if (cached !== undefined) return cached;\n let content: ThreadAssistantMessagePart[] | null = null;\n for (const [index, part] of row.content.entries()) {\n if (part.type !== 'tool-call' || part.toolName !== TASK_TOOL_NAME) continue;\n const nested = laneTranscriptOf(part.artifact);\n if (nested === undefined) continue;\n // A running lane's placeholder is preliminary: its newest nested turn streams.\n const running = part.isPreliminary === true;\n const restored = withRepeatedResult(nested, part.result);\n const transcript =\n running || part.isError === true\n ? restored\n : withLaneReport(restored, getMessageText({ content: part.result }));\n const messages = laneMessages(part.toolCallId, transcript, running);\n if (messages === undefined) continue;\n const kept = attachedParts.get(part);\n const attached =\n kept !== undefined && kept.messages === messages ? kept.part : { ...part, messages };\n attachedParts.set(part, { messages, part: attached });\n content ??= [...row.content];\n content[index] = attached;\n }\n const attachedRow: ThreadMessage = content === null ? row : { ...row, content };\n attachedRows.set(row, attachedRow);\n return attachedRow;\n };\n\n return (rows) => {\n const attached = rows.map(attach);\n if (lanes.size === 0) return attached;\n const held = new Set<string>();\n for (const row of rows) {\n if (row.role !== 'assistant') continue;\n for (const part of row.content) {\n if (part.type !== 'tool-call' || part.toolName !== TASK_TOOL_NAME) continue;\n held.add(part.toolCallId);\n }\n }\n for (const toolCallId of lanes.keys()) {\n if (!held.has(toolCallId)) lanes.delete(toolCallId);\n }\n return attached;\n };\n}\n\n/**\n * A conversion memory for one attach (see\n * `useDeepAgentThread({ createConversionCache })`): the shared one's rows, with\n * each `task` tool-call part carrying its subagent's converted transcript as\n * `messages`. A pass that changes no row hands back the previous list itself.\n */\nexport function createSubagentTranscriptConversionCache(\n options: DeepAgentMessageConversionOptions = {}\n): DeepAgentConversionMemory {\n const thread = createDeepAgentMessageConversionCache(options);\n const attach = subagentTranscriptAttacher();\n let previousRows: ThreadMessage[] | null = null;\n let previous: ThreadMessage[] = [];\n return {\n convert(messages, isRunning) {\n const rows = thread.convert(messages, isRunning);\n if (rows === previousRows) return previous;\n previousRows = rows;\n const next = attach(rows);\n const unchanged =\n next.length === previous.length && next.every((row, index) => row === previous[index]);\n if (!unchanged) previous = next;\n return previous;\n },\n };\n}\n\n/**\n * Shared by every in-eval transcript: entries are keyed by source message, so\n * none outlives the wire state holding it and threads cannot collide.\n */\nconst inEvalTranscriptCache = createExternalMessageConversionCache();\nconst IN_EVAL_TRANSCRIPT_METADATA = {};\nconst inEvalTranscripts = new WeakMap<\n readonly LangChainMessage[],\n { readonly running: boolean; readonly messages: readonly ThreadMessage[] }\n>();\n\nconst NO_MESSAGES: readonly ThreadMessage[] = [];\n\n/**\n * An in-eval dispatch's live transcript (a `ptc_subagents` entry's `messages`)\n * as a nested thread, converted like a `task` lane's. Memoized per transcript\n * array; a step the lane did not change keeps its converted object. A\n * transcript the converter refuses reads as empty.\n */\nexport function convertSubagentTranscript(\n transcript: readonly LangChainMessage[],\n running: boolean\n): readonly ThreadMessage[] {\n const cached = inEvalTranscripts.get(transcript);\n if (cached !== undefined && cached.running === running) return cached.messages;\n let messages: readonly ThreadMessage[] = NO_MESSAGES;\n try {\n messages = convertExternalMessages(\n transcriptSources(transcript),\n convertDeepAgentLangChainMessage,\n running,\n IN_EVAL_TRANSCRIPT_METADATA,\n inEvalTranscriptCache\n ).map(markPreliminaryToolResults);\n } catch (error) {\n reportUnconvertibleTranscript(transcript, error);\n }\n inEvalTranscripts.set(transcript, { running, messages });\n return messages;\n}\n"],"names":["messages","DEEP_AGENT_REPEATS_RESULT_KEY","sanitizeLangChainMessages","TASK_TOOL_NAME","getMessageText","createDeepAgentMessageConversionCache","createExternalMessageConversionCache","convertExternalMessages","convertDeepAgentLangChainMessage","markPreliminaryToolResults"],"mappings":"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"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { createExternalMessageConversionCache } from "@assistant-ui/core/react";
|
|
1
|
+
import { convertExternalMessages, createExternalMessageConversionCache } from "@assistant-ui/core/react";
|
|
2
2
|
import { DEEP_AGENT_REPEATS_RESULT_KEY } from "./generated/runconfig-contract.js";
|
|
3
3
|
import { getMessageText } from "./message-text.js";
|
|
4
|
-
import {
|
|
4
|
+
import { convertDeepAgentLangChainMessage, markPreliminaryToolResults, sanitizeLangChainMessages, createDeepAgentMessageConversionCache } from "./messages.js";
|
|
5
5
|
import { TASK_TOOL_NAME } from "./tool-names.js";
|
|
6
6
|
function laneTranscriptOf(artifact) {
|
|
7
7
|
if (typeof artifact !== "object" || artifact === null) return void 0;
|
|
@@ -199,8 +199,30 @@ function createSubagentTranscriptConversionCache(options = {}) {
|
|
|
199
199
|
}
|
|
200
200
|
};
|
|
201
201
|
}
|
|
202
|
-
createExternalMessageConversionCache();
|
|
202
|
+
const inEvalTranscriptCache = createExternalMessageConversionCache();
|
|
203
|
+
const IN_EVAL_TRANSCRIPT_METADATA = {};
|
|
204
|
+
const inEvalTranscripts = /* @__PURE__ */ new WeakMap();
|
|
205
|
+
const NO_MESSAGES = [];
|
|
206
|
+
function convertSubagentTranscript(transcript, running) {
|
|
207
|
+
const cached = inEvalTranscripts.get(transcript);
|
|
208
|
+
if (cached !== void 0 && cached.running === running) return cached.messages;
|
|
209
|
+
let messages = NO_MESSAGES;
|
|
210
|
+
try {
|
|
211
|
+
messages = convertExternalMessages(
|
|
212
|
+
transcriptSources(transcript),
|
|
213
|
+
convertDeepAgentLangChainMessage,
|
|
214
|
+
running,
|
|
215
|
+
IN_EVAL_TRANSCRIPT_METADATA,
|
|
216
|
+
inEvalTranscriptCache
|
|
217
|
+
).map(markPreliminaryToolResults);
|
|
218
|
+
} catch (error) {
|
|
219
|
+
reportUnconvertibleTranscript(transcript, error);
|
|
220
|
+
}
|
|
221
|
+
inEvalTranscripts.set(transcript, { running, messages });
|
|
222
|
+
return messages;
|
|
223
|
+
}
|
|
203
224
|
export {
|
|
225
|
+
convertSubagentTranscript,
|
|
204
226
|
createSubagentLaneConversion,
|
|
205
227
|
createSubagentTranscriptConversionCache,
|
|
206
228
|
withLaneReport,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"subagent-transcripts.js","sources":["../../../../agent-runtime/src/subagent-transcripts.ts"],"sourcesContent":["/**\n * A subagent's transcript as assistant-ui's native nested thread.\n *\n * The backend nests a `task` subagent's transcript in its card's ToolMessage,\n * at `artifact.subgraph_state.messages`: the newest steps on the running\n * placeholder, the whole transcript on the real result. The conversion cache\n * built here converts that transcript through the thread's own pipeline and\n * sets it as the `task` tool-call part's `messages`, assistant-ui's channel for\n * a tool call's nested conversation. `ReadonlyThreadProvider` (the provider\n * under `MessagePartPrimitive.Messages`) renders it as a read-only thread under\n * the parent's tool UIs, and `thread.tasks` derives child work from it.\n *\n * Hosts opt in (`useDeepAgentThread({ createConversionCache })`). It builds on\n * the shared conversion memory from outside, so the shared converter, which\n * ships in every surface's startup bundle (the Office pane's startup and total\n * budgets have no room to spare), stays as it is, and a surface that draws no\n * subagent transcript pays nothing for it.\n */\nimport type {\n ThreadAssistantMessagePart,\n ThreadMessage,\n ToolCallMessagePart,\n} from '@assistant-ui/core';\nimport {\n convertExternalMessages,\n createExternalMessageConversionCache,\n} from '@assistant-ui/core/react';\nimport type { LangChainMessage } from '@assistant-ui/react-langgraph';\nimport { DEEP_AGENT_REPEATS_RESULT_KEY } from './generated/runconfig-contract';\nimport { getMessageText } from './message-text';\nimport {\n convertDeepAgentLangChainMessage,\n createDeepAgentMessageConversionCache,\n type DeepAgentConversionMemory,\n type DeepAgentMessageConversionOptions,\n markPreliminaryToolResults,\n sanitizeLangChainMessages,\n} from './messages';\nimport { TASK_TOOL_NAME } from './tool-names';\n\n/** The transcript nested on a `task` card's artifact, or `undefined` when it carries none. */\nfunction laneTranscriptOf(artifact: unknown): readonly LangChainMessage[] | undefined {\n if (typeof artifact !== 'object' || artifact === null) return undefined;\n const state: unknown = (artifact as { subgraph_state?: unknown }).subgraph_state;\n if (typeof state !== 'object' || state === null) return undefined;\n const messages: unknown = (state as { messages?: unknown }).messages;\n return Array.isArray(messages) ? (messages as LangChainMessage[]) : undefined;\n}\n\nconst restoredTranscripts = new WeakMap<\n readonly LangChainMessage[],\n { readonly content: unknown; readonly transcript: readonly LangChainMessage[] }\n>();\n\n/**\n * A card's nested transcript with its report restored. On a cut finished card\n * the last step can ride with empty content and `DEEP_AGENT_REPEATS_RESULT_KEY`:\n * its text is the subagent's report, the card's own `content`, which the card\n * carries anyway. The step gets that content back. Any other transcript comes\n * back as it is. Memoized per transcript, so a lane's conversion sees one array.\n */\nexport function withRepeatedResult(\n transcript: readonly LangChainMessage[],\n content: unknown\n): readonly LangChainMessage[] {\n const last = transcript[transcript.length - 1];\n if (\n last === undefined ||\n (last as Record<string, unknown>)[DEEP_AGENT_REPEATS_RESULT_KEY] !== true ||\n (typeof content !== 'string' && !Array.isArray(content))\n ) {\n return transcript;\n }\n const cached = restoredTranscripts.get(transcript);\n if (cached !== undefined && cached.content === content) return cached.transcript;\n const restored = [...transcript.slice(0, -1), { ...last, content } as LangChainMessage];\n restoredTranscripts.set(transcript, { content, transcript: restored });\n return restored;\n}\n\ntype AssistantLangChainMessage = Extract<LangChainMessage, { type: 'ai' }>;\n\nconst convertibleChunkTurns = new WeakMap<LangChainMessage, LangChainMessage>();\n\n/**\n * A nested assistant turn stored under the chunk class name (the legacy\n * assistant-stream fold keeps `AIMessageChunk`) as the `ai` message it is: the\n * shipped converter maps only `ai`, and would drop the turn.\n */\nfunction asConvertibleTurn(message: LangChainMessage): LangChainMessage {\n const type: string = message.type;\n if (type !== 'AIMessageChunk') return message;\n const cached = convertibleChunkTurns.get(message);\n if (cached !== undefined) return cached;\n const turn: LangChainMessage = { ...(message as AssistantLangChainMessage), type: 'ai' };\n convertibleChunkTurns.set(message, turn);\n return turn;\n}\n\n/** A lane step as the assistant turn it is when it calls nothing; `null` for any other step. */\nfunction answeringTurn(step: LangChainMessage): AssistantLangChainMessage | null {\n const turn = asConvertibleTurn(step);\n return turn.type === 'ai' && (turn.tool_calls?.length ?? 0) === 0 ? turn : null;\n}\n\nconst isTextBlock = (block: unknown): block is { type: 'text'; text?: unknown } =>\n typeof block === 'object' && block !== null && 'type' in block && block.type === 'text';\n\n/** The text a step's content shows: the string, or its text blocks joined. */\nfunction shownText(content: unknown): string {\n if (typeof content === 'string') return content;\n if (!Array.isArray(content)) return '';\n let text = '';\n for (const block of content) {\n if (isTextBlock(block) && typeof block.text === 'string') text += block.text;\n }\n return text;\n}\n\n/** A turn that calls nothing and shows nothing: no text, and no block of another kind. */\nfunction blankTurn(step: LangChainMessage): AssistantLangChainMessage | null {\n const turn = answeringTurn(step);\n if (turn === null || shownText(turn.content).trim() !== '') return null;\n return typeof turn.content === 'string' || turn.content.every(isTextBlock) ? turn : null;\n}\n\nconst reportedLanes = new WeakMap<\n readonly LangChainMessage[],\n { readonly report: string; readonly transcript: readonly LangChainMessage[] }\n>();\n\n/**\n * A finished lane's transcript, closed by its report. A `task` card's content\n * is its subagent's report, the text of the lane's last turn. A transcript\n * that ends on that turn (an assistant step that calls nothing and says\n * something, which {@link withRepeatedResult} restores when it rides marked)\n * comes back as it is, even when blank steps follow it. A lane stored without\n * it shows the report as its closing answer: a lane whose last step is a blank\n * assistant step (lanes that finished before the host marked a repeated\n * report) gets the report in that step, its id and stamps kept, and a lane\n * that ends on a tool result or a turn that calls tools gets a closing step\n * after its last one. An empty report or transcript changes nothing. For\n * display only: the wire and the stored lane are untouched. Memoized per\n * transcript, so a lane's conversion sees one array.\n */\nexport function withLaneReport(\n transcript: readonly LangChainMessage[],\n report: string | undefined\n): readonly LangChainMessage[] {\n if (report === undefined || report.trim() === '' || transcript.length === 0) return transcript;\n // The lane's closing turn: its last step past any blank ones.\n let at = transcript.length - 1;\n let closing = transcript[at];\n while (closing !== undefined && blankTurn(closing) !== null) {\n at -= 1;\n closing = transcript[at];\n }\n const answered =\n closing !== undefined &&\n answeringTurn(closing) !== null &&\n shownText(closing.content).trim() !== '';\n if (answered) return transcript;\n const cached = reportedLanes.get(transcript);\n if (cached !== undefined && cached.report === report) return cached.transcript;\n const last = transcript[transcript.length - 1];\n const blank = last === undefined ? null : blankTurn(last);\n const closed: readonly LangChainMessage[] =\n blank !== null\n ? [...transcript.slice(0, -1), { ...blank, content: report }]\n : [...transcript, { id: `${last?.id ?? 'lane'}:report`, type: 'ai', content: report }];\n reportedLanes.set(transcript, { report, transcript: closed });\n return closed;\n}\n\n/** A nested transcript as the thread's own pipeline reads it (see {@link sanitizeLangChainMessages}). */\nfunction transcriptSources(transcript: readonly LangChainMessage[]): LangChainMessage[] {\n return sanitizeLangChainMessages(transcript.map(asConvertibleTurn));\n}\n\nconst reportedTranscripts = new WeakSet<readonly LangChainMessage[]>();\n\n/**\n * A nested transcript the shipped converter refused (it throws on, e.g., a\n * result whose tool name differs from its call's): reported once, and its card\n * goes without a nested thread instead of taking the whole thread down.\n */\nfunction reportUnconvertibleTranscript(\n transcript: readonly LangChainMessage[],\n error: unknown\n): void {\n if (reportedTranscripts.has(transcript)) return;\n reportedTranscripts.add(transcript);\n console.warn('[agent-runtime] a subagent transcript could not be converted', error);\n}\n\n/** Whether two values the wire carries (JSON: no functions, no cycles) are equal. */\nfunction sameJson(a: unknown, b: unknown): boolean {\n if (Object.is(a, b)) return true;\n if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null) return false;\n if (Array.isArray(a) || Array.isArray(b)) {\n return (\n Array.isArray(a) &&\n Array.isArray(b) &&\n a.length === b.length &&\n a.every((item, index) => sameJson(item, b[index]))\n );\n }\n const left = a as Record<string, unknown>;\n const right = b as Record<string, unknown>;\n const keys = Object.keys(left);\n return (\n keys.length === Object.keys(right).length &&\n keys.every(\n (key) =>\n Object.prototype.hasOwnProperty.call(right, key) && sameJson(left[key], right[key])\n )\n );\n}\n\n/**\n * Keeps a lane's unchanged steps as the objects it already converted. The host\n * replaces a card's whole document whenever the card changes, so every step it\n * carries arrives as a new object, and the conversion memory, which tells the\n * steps a turn continues by object, would see a new turn each time the card's\n * cut drops its oldest step and the turn's first step changes. A step whose id\n * and content are unchanged is the object held for that id. O(the steps' size).\n */\nfunction steadyStepObjects(): (\n transcript: readonly LangChainMessage[]\n) => readonly LangChainMessage[] {\n let held = new Map<string, LangChainMessage>();\n return (transcript) => {\n const next = new Map<string, LangChainMessage>();\n let kept = false;\n const steadied = transcript.map((entry) => {\n const id = entry.id;\n if (typeof id !== 'string') return entry;\n const before = held.get(id);\n const step = before !== undefined && sameJson(before, entry) ? before : entry;\n next.set(id, step);\n if (step !== entry) kept = true;\n return step;\n });\n held = next;\n return kept ? steadied : transcript;\n };\n}\n\n/** One lane's transcript, converted: `undefined` when the converter refused it. */\nexport type SubagentLaneConversion = (\n transcript: readonly LangChainMessage[],\n running: boolean\n) => readonly ThreadMessage[] | undefined;\n\n/**\n * One `task` lane's own conversion memory, for whatever converts that lane:\n * its unchanged steps keep their converted objects while it streams (a step the\n * wire sends again unchanged included, see {@link steadyStepObjects}), and its\n * turn keeps its row key (`deepAgentRowKey`) and part keys when the turn's head\n * moves — the oldest steps of a cut card falling off as the lane runs, or older\n * steps a transcript drawer pages in. A surface that keeps its disclosures under\n * that key keeps them open through both. A transcript array converted last time\n * is not converted again.\n */\nexport function createSubagentLaneConversion(): SubagentLaneConversion {\n const memory = createSubagentTranscriptConversionCache();\n const steady = steadyStepObjects();\n let last: {\n readonly transcript: readonly LangChainMessage[];\n readonly running: boolean;\n readonly messages: readonly ThreadMessage[] | undefined;\n } | null = null;\n return (transcript, running) => {\n if (last !== null && last.transcript === transcript && last.running === running) {\n return last.messages;\n }\n let messages: readonly ThreadMessage[] | undefined;\n try {\n messages = memory.convert(transcriptSources(steady(transcript)), running);\n } catch (error) {\n reportUnconvertibleTranscript(transcript, error);\n }\n last = { transcript, running, messages };\n return messages;\n };\n}\n\n/**\n * Sets each `task` part of a pass's converted rows to carry its nested\n * transcript as `messages`, for one attach. Each lane has its own conversion\n * memory, so its unchanged steps keep their converted objects while it\n * streams, as the thread's own messages do, and a lane whose transcript array\n * did not change is not converted again. A row, and a part whose lane did not\n * change, keep their identity: the same row always comes back as the same\n * object. A lane's memory lives only while a row of the pass holds its `task`\n * call: a lane a rewind removes, a branch switch hides or the window leaves\n * behind is let go with its transcript, and converted afresh if it returns.\n */\nfunction subagentTranscriptAttacher(): (rows: readonly ThreadMessage[]) => ThreadMessage[] {\n const lanes = new Map<string, SubagentLaneConversion>();\n const attachedRows = new WeakMap<ThreadMessage, ThreadMessage>();\n const attachedParts = new WeakMap<\n ToolCallMessagePart,\n { readonly messages: readonly ThreadMessage[]; readonly part: ToolCallMessagePart }\n >();\n\n const laneMessages = (\n toolCallId: string,\n transcript: readonly LangChainMessage[],\n running: boolean\n ): readonly ThreadMessage[] | undefined => {\n let lane = lanes.get(toolCallId);\n if (lane === undefined) {\n lane = createSubagentLaneConversion();\n lanes.set(toolCallId, lane);\n }\n return lane(transcript, running);\n };\n\n const attach = (row: ThreadMessage): ThreadMessage => {\n if (row.role !== 'assistant') return row;\n const cached = attachedRows.get(row);\n if (cached !== undefined) return cached;\n let content: ThreadAssistantMessagePart[] | null = null;\n for (const [index, part] of row.content.entries()) {\n if (part.type !== 'tool-call' || part.toolName !== TASK_TOOL_NAME) continue;\n const nested = laneTranscriptOf(part.artifact);\n if (nested === undefined) continue;\n // A running lane's placeholder is preliminary: its newest nested turn streams.\n const running = part.isPreliminary === true;\n const restored = withRepeatedResult(nested, part.result);\n const transcript =\n running || part.isError === true\n ? restored\n : withLaneReport(restored, getMessageText({ content: part.result }));\n const messages = laneMessages(part.toolCallId, transcript, running);\n if (messages === undefined) continue;\n const kept = attachedParts.get(part);\n const attached =\n kept !== undefined && kept.messages === messages ? kept.part : { ...part, messages };\n attachedParts.set(part, { messages, part: attached });\n content ??= [...row.content];\n content[index] = attached;\n }\n const attachedRow: ThreadMessage = content === null ? row : { ...row, content };\n attachedRows.set(row, attachedRow);\n return attachedRow;\n };\n\n return (rows) => {\n const attached = rows.map(attach);\n if (lanes.size === 0) return attached;\n const held = new Set<string>();\n for (const row of rows) {\n if (row.role !== 'assistant') continue;\n for (const part of row.content) {\n if (part.type !== 'tool-call' || part.toolName !== TASK_TOOL_NAME) continue;\n held.add(part.toolCallId);\n }\n }\n for (const toolCallId of lanes.keys()) {\n if (!held.has(toolCallId)) lanes.delete(toolCallId);\n }\n return attached;\n };\n}\n\n/**\n * A conversion memory for one attach (see\n * `useDeepAgentThread({ createConversionCache })`): the shared one's rows, with\n * each `task` tool-call part carrying its subagent's converted transcript as\n * `messages`. A pass that changes no row hands back the previous list itself.\n */\nexport function createSubagentTranscriptConversionCache(\n options: DeepAgentMessageConversionOptions = {}\n): DeepAgentConversionMemory {\n const thread = createDeepAgentMessageConversionCache(options);\n const attach = subagentTranscriptAttacher();\n let previousRows: ThreadMessage[] | null = null;\n let previous: ThreadMessage[] = [];\n return {\n convert(messages, isRunning) {\n const rows = thread.convert(messages, isRunning);\n if (rows === previousRows) return previous;\n previousRows = rows;\n const next = attach(rows);\n const unchanged =\n next.length === previous.length && next.every((row, index) => row === previous[index]);\n if (!unchanged) previous = next;\n return previous;\n },\n };\n}\n\n/**\n * Shared by every in-eval transcript: entries are keyed by source message, so\n * none outlives the wire state holding it and threads cannot collide.\n */\nconst inEvalTranscriptCache = createExternalMessageConversionCache();\nconst IN_EVAL_TRANSCRIPT_METADATA = {};\nconst inEvalTranscripts = new WeakMap<\n readonly LangChainMessage[],\n { readonly running: boolean; readonly messages: readonly ThreadMessage[] }\n>();\n\nconst NO_MESSAGES: readonly ThreadMessage[] = [];\n\n/**\n * An in-eval dispatch's live transcript (a `ptc_subagents` entry's `messages`)\n * as a nested thread, converted like a `task` lane's. Memoized per transcript\n * array; a step the lane did not change keeps its converted object. A\n * transcript the converter refuses reads as empty.\n */\nexport function convertSubagentTranscript(\n transcript: readonly LangChainMessage[],\n running: boolean\n): readonly ThreadMessage[] {\n const cached = inEvalTranscripts.get(transcript);\n if (cached !== undefined && cached.running === running) return cached.messages;\n let messages: readonly ThreadMessage[] = NO_MESSAGES;\n try {\n messages = convertExternalMessages(\n transcriptSources(transcript),\n convertDeepAgentLangChainMessage,\n running,\n IN_EVAL_TRANSCRIPT_METADATA,\n inEvalTranscriptCache\n ).map(markPreliminaryToolResults);\n } catch (error) {\n reportUnconvertibleTranscript(transcript, error);\n }\n inEvalTranscripts.set(transcript, { running, messages });\n return messages;\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"subagent-transcripts.js","sources":["../../../../agent-runtime/src/subagent-transcripts.ts"],"sourcesContent":["/**\n * A subagent's transcript as assistant-ui's native nested thread.\n *\n * The backend nests a `task` subagent's transcript in its card's ToolMessage,\n * at `artifact.subgraph_state.messages`: the newest steps on the running\n * placeholder, the whole transcript on the real result. The conversion cache\n * built here converts that transcript through the thread's own pipeline and\n * sets it as the `task` tool-call part's `messages`, assistant-ui's channel for\n * a tool call's nested conversation. `ReadonlyThreadProvider` (the provider\n * under `MessagePartPrimitive.Messages`) renders it as a read-only thread under\n * the parent's tool UIs, and `thread.tasks` derives child work from it.\n *\n * Hosts opt in (`useDeepAgentThread({ createConversionCache })`). It builds on\n * the shared conversion memory from outside, so the shared converter, which\n * ships in every surface's startup bundle (the Office pane's startup and total\n * budgets have no room to spare), stays as it is, and a surface that draws no\n * subagent transcript pays nothing for it.\n */\nimport type {\n ThreadAssistantMessagePart,\n ThreadMessage,\n ToolCallMessagePart,\n} from '@assistant-ui/core';\nimport {\n convertExternalMessages,\n createExternalMessageConversionCache,\n} from '@assistant-ui/core/react';\nimport type { LangChainMessage } from '@assistant-ui/react-langgraph';\nimport { DEEP_AGENT_REPEATS_RESULT_KEY } from './generated/runconfig-contract';\nimport { getMessageText } from './message-text';\nimport {\n convertDeepAgentLangChainMessage,\n createDeepAgentMessageConversionCache,\n type DeepAgentConversionMemory,\n type DeepAgentMessageConversionOptions,\n markPreliminaryToolResults,\n sanitizeLangChainMessages,\n} from './messages';\nimport { TASK_TOOL_NAME } from './tool-names';\n\n/** The transcript nested on a `task` card's artifact, or `undefined` when it carries none. */\nfunction laneTranscriptOf(artifact: unknown): readonly LangChainMessage[] | undefined {\n if (typeof artifact !== 'object' || artifact === null) return undefined;\n const state: unknown = (artifact as { subgraph_state?: unknown }).subgraph_state;\n if (typeof state !== 'object' || state === null) return undefined;\n const messages: unknown = (state as { messages?: unknown }).messages;\n return Array.isArray(messages) ? (messages as LangChainMessage[]) : undefined;\n}\n\nconst restoredTranscripts = new WeakMap<\n readonly LangChainMessage[],\n { readonly content: unknown; readonly transcript: readonly LangChainMessage[] }\n>();\n\n/**\n * A card's nested transcript with its report restored. On a cut finished card\n * the last step can ride with empty content and `DEEP_AGENT_REPEATS_RESULT_KEY`:\n * its text is the subagent's report, the card's own `content`, which the card\n * carries anyway. The step gets that content back. Any other transcript comes\n * back as it is. Memoized per transcript, so a lane's conversion sees one array.\n */\nexport function withRepeatedResult(\n transcript: readonly LangChainMessage[],\n content: unknown\n): readonly LangChainMessage[] {\n const last = transcript[transcript.length - 1];\n if (\n last === undefined ||\n (last as Record<string, unknown>)[DEEP_AGENT_REPEATS_RESULT_KEY] !== true ||\n (typeof content !== 'string' && !Array.isArray(content))\n ) {\n return transcript;\n }\n const cached = restoredTranscripts.get(transcript);\n if (cached !== undefined && cached.content === content) return cached.transcript;\n const restored = [...transcript.slice(0, -1), { ...last, content } as LangChainMessage];\n restoredTranscripts.set(transcript, { content, transcript: restored });\n return restored;\n}\n\ntype AssistantLangChainMessage = Extract<LangChainMessage, { type: 'ai' }>;\n\nconst convertibleChunkTurns = new WeakMap<LangChainMessage, LangChainMessage>();\n\n/**\n * A nested assistant turn stored under the chunk class name (the legacy\n * assistant-stream fold keeps `AIMessageChunk`) as the `ai` message it is: the\n * shipped converter maps only `ai`, and would drop the turn.\n */\nfunction asConvertibleTurn(message: LangChainMessage): LangChainMessage {\n const type: string = message.type;\n if (type !== 'AIMessageChunk') return message;\n const cached = convertibleChunkTurns.get(message);\n if (cached !== undefined) return cached;\n const turn: LangChainMessage = { ...(message as AssistantLangChainMessage), type: 'ai' };\n convertibleChunkTurns.set(message, turn);\n return turn;\n}\n\n/** A lane step as the assistant turn it is when it calls nothing; `null` for any other step. */\nfunction answeringTurn(step: LangChainMessage): AssistantLangChainMessage | null {\n const turn = asConvertibleTurn(step);\n return turn.type === 'ai' && (turn.tool_calls?.length ?? 0) === 0 ? turn : null;\n}\n\nconst isTextBlock = (block: unknown): block is { type: 'text'; text?: unknown } =>\n typeof block === 'object' && block !== null && 'type' in block && block.type === 'text';\n\n/** The text a step's content shows: the string, or its text blocks joined. */\nfunction shownText(content: unknown): string {\n if (typeof content === 'string') return content;\n if (!Array.isArray(content)) return '';\n let text = '';\n for (const block of content) {\n if (isTextBlock(block) && typeof block.text === 'string') text += block.text;\n }\n return text;\n}\n\n/** A turn that calls nothing and shows nothing: no text, and no block of another kind. */\nfunction blankTurn(step: LangChainMessage): AssistantLangChainMessage | null {\n const turn = answeringTurn(step);\n if (turn === null || shownText(turn.content).trim() !== '') return null;\n return typeof turn.content === 'string' || turn.content.every(isTextBlock) ? turn : null;\n}\n\nconst reportedLanes = new WeakMap<\n readonly LangChainMessage[],\n { readonly report: string; readonly transcript: readonly LangChainMessage[] }\n>();\n\n/**\n * A finished lane's transcript, closed by its report. A `task` card's content\n * is its subagent's report, the text of the lane's last turn. A transcript\n * that ends on that turn (an assistant step that calls nothing and says\n * something, which {@link withRepeatedResult} restores when it rides marked)\n * comes back as it is, even when blank steps follow it. A lane stored without\n * it shows the report as its closing answer: a lane whose last step is a blank\n * assistant step (lanes that finished before the host marked a repeated\n * report) gets the report in that step, its id and stamps kept, and a lane\n * that ends on a tool result or a turn that calls tools gets a closing step\n * after its last one. An empty report or transcript changes nothing. For\n * display only: the wire and the stored lane are untouched. Memoized per\n * transcript, so a lane's conversion sees one array.\n */\nexport function withLaneReport(\n transcript: readonly LangChainMessage[],\n report: string | undefined\n): readonly LangChainMessage[] {\n if (report === undefined || report.trim() === '' || transcript.length === 0) return transcript;\n // The lane's closing turn: its last step past any blank ones.\n let at = transcript.length - 1;\n let closing = transcript[at];\n while (closing !== undefined && blankTurn(closing) !== null) {\n at -= 1;\n closing = transcript[at];\n }\n const answered =\n closing !== undefined &&\n answeringTurn(closing) !== null &&\n shownText(closing.content).trim() !== '';\n if (answered) return transcript;\n const cached = reportedLanes.get(transcript);\n if (cached !== undefined && cached.report === report) return cached.transcript;\n const last = transcript[transcript.length - 1];\n const blank = last === undefined ? null : blankTurn(last);\n const closed: readonly LangChainMessage[] =\n blank !== null\n ? [...transcript.slice(0, -1), { ...blank, content: report }]\n : [...transcript, { id: `${last?.id ?? 'lane'}:report`, type: 'ai', content: report }];\n reportedLanes.set(transcript, { report, transcript: closed });\n return closed;\n}\n\n/** A nested transcript as the thread's own pipeline reads it (see {@link sanitizeLangChainMessages}). */\nfunction transcriptSources(transcript: readonly LangChainMessage[]): LangChainMessage[] {\n return sanitizeLangChainMessages(transcript.map(asConvertibleTurn));\n}\n\nconst reportedTranscripts = new WeakSet<readonly LangChainMessage[]>();\n\n/**\n * A nested transcript the shipped converter refused (it throws on, e.g., a\n * result whose tool name differs from its call's): reported once, and its card\n * goes without a nested thread instead of taking the whole thread down.\n */\nfunction reportUnconvertibleTranscript(\n transcript: readonly LangChainMessage[],\n error: unknown\n): void {\n if (reportedTranscripts.has(transcript)) return;\n reportedTranscripts.add(transcript);\n console.warn('[agent-runtime] a subagent transcript could not be converted', error);\n}\n\n/** Whether two values the wire carries (JSON: no functions, no cycles) are equal. */\nfunction sameJson(a: unknown, b: unknown): boolean {\n if (Object.is(a, b)) return true;\n if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null) return false;\n if (Array.isArray(a) || Array.isArray(b)) {\n return (\n Array.isArray(a) &&\n Array.isArray(b) &&\n a.length === b.length &&\n a.every((item, index) => sameJson(item, b[index]))\n );\n }\n const left = a as Record<string, unknown>;\n const right = b as Record<string, unknown>;\n const keys = Object.keys(left);\n return (\n keys.length === Object.keys(right).length &&\n keys.every(\n (key) =>\n Object.prototype.hasOwnProperty.call(right, key) && sameJson(left[key], right[key])\n )\n );\n}\n\n/**\n * Keeps a lane's unchanged steps as the objects it already converted. The host\n * replaces a card's whole document whenever the card changes, so every step it\n * carries arrives as a new object, and the conversion memory, which tells the\n * steps a turn continues by object, would see a new turn each time the card's\n * cut drops its oldest step and the turn's first step changes. A step whose id\n * and content are unchanged is the object held for that id. O(the steps' size).\n */\nfunction steadyStepObjects(): (\n transcript: readonly LangChainMessage[]\n) => readonly LangChainMessage[] {\n let held = new Map<string, LangChainMessage>();\n return (transcript) => {\n const next = new Map<string, LangChainMessage>();\n let kept = false;\n const steadied = transcript.map((entry) => {\n const id = entry.id;\n if (typeof id !== 'string') return entry;\n const before = held.get(id);\n const step = before !== undefined && sameJson(before, entry) ? before : entry;\n next.set(id, step);\n if (step !== entry) kept = true;\n return step;\n });\n held = next;\n return kept ? steadied : transcript;\n };\n}\n\n/** One lane's transcript, converted: `undefined` when the converter refused it. */\nexport type SubagentLaneConversion = (\n transcript: readonly LangChainMessage[],\n running: boolean\n) => readonly ThreadMessage[] | undefined;\n\n/**\n * One `task` lane's own conversion memory, for whatever converts that lane:\n * its unchanged steps keep their converted objects while it streams (a step the\n * wire sends again unchanged included, see {@link steadyStepObjects}), and its\n * turn keeps its row key (`deepAgentRowKey`) and part keys when the turn's head\n * moves — the oldest steps of a cut card falling off as the lane runs, or older\n * steps a transcript drawer pages in. A surface that keeps its disclosures under\n * that key keeps them open through both. A transcript array converted last time\n * is not converted again.\n */\nexport function createSubagentLaneConversion(): SubagentLaneConversion {\n const memory = createSubagentTranscriptConversionCache();\n const steady = steadyStepObjects();\n let last: {\n readonly transcript: readonly LangChainMessage[];\n readonly running: boolean;\n readonly messages: readonly ThreadMessage[] | undefined;\n } | null = null;\n return (transcript, running) => {\n if (last !== null && last.transcript === transcript && last.running === running) {\n return last.messages;\n }\n let messages: readonly ThreadMessage[] | undefined;\n try {\n messages = memory.convert(transcriptSources(steady(transcript)), running);\n } catch (error) {\n reportUnconvertibleTranscript(transcript, error);\n }\n last = { transcript, running, messages };\n return messages;\n };\n}\n\n/**\n * Sets each `task` part of a pass's converted rows to carry its nested\n * transcript as `messages`, for one attach. Each lane has its own conversion\n * memory, so its unchanged steps keep their converted objects while it\n * streams, as the thread's own messages do, and a lane whose transcript array\n * did not change is not converted again. A row, and a part whose lane did not\n * change, keep their identity: the same row always comes back as the same\n * object. A lane's memory lives only while a row of the pass holds its `task`\n * call: a lane a rewind removes, a branch switch hides or the window leaves\n * behind is let go with its transcript, and converted afresh if it returns.\n */\nfunction subagentTranscriptAttacher(): (rows: readonly ThreadMessage[]) => ThreadMessage[] {\n const lanes = new Map<string, SubagentLaneConversion>();\n const attachedRows = new WeakMap<ThreadMessage, ThreadMessage>();\n const attachedParts = new WeakMap<\n ToolCallMessagePart,\n { readonly messages: readonly ThreadMessage[]; readonly part: ToolCallMessagePart }\n >();\n\n const laneMessages = (\n toolCallId: string,\n transcript: readonly LangChainMessage[],\n running: boolean\n ): readonly ThreadMessage[] | undefined => {\n let lane = lanes.get(toolCallId);\n if (lane === undefined) {\n lane = createSubagentLaneConversion();\n lanes.set(toolCallId, lane);\n }\n return lane(transcript, running);\n };\n\n const attach = (row: ThreadMessage): ThreadMessage => {\n if (row.role !== 'assistant') return row;\n const cached = attachedRows.get(row);\n if (cached !== undefined) return cached;\n let content: ThreadAssistantMessagePart[] | null = null;\n for (const [index, part] of row.content.entries()) {\n if (part.type !== 'tool-call' || part.toolName !== TASK_TOOL_NAME) continue;\n const nested = laneTranscriptOf(part.artifact);\n if (nested === undefined) continue;\n // A running lane's placeholder is preliminary: its newest nested turn streams.\n const running = part.isPreliminary === true;\n const restored = withRepeatedResult(nested, part.result);\n const transcript =\n running || part.isError === true\n ? restored\n : withLaneReport(restored, getMessageText({ content: part.result }));\n const messages = laneMessages(part.toolCallId, transcript, running);\n if (messages === undefined) continue;\n const kept = attachedParts.get(part);\n const attached =\n kept !== undefined && kept.messages === messages ? kept.part : { ...part, messages };\n attachedParts.set(part, { messages, part: attached });\n content ??= [...row.content];\n content[index] = attached;\n }\n const attachedRow: ThreadMessage = content === null ? row : { ...row, content };\n attachedRows.set(row, attachedRow);\n return attachedRow;\n };\n\n return (rows) => {\n const attached = rows.map(attach);\n if (lanes.size === 0) return attached;\n const held = new Set<string>();\n for (const row of rows) {\n if (row.role !== 'assistant') continue;\n for (const part of row.content) {\n if (part.type !== 'tool-call' || part.toolName !== TASK_TOOL_NAME) continue;\n held.add(part.toolCallId);\n }\n }\n for (const toolCallId of lanes.keys()) {\n if (!held.has(toolCallId)) lanes.delete(toolCallId);\n }\n return attached;\n };\n}\n\n/**\n * A conversion memory for one attach (see\n * `useDeepAgentThread({ createConversionCache })`): the shared one's rows, with\n * each `task` tool-call part carrying its subagent's converted transcript as\n * `messages`. A pass that changes no row hands back the previous list itself.\n */\nexport function createSubagentTranscriptConversionCache(\n options: DeepAgentMessageConversionOptions = {}\n): DeepAgentConversionMemory {\n const thread = createDeepAgentMessageConversionCache(options);\n const attach = subagentTranscriptAttacher();\n let previousRows: ThreadMessage[] | null = null;\n let previous: ThreadMessage[] = [];\n return {\n convert(messages, isRunning) {\n const rows = thread.convert(messages, isRunning);\n if (rows === previousRows) return previous;\n previousRows = rows;\n const next = attach(rows);\n const unchanged =\n next.length === previous.length && next.every((row, index) => row === previous[index]);\n if (!unchanged) previous = next;\n return previous;\n },\n };\n}\n\n/**\n * Shared by every in-eval transcript: entries are keyed by source message, so\n * none outlives the wire state holding it and threads cannot collide.\n */\nconst inEvalTranscriptCache = createExternalMessageConversionCache();\nconst IN_EVAL_TRANSCRIPT_METADATA = {};\nconst inEvalTranscripts = new WeakMap<\n readonly LangChainMessage[],\n { readonly running: boolean; readonly messages: readonly ThreadMessage[] }\n>();\n\nconst NO_MESSAGES: readonly ThreadMessage[] = [];\n\n/**\n * An in-eval dispatch's live transcript (a `ptc_subagents` entry's `messages`)\n * as a nested thread, converted like a `task` lane's. Memoized per transcript\n * array; a step the lane did not change keeps its converted object. A\n * transcript the converter refuses reads as empty.\n */\nexport function convertSubagentTranscript(\n transcript: readonly LangChainMessage[],\n running: boolean\n): readonly ThreadMessage[] {\n const cached = inEvalTranscripts.get(transcript);\n if (cached !== undefined && cached.running === running) return cached.messages;\n let messages: readonly ThreadMessage[] = NO_MESSAGES;\n try {\n messages = convertExternalMessages(\n transcriptSources(transcript),\n convertDeepAgentLangChainMessage,\n running,\n IN_EVAL_TRANSCRIPT_METADATA,\n inEvalTranscriptCache\n ).map(markPreliminaryToolResults);\n } catch (error) {\n reportUnconvertibleTranscript(transcript, error);\n }\n inEvalTranscripts.set(transcript, { running, messages });\n return messages;\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -234,6 +234,112 @@ function subagentToolCallAwaitingApproval({
|
|
|
234
234
|
const part = findToolCall({ messages: lane.messages, toolCallIds });
|
|
235
235
|
return part === void 0 ? void 0 : { id: part.toolCallId, name: part.toolName, args: part.args };
|
|
236
236
|
}
|
|
237
|
+
const NO_UNSEEN_APPROVAL_LANES = {
|
|
238
|
+
laneIds: /* @__PURE__ */ new Set(),
|
|
239
|
+
gatedCallId: void 0
|
|
240
|
+
};
|
|
241
|
+
function subagentLanesHoldingUnseenApproval({
|
|
242
|
+
lanes,
|
|
243
|
+
turnToolCallIds,
|
|
244
|
+
toolCallIds
|
|
245
|
+
}) {
|
|
246
|
+
if (toolCallIds.size === 0) return NO_UNSEEN_APPROVAL_LANES;
|
|
247
|
+
const shown = /* @__PURE__ */ new Set();
|
|
248
|
+
for (const lane of lanes) {
|
|
249
|
+
for (const message of lane.messages) {
|
|
250
|
+
if (message.role !== "assistant") continue;
|
|
251
|
+
for (const part of message.content) {
|
|
252
|
+
if (part.type === "tool-call" && toolCallIds.has(part.toolCallId)) {
|
|
253
|
+
shown.add(part.toolCallId);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
const unseen = [...toolCallIds].filter(
|
|
259
|
+
(id) => !turnToolCallIds.has(id) && !shown.has(id)
|
|
260
|
+
);
|
|
261
|
+
if (unseen.length === 0) return NO_UNSEEN_APPROVAL_LANES;
|
|
262
|
+
const holders = lanes.filter(
|
|
263
|
+
(lane) => !isTerminalSubagentStatus(lane.status) && lane.messages.length === 0
|
|
264
|
+
);
|
|
265
|
+
if (holders.length === 0) return NO_UNSEEN_APPROVAL_LANES;
|
|
266
|
+
return {
|
|
267
|
+
laneIds: new Set(holders.map((lane) => lane.id)),
|
|
268
|
+
gatedCallId: holders.length === 1 && unseen.length === 1 ? unseen[0] : void 0
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
function openTurnToolCallIds(message) {
|
|
272
|
+
var _a;
|
|
273
|
+
if (message.role !== "assistant" || !message.isLast) return void 0;
|
|
274
|
+
const status = (_a = message.status) == null ? void 0 : _a.type;
|
|
275
|
+
if (status !== "running" && status !== "requires-action") return void 0;
|
|
276
|
+
const ids = /* @__PURE__ */ new Set();
|
|
277
|
+
for (const part of message.content) {
|
|
278
|
+
if (part.type === "tool-call") ids.add(part.toolCallId);
|
|
279
|
+
}
|
|
280
|
+
return ids;
|
|
281
|
+
}
|
|
282
|
+
const SUBAGENT_AWAITING_APPROVAL_LABEL = "Waiting for approval";
|
|
283
|
+
const NOT_AWAITING_APPROVAL = {
|
|
284
|
+
awaitingApproval: false,
|
|
285
|
+
gatedCall: void 0
|
|
286
|
+
};
|
|
287
|
+
function subagentLaneApprovalWait({
|
|
288
|
+
lane,
|
|
289
|
+
toolCallIds,
|
|
290
|
+
unseenApproval,
|
|
291
|
+
actionRequests
|
|
292
|
+
}) {
|
|
293
|
+
const unseen = !isTerminalSubagentStatus(lane.status) && (unseenApproval == null ? void 0 : unseenApproval.laneIds.has(lane.id)) === true;
|
|
294
|
+
if (!subagentAwaitingApproval({ lane, toolCallIds }) && !unseen) return NOT_AWAITING_APPROVAL;
|
|
295
|
+
const shown = subagentToolCallAwaitingApproval({ lane, toolCallIds });
|
|
296
|
+
if (shown !== void 0 || !unseen) return { awaitingApproval: true, gatedCall: shown };
|
|
297
|
+
const gatedCallId = unseenApproval == null ? void 0 : unseenApproval.gatedCallId;
|
|
298
|
+
const request = gatedCallId ? actionRequests == null ? void 0 : actionRequests.find((candidate) => candidate.toolCallId === gatedCallId) : void 0;
|
|
299
|
+
return {
|
|
300
|
+
awaitingApproval: true,
|
|
301
|
+
gatedCall: request && gatedCallId ? (
|
|
302
|
+
// The approval's args crossed the wire as JSON, as a call's own do.
|
|
303
|
+
{ id: gatedCallId, name: request.name, args: request.args }
|
|
304
|
+
) : void 0,
|
|
305
|
+
insideLane: true
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
function turnLaneApprovalWait({
|
|
309
|
+
message,
|
|
310
|
+
toolCallId,
|
|
311
|
+
toolCallIds,
|
|
312
|
+
actionRequests
|
|
313
|
+
}) {
|
|
314
|
+
if (toolCallIds.size === 0 || message.role !== "assistant") return NOT_AWAITING_APPROVAL;
|
|
315
|
+
const artifacts = /* @__PURE__ */ new Map();
|
|
316
|
+
for (const part of message.content) {
|
|
317
|
+
if (part.type === "tool-call" && part.toolName === toolNames.TASK_TOOL_NAME) {
|
|
318
|
+
artifacts.set(part.toolCallId, part.artifact);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
const lanes = [...taskSubagentsOfTurn(message).values()].map((lane2) => {
|
|
322
|
+
if (lane2.messages.length > 0) return lane2;
|
|
323
|
+
const carried = nestedMessagesFromArtifact(artifacts.get(lane2.id));
|
|
324
|
+
return carried.length === 0 ? lane2 : { ...lane2, messages: subagentTranscripts.convertSubagentTranscript(carried, lane2.status === "running") };
|
|
325
|
+
});
|
|
326
|
+
const lane = lanes.find((candidate) => candidate.id === toolCallId);
|
|
327
|
+
if (lane === void 0) return NOT_AWAITING_APPROVAL;
|
|
328
|
+
const turnToolCallIds = openTurnToolCallIds(message);
|
|
329
|
+
return subagentLaneApprovalWait({
|
|
330
|
+
lane,
|
|
331
|
+
toolCallIds,
|
|
332
|
+
unseenApproval: turnToolCallIds === void 0 ? void 0 : subagentLanesHoldingUnseenApproval({ lanes, turnToolCallIds, toolCallIds }),
|
|
333
|
+
actionRequests
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
function subagentApprovalNoteLabel({
|
|
337
|
+
gatedCall,
|
|
338
|
+
insideLane = false
|
|
339
|
+
}) {
|
|
340
|
+
if (gatedCall !== void 0) return "Needs approval to run";
|
|
341
|
+
return insideLane ? "Needs approval to continue" : "Needs approval to start";
|
|
342
|
+
}
|
|
237
343
|
function turnToolCallsAwaitingApproval({
|
|
238
344
|
message,
|
|
239
345
|
toolCallIds
|
|
@@ -267,17 +373,23 @@ function useTurnToolCallsAwaitingApproval(toolCallIds) {
|
|
|
267
373
|
[key]
|
|
268
374
|
);
|
|
269
375
|
}
|
|
376
|
+
exports.SUBAGENT_AWAITING_APPROVAL_LABEL = SUBAGENT_AWAITING_APPROVAL_LABEL;
|
|
270
377
|
exports.countInEvalDispatches = countInEvalDispatches;
|
|
271
378
|
exports.isTerminalSubagentStatus = isTerminalSubagentStatus;
|
|
272
379
|
exports.nestedMessagesFromArtifact = nestedMessagesFromArtifact;
|
|
273
380
|
exports.nestedTodosFromArtifact = nestedTodosFromArtifact;
|
|
381
|
+
exports.openTurnToolCallIds = openTurnToolCallIds;
|
|
274
382
|
exports.ptcSubagentsFromArtifact = ptcSubagentsFromArtifact;
|
|
275
383
|
exports.readLaneUsage = readLaneUsage;
|
|
276
384
|
exports.readTruncatedLane = readTruncatedLane;
|
|
385
|
+
exports.subagentApprovalNoteLabel = subagentApprovalNoteLabel;
|
|
277
386
|
exports.subagentAwaitingApproval = subagentAwaitingApproval;
|
|
387
|
+
exports.subagentLaneApprovalWait = subagentLaneApprovalWait;
|
|
388
|
+
exports.subagentLanesHoldingUnseenApproval = subagentLanesHoldingUnseenApproval;
|
|
278
389
|
exports.subagentToolCallAwaitingApproval = subagentToolCallAwaitingApproval;
|
|
279
390
|
exports.taskSubagentsOfTurn = taskSubagentsOfTurn;
|
|
280
391
|
exports.transcriptStepUsage = transcriptStepUsage;
|
|
392
|
+
exports.turnLaneApprovalWait = turnLaneApprovalWait;
|
|
281
393
|
exports.turnToolCallsAwaitingApproval = turnToolCallsAwaitingApproval;
|
|
282
394
|
exports.useTurnToolCallsAwaitingApproval = useTurnToolCallsAwaitingApproval;
|
|
283
395
|
//# sourceMappingURL=subagents.cjs.map
|