@athenaintel/react 0.50.0 → 0.53.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 +24 -0
- package/dist/agent-runtime/src/desktop-handoff.d.ts +10 -0
- package/dist/agent-runtime/src/edit-command.cjs +21 -0
- 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 +21 -0
- package/dist/agent-runtime/src/edit-command.js.map +1 -1
- package/dist/agent-runtime/src/handoff-arrival.cjs +19 -0
- package/dist/agent-runtime/src/handoff-arrival.cjs.map +1 -1
- package/dist/agent-runtime/src/handoff-arrival.d.ts +18 -3
- package/dist/agent-runtime/src/handoff-arrival.js +20 -1
- package/dist/agent-runtime/src/handoff-arrival.js.map +1 -1
- package/dist/agent-runtime/src/handoff-cursor-id.cjs +6 -0
- package/dist/agent-runtime/src/handoff-cursor-id.cjs.map +1 -0
- package/dist/agent-runtime/src/handoff-cursor-id.d.ts +12 -0
- package/dist/agent-runtime/src/handoff-cursor-id.js +6 -0
- package/dist/agent-runtime/src/handoff-cursor-id.js.map +1 -0
- package/dist/agent-runtime/src/handoff-cursor.cjs +88 -0
- package/dist/agent-runtime/src/handoff-cursor.cjs.map +1 -0
- package/dist/agent-runtime/src/handoff-cursor.d.ts +120 -0
- package/dist/agent-runtime/src/handoff-cursor.js +88 -0
- package/dist/agent-runtime/src/handoff-cursor.js.map +1 -0
- package/dist/agent-runtime/src/index.d.ts +4 -3
- package/dist/agent-runtime/src/thread-handoff-flow-machine.d.ts +6 -0
- package/dist/agent-runtime/src/thread-handoff-protocol.d.ts +114 -4
- package/dist/agent-runtime/src/thread-handoff.d.ts +1 -1
- package/dist/agent-runtime/src/use-deep-agent-window-sync.cjs +8 -3
- 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 +7 -0
- package/dist/agent-runtime/src/use-deep-agent-window-sync.js +9 -4
- 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 -0
- package/dist/chat/AthenaChat.cjs +27 -12
- package/dist/chat/AthenaChat.cjs.map +1 -1
- package/dist/chat/AthenaChat.d.ts +13 -0
- package/dist/chat/AthenaChat.js +27 -12
- package/dist/chat/AthenaChat.js.map +1 -1
- package/dist/chat/AthenaPlanStrip.cjs +97 -0
- package/dist/chat/AthenaPlanStrip.cjs.map +1 -0
- package/dist/chat/AthenaPlanStrip.d.ts +15 -0
- package/dist/chat/AthenaPlanStrip.js +97 -0
- package/dist/chat/AthenaPlanStrip.js.map +1 -0
- package/dist/chat/super-grouping/SuperGroupingCard.cjs +4 -2
- package/dist/chat/super-grouping/SuperGroupingCard.cjs.map +1 -1
- package/dist/chat/super-grouping/SuperGroupingCard.js +4 -2
- package/dist/chat/super-grouping/SuperGroupingCard.js.map +1 -1
- package/dist/chat/super-grouping/SuperGroupingTranscript.cjs +4 -2
- package/dist/chat/super-grouping/SuperGroupingTranscript.cjs.map +1 -1
- package/dist/chat/super-grouping/SuperGroupingTranscript.js +4 -2
- package/dist/chat/super-grouping/SuperGroupingTranscript.js.map +1 -1
- package/dist/chat/super-grouping/activity-timeline.cjs +6 -4
- package/dist/chat/super-grouping/activity-timeline.cjs.map +1 -1
- package/dist/chat/super-grouping/activity-timeline.d.ts +9 -1
- package/dist/chat/super-grouping/activity-timeline.js +6 -4
- package/dist/chat/super-grouping/activity-timeline.js.map +1 -1
- package/dist/chat/super-grouping/index.d.ts +1 -1
- package/dist/chat/super-grouping/use-earlier-steps-unloaded.cjs +17 -0
- package/dist/chat/super-grouping/use-earlier-steps-unloaded.cjs.map +1 -0
- package/dist/chat/super-grouping/use-earlier-steps-unloaded.d.ts +8 -0
- package/dist/chat/super-grouping/use-earlier-steps-unloaded.js +17 -0
- package/dist/chat/super-grouping/use-earlier-steps-unloaded.js.map +1 -0
- package/dist/chat-ui/index.cjs +8 -0
- package/dist/chat-ui/index.cjs.map +1 -1
- package/dist/chat-ui/index.d.ts +2 -1
- package/dist/chat-ui/index.js +9 -1
- package/dist/chat-ui/index.js.map +1 -1
- package/dist/chat-ui/mentions/mention-chip.cjs +1 -12
- package/dist/chat-ui/mentions/mention-chip.cjs.map +1 -1
- package/dist/chat-ui/mentions/mention-chip.js +1 -12
- package/dist/chat-ui/mentions/mention-chip.js.map +1 -1
- package/dist/chat-ui/mentions/use-mention-asset.cjs +18 -3
- package/dist/chat-ui/mentions/use-mention-asset.cjs.map +1 -1
- package/dist/chat-ui/mentions/use-mention-asset.js +19 -4
- package/dist/chat-ui/mentions/use-mention-asset.js.map +1 -1
- package/dist/chat-ui/thread/chat-thread-shell.cjs +2 -0
- package/dist/chat-ui/thread/chat-thread-shell.cjs.map +1 -1
- package/dist/chat-ui/thread/chat-thread-shell.d.ts +4 -0
- package/dist/chat-ui/thread/chat-thread-shell.js +2 -0
- package/dist/chat-ui/thread/chat-thread-shell.js.map +1 -1
- package/dist/chat-ui/thread/handoff-cursor.cjs +216 -0
- package/dist/chat-ui/thread/handoff-cursor.cjs.map +1 -0
- package/dist/chat-ui/thread/handoff-cursor.d.ts +57 -0
- package/dist/chat-ui/thread/handoff-cursor.js +216 -0
- package/dist/chat-ui/thread/handoff-cursor.js.map +1 -0
- package/dist/chat-ui/thread/statewire-history-edge.cjs +2 -0
- package/dist/chat-ui/thread/statewire-history-edge.cjs.map +1 -1
- package/dist/chat-ui/thread/statewire-history-edge.d.ts +2 -0
- package/dist/chat-ui/thread/statewire-history-edge.js +3 -1
- package/dist/chat-ui/thread/statewire-history-edge.js.map +1 -1
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/keryx-http/src/unconfirmed-update-attributions.cjs +20 -3
- package/dist/keryx-http/src/unconfirmed-update-attributions.cjs.map +1 -1
- package/dist/keryx-http/src/unconfirmed-update-attributions.d.ts +11 -2
- package/dist/keryx-http/src/unconfirmed-update-attributions.js +21 -4
- package/dist/keryx-http/src/unconfirmed-update-attributions.js.map +1 -1
- package/dist/react/package.json.cjs +1 -1
- package/dist/react/package.json.js +1 -1
- package/dist/styles.css +1 -1
- package/dist/tools/write-todos-tool-ui.cjs +8 -0
- package/dist/tools/write-todos-tool-ui.cjs.map +1 -1
- package/dist/tools/write-todos-tool-ui.d.ts +2 -0
- package/dist/tools/write-todos-tool-ui.js +8 -0
- package/dist/tools/write-todos-tool-ui.js.map +1 -1
- package/dist/workbench/session-workbench-panes.cjs +1 -8
- package/dist/workbench/session-workbench-panes.cjs.map +1 -1
- package/dist/workbench/session-workbench-panes.js +2 -9
- package/dist/workbench/session-workbench-panes.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -951,6 +951,30 @@ Both use the same bearer / API key as session creation. The full recipe
|
|
|
951
951
|
template skill `voice-session-guide.md`. `createAthenaVoiceSession` is also
|
|
952
952
|
exported if you hold the bridge state yourself.
|
|
953
953
|
|
|
954
|
+
## What's new in 0.53.0
|
|
955
|
+
|
|
956
|
+
**A chip names a chat by its own title.** Asset chips now look their assets up through Athena's `getAssetChipDetails`, which reads only what a chip draws (the asset's title and type) instead of each asset's whole row. A chip that links a chat session (`type=session`, an `asset_thread_…` id, or an anchor into a message or tool call) now shows the session's own title like every other asset, where it used to keep the label the model wrote, because a session's row was too heavy to read for a name. A repository file citation still reads as its file and lines. Against an Athena that predates the field, the SDK asks once per backend and then keeps each link's label, as before; nothing falls back to reading whole rows. Nothing to change in your app.
|
|
957
|
+
|
|
958
|
+
## What's new in 0.52.1
|
|
959
|
+
|
|
960
|
+
**0.52.0 as published.** The 0.52.0 build bundled zod (reached through the handover cursor's message-id check) under `dist/node_modules`, which npm never packs, so it was never published. The check now lives in a zod-free module, and the build fails if `dist/node_modules` ever appears. Otherwise identical to 0.52.0 below.
|
|
961
|
+
|
|
962
|
+
## What's new in 0.52.0
|
|
963
|
+
|
|
964
|
+
**The run's plan is pinned above the composer.** When the agent plans with `write_todos`, `AthenaChat` shows a one-line plan strip above the composer: "Plan 2/5" and the step in progress, with a progress bar. It opens to the whole list with each step's status. It reads the thread's `todos` state, as Olympus's plan panel does, so it stays true while the transcript's own plan cards are paged out or collapsed into their turn. `AthenaPlanStrip` is exported for your own layout. `AthenaChat`'s `plan` prop replaces the strip with your own element, and `plan={null}` shows none.
|
|
965
|
+
|
|
966
|
+
**A long turn at the top of a session counts honestly and loads in a few pages.** A session's oldest loaded turn can still have earlier steps on the host. Its card used to count only the loaded ones as if they were all of them, so a 373-call run read "Used 43 tools". It now reads "Used 43+ tools" until its prompt pages in, then the exact count (`getActivitySummary(timeline, { earlierStepsUnloaded })`). Paging into such a turn now asks for the host's largest window (200 messages) instead of the thread's (40). A page that lands inside a turn shows nothing new until the turn's prompt arrives, so reaching the top of that 373-call run takes about four round trips instead of nineteen. Pages above a prompt keep the thread's window.
|
|
967
|
+
|
|
968
|
+
**The scroll-to-top button sits beside the text, not on it.** It moved from the middle of the thread column to the viewport's top-right edge and got smaller, so it no longer covers the line you're reading.
|
|
969
|
+
|
|
970
|
+
## What's new in 0.51.0
|
|
971
|
+
|
|
972
|
+
**A row above the composer's editor.** `<AthenaChat composer={{ aboveEditor }}>` renders the host's node at the top of the composer, inside its border and above the editor (and above any voice offer, quote or attachment preview): the place Spaces shows its running status and Follow Agent Operations switch. Athena Desktop puts its Follow Athena switch there. It renders inside the chat's thread scope, like the toolbar slots, so `useAthenaStatewireThreadId()` names the conversation it sits in. Without it, nothing changes.
|
|
973
|
+
|
|
974
|
+
```tsx
|
|
975
|
+
<AthenaChat composer={{ aboveEditor: <FollowSwitch /> }} />
|
|
976
|
+
```
|
|
977
|
+
|
|
954
978
|
## What's new in 0.50.0
|
|
955
979
|
|
|
956
980
|
**An agent's own app lists its conversations from every channel.** An app that runs as a collab agent (`agent="collab_agent:<asset_id>"`) can now list the conversations people have with that agent anywhere else, such as Slack, email or SMS, beside its own sessions. Pass `threadListAgentHome` with `enableThreadList`:
|
|
@@ -70,6 +70,8 @@ export declare const desktopHandoffViewSchema: z.ZodObject<{
|
|
|
70
70
|
voice: "voice";
|
|
71
71
|
}>>;
|
|
72
72
|
draft: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
73
|
+
cursor: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
74
|
+
revision: z.ZodOptional<z.ZodNumber>;
|
|
73
75
|
}, z.core.$strip>;
|
|
74
76
|
export type DesktopHandoffView = z.infer<typeof desktopHandoffViewSchema>;
|
|
75
77
|
export declare const desktopHandoffLaunchSchema: z.ZodObject<{
|
|
@@ -129,6 +131,8 @@ export declare const createDesktopHandoffResponseSchema: z.ZodObject<{
|
|
|
129
131
|
voice: "voice";
|
|
130
132
|
}>>;
|
|
131
133
|
draft: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
134
|
+
cursor: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
135
|
+
revision: z.ZodOptional<z.ZodNumber>;
|
|
132
136
|
}, z.core.$strip>;
|
|
133
137
|
launch: z.ZodObject<{
|
|
134
138
|
kind: z.ZodLiteral<"desktop">;
|
|
@@ -144,6 +148,12 @@ export interface CreateDesktopHandoffInput {
|
|
|
144
148
|
assets: DesktopHandoffAsset[];
|
|
145
149
|
active_asset_id?: string | null;
|
|
146
150
|
source: "spaces";
|
|
151
|
+
/**
|
|
152
|
+
* Where the source was reading (a message id, or `""` at the bottom). Only to
|
|
153
|
+
* a host whose readiness says `supports_handoff_cursor`: the route refuses
|
|
154
|
+
* unknown fields.
|
|
155
|
+
*/
|
|
156
|
+
cursor?: string;
|
|
147
157
|
}
|
|
148
158
|
/** Only a destination acknowledgement proves a launch. Timeouts prove no installation state. */
|
|
149
159
|
export declare function desktopHandoffSummary({ handoff, }: {
|
|
@@ -59,6 +59,25 @@ function buildDeepAgentEditCommand({
|
|
|
59
59
|
runAnchorMessageId
|
|
60
60
|
};
|
|
61
61
|
}
|
|
62
|
+
function deepAgentHistoryHasOlder(extras) {
|
|
63
|
+
var _a;
|
|
64
|
+
const typed = readDeepAgentStatewireExtras(extras);
|
|
65
|
+
if (typed === null) return false;
|
|
66
|
+
return ((_a = readWindow(typed)) == null ? void 0 : _a.hasOlder) ?? null;
|
|
67
|
+
}
|
|
68
|
+
async function loadOlderDeepAgentHistoryPage({
|
|
69
|
+
readExtras,
|
|
70
|
+
pageSize
|
|
71
|
+
}) {
|
|
72
|
+
const extras = readExtras();
|
|
73
|
+
if (!(extras == null ? void 0 : extras.invoke)) return false;
|
|
74
|
+
const window = readWindow(extras);
|
|
75
|
+
if (window === null || !window.hasOlder) return false;
|
|
76
|
+
const requests = messageWindow.pageInterestRequests(window, pageSize);
|
|
77
|
+
if (requests === null) return false;
|
|
78
|
+
await extras.invoke(runconfigContract.DEEP_AGENT_INTEREST_PROTOCOL, "set", [requests]);
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
62
81
|
const MAX_EDIT_ANCHOR_HISTORY_LOADS = 5;
|
|
63
82
|
async function resolveDeepAgentEditCommand({
|
|
64
83
|
readExtras,
|
|
@@ -86,7 +105,9 @@ async function resolveDeepAgentEditCommand({
|
|
|
86
105
|
}
|
|
87
106
|
exports.MAX_EDIT_ANCHOR_HISTORY_LOADS = MAX_EDIT_ANCHOR_HISTORY_LOADS;
|
|
88
107
|
exports.buildDeepAgentEditCommand = buildDeepAgentEditCommand;
|
|
108
|
+
exports.deepAgentHistoryHasOlder = deepAgentHistoryHasOlder;
|
|
89
109
|
exports.isSourceBelowLoadedWindow = isSourceBelowLoadedWindow;
|
|
110
|
+
exports.loadOlderDeepAgentHistoryPage = loadOlderDeepAgentHistoryPage;
|
|
90
111
|
exports.readDeepAgentStatewireExtras = readDeepAgentStatewireExtras;
|
|
91
112
|
exports.resolveDeepAgentEditCommand = resolveDeepAgentEditCommand;
|
|
92
113
|
exports.resolveWireAnchorMessageId = resolveWireAnchorMessageId;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"edit-command.cjs","sources":["../../../../agent-runtime/src/edit-command.ts"],"sourcesContent":["import type { StatewireThread } from '@assistant-ui/react-statewire';\nimport { DEEP_AGENT_INTEREST_PROTOCOL } from './generated/runconfig-contract';\nimport {\n type DeepAgentMessageWindow,\n pageInterestRequests,\n readDeepAgentMessageWindow,\n} from './message-window';\nimport { hasStatewireThreadExtras } from './thread-extras';\n\n/**\n * Wire-accurate `run/edit` construction for deep-agent retry/edit, shared by\n * every surface that resubmits a user message.\n *\n * assistant-ui builds the command's `runAnchorMessageId` from the CONVERTED\n * thread messages (`resolveEditCommand`: `messages[index - 1].id`), but the\n * deep-agent host validates that anchor against the edited message's parent in\n * its RAW message list (`harness_sdk.linear_thread`: the entry before it in\n * `state[\"messages\"]`). Those two lists do not line up:\n *\n * - the converter chunks an assistant turn and its tool messages into ONE\n * thread message carrying only the FIRST wire id, so the anchor names the\n * head of the previous turn instead of its last tool message;\n * - `sanitizeLangChainMessages` drops hidden-from-frontend and unconvertible\n * entries, so a dropped tail message is invisible to the anchor too.\n *\n * Either way the host rejects the command with `wrong-anchor`\n * (`runAnchorMessageId … is not the source's parent`) AFTER the composer's\n * synchronous `send()` returned, so the retry/edit silently does nothing.\n *\n * Building the command here off the replicated raw state makes the anchor the\n * same value the host computes.\n *\n * The raw list is the loaded interest window, so the first loaded message's\n * parent is not in it while older messages exist — an edit there must not\n * anchor on `null` (the root, rejected `wrong-anchor`); a message below the\n * window has no parent on this client at all. {@link resolveDeepAgentEditCommand}\n * states older page interests until the parent is loaded.\n */\n\n/**\n * Host metadata frozen at submission, settling once the async capture does.\n * The message-metadata preparation surface — `prepareMessageMetadata`,\n * `discardPreparedMessageMetadata`, `sendPreparedEdit` and `sendCommand`'s\n * `preparedMetadata` option — is what the monorepo patches into\n * `@assistant-ui/react-statewire@0.31.0` (`patches/`). It is declared\n * structurally here rather than as `StatewireThread.Extras[...]` so that the\n * published sources also compile against the unpatched upstream typings, where\n * those members are absent and the edit path's optional calls never run.\n */\nexport type DeepAgentPreparedMessageMetadata = Promise<Record<string, unknown> | undefined>;\n\n/** The pieces of the statewire thread extras the edit path reads. */\nexport interface DeepAgentStatewireExtras {\n /** The library's dispatch; it resolves with the host's reply to the command. */\n sendCommand: (\n command: StatewireThread.Command,\n options?: { preparedMetadata?: DeepAgentPreparedMessageMetadata | undefined },\n ) => Promise<unknown> | void;\n /** Optional on older hosts; capture before loading an edit's history anchor. */\n prepareMessageMetadata?: (() => DeepAgentPreparedMessageMetadata) | undefined;\n discardPreparedMessageMetadata?:\n | ((prepared: DeepAgentPreparedMessageMetadata) => void)\n | undefined;\n sendPreparedEdit?:\n | ((sourceId: string, prepared: DeepAgentPreparedMessageMetadata) => void)\n | undefined;\n isPreparingSubmission?: boolean;\n /** The composed thread state — its `messages` are the raw wire messages of the loaded window. */\n state: { messages?: readonly { id?: unknown }[] } | null;\n /** The mounted documents (the interest window), absent on a wire without documents. */\n documents?: StatewireThread.Extras['documents'];\n invoke?: StatewireThread.Extras['invoke'];\n /** The live run, when one is in flight; `epoch` advances on each dispatch. */\n runs: { runId?: string | null; epoch?: number };\n /** The host's replicated run status; absent on a surface that predates it. */\n status?: StatewireThread.ThreadStatus;\n}\n\n/**\n * The statewire extras of the nearest aui thread, or `null` when the thread is\n * not a statewire thread (e.g. a read-only subagent transcript runtime).\n * Gated on {@link hasStatewireThreadExtras} for the same reason the approval\n * card is: the library's own hooks throw on a non-statewire thread.\n */\nexport function readDeepAgentStatewireExtras(extras: unknown): DeepAgentStatewireExtras | null {\n if (!hasStatewireThreadExtras(extras)) return null;\n return extras as DeepAgentStatewireExtras;\n}\n\n/**\n * The host's parent of `sourceId`: the raw wire message directly before it\n * (`null` when it is the first of the whole transcript). Returns `undefined`\n * when `sourceId` is not in the raw list at all — an optimistic bubble the\n * host has not settled yet, a message below the loaded window, or a surface\n * whose state this module cannot read — or when it is the first loaded\n * message while older ones exist (`hasOlder`), so the caller can load more or\n * fall back to the library's own path instead of asserting a wrong anchor.\n */\nexport function resolveWireAnchorMessageId({\n wireMessages,\n sourceId,\n hasOlder = false,\n}: {\n wireMessages: readonly { id?: unknown }[] | undefined;\n sourceId: string;\n hasOlder?: boolean;\n}): string | null | undefined {\n if (!wireMessages) return undefined;\n const index = wireMessages.findIndex((message) => message?.id === sourceId);\n if (index < 0) return undefined;\n if (index === 0) return hasOlder ? undefined : null;\n const parentId = wireMessages[index - 1]?.id;\n return typeof parentId === 'string' ? parentId : undefined;\n}\n\n/**\n * Whether `sourceId`'s parent lies below the loaded window: the message is\n * absent from the raw list, or its first entry, while older messages exist.\n * The caller loads older history before an edit can be anchored.\n */\nexport function isSourceBelowLoadedWindow({\n wireMessages,\n sourceId,\n window,\n}: {\n wireMessages: readonly { id?: unknown }[] | undefined;\n sourceId: string;\n window: DeepAgentMessageWindow | null;\n}): boolean {\n if (!wireMessages || window === null || !window.hasOlder) return false;\n const index = wireMessages.findIndex((message) => message?.id === sourceId);\n return index <= 0;\n}\n\nconst readWindow = (extras: DeepAgentStatewireExtras) =>\n readDeepAgentMessageWindow(extras.documents?.list());\n\n/**\n * A fresh command / message id (uniqueness, not secrecy). `crypto.randomUUID`\n * where the host runtime has it — every browser, Bun, Node and Hermes with\n * webcrypto — and a v4-shaped fallback elsewhere, so the shared path never\n * throws on a runtime without full webcrypto.\n */\nfunction mintCommandId(): string {\n const webcrypto = (globalThis as { crypto?: { randomUUID?: () => string } }).crypto;\n if (typeof webcrypto?.randomUUID === 'function') return webcrypto.randomUUID();\n const hex = (): string => Math.floor(Math.random() * 0x10000).toString(16).padStart(4, '0');\n return `${hex()}${hex()}-${hex()}-4${hex().slice(1)}-${hex()}-${hex()}${hex()}${hex()}`;\n}\n\n/**\n * The `run/edit` command for resubmitting `sourceId` with `text`, anchored on\n * the host's own parent. `null` when the anchor cannot be resolved from the\n * replicated state.\n */\nexport function buildDeepAgentEditCommand({\n extras,\n sourceId,\n text,\n}: {\n extras: DeepAgentStatewireExtras;\n sourceId: string;\n text: string;\n}): StatewireThread.RunEditCommand | null {\n const runAnchorMessageId = resolveWireAnchorMessageId({\n wireMessages: extras.state?.messages,\n sourceId,\n hasOlder: readWindow(extras)?.hasOlder ?? false,\n });\n if (runAnchorMessageId === undefined) return null;\n return {\n type: 'run/edit',\n runId: extras.runs.runId ?? mintCommandId(),\n message: { id: mintCommandId(), role: 'user', parts: [{ type: 'text', text }] },\n sourceId,\n runAnchorMessageId,\n };\n}\n\n/**\n * State one more page of older history for a windowed thread: the next page\n * past the loaded window's oldest message. Resolves `true` once the host has\n * mounted it (it answers the statement after mounting), `false` when there is\n * nothing older, the thread is not a windowed statewire thread, or no page can\n * be stated. For a reader that needs a message below the loaded window — a\n * search result revealing an older turn of a long conversation — and pages\n * until it renders.\n */\nexport async function loadOlderDeepAgentHistoryPage({\n readExtras,\n pageSize,\n}: {\n readExtras: () => DeepAgentStatewireExtras | null;\n pageSize?: number;\n}): Promise<boolean> {\n const extras = readExtras();\n if (!extras?.invoke) return false;\n const window = readWindow(extras);\n if (window === null || !window.hasOlder) return false;\n const requests = pageInterestRequests(window, pageSize);\n if (requests === null) return false;\n await extras.invoke(DEEP_AGENT_INTEREST_PROTOCOL, 'set', [requests]);\n return true;\n}\n\n/**\n * Pages of older history stated while looking for an edited message's host\n * parent. Each page is up to a window's worth of messages, so an edit reaches\n * this many windows below what was loaded before it gives up.\n */\nexport const MAX_EDIT_ANCHOR_HISTORY_LOADS = 5;\n\n/**\n * The anchored `run/edit` for `sourceId`, stating older page interests one at\n * a time when the message's parent lies below the loaded window. Every page\n * is read back from the live documents (`readExtras` — the host mounts them\n * before it answers the statement), so the anchor is always the host's own\n * parent id.\n *\n * Resolves `null` — the caller's cue for the library's composer path, whose\n * failure it reports — when the thread is not a statewire thread, the host has\n * not settled the message, the history is exhausted without it, or the page\n * budget ({@link MAX_EDIT_ANCHOR_HISTORY_LOADS}) is spent. A synchronous\n * resolution never awaits: the first read either anchors at once or decides\n * that pages are needed.\n */\nexport async function resolveDeepAgentEditCommand({\n readExtras,\n sourceId,\n text,\n pageSize,\n maxHistoryLoads = MAX_EDIT_ANCHOR_HISTORY_LOADS,\n}: {\n /** The live thread extras — re-read per page so each anchor attempt sees the widened window. */\n readExtras: () => DeepAgentStatewireExtras | null;\n sourceId: string;\n text: string;\n pageSize?: number;\n maxHistoryLoads?: number;\n}): Promise<StatewireThread.RunEditCommand | null> {\n for (let page = 0; page <= maxHistoryLoads; page += 1) {\n const extras = readExtras();\n if (!extras) return null;\n const command = buildDeepAgentEditCommand({ extras, sourceId, text });\n if (command) return command;\n if (page === maxHistoryLoads || !extras.invoke) return null;\n const window = readWindow(extras);\n if (!isSourceBelowLoadedWindow({ wireMessages: extras.state?.messages, sourceId, window })) {\n return null;\n }\n const requests = window === null ? null : pageInterestRequests(window, pageSize);\n if (requests === null) return null;\n await extras.invoke(DEEP_AGENT_INTEREST_PROTOCOL, 'set', [requests]);\n }\n return null;\n}\n"],"names":["hasStatewireThreadExtras","readDeepAgentMessageWindow","pageInterestRequests","DEEP_AGENT_INTEREST_PROTOCOL"],"mappings":";;;;;AAoFO,SAAS,6BAA6B,QAAkD;AAC7F,MAAI,CAACA,aAAAA,yBAAyB,MAAM,EAAG,QAAO;AAC9C,SAAO;AACT;AAWO,SAAS,2BAA2B;AAAA,EACzC;AAAA,EACA;AAAA,EACA,WAAW;AACb,GAI8B;;AAC5B,MAAI,CAAC,aAAc,QAAO;AAC1B,QAAM,QAAQ,aAAa,UAAU,CAAC,aAAY,mCAAS,QAAO,QAAQ;AAC1E,MAAI,QAAQ,EAAG,QAAO;AACtB,MAAI,UAAU,EAAG,QAAO,WAAW,SAAY;AAC/C,QAAM,YAAW,kBAAa,QAAQ,CAAC,MAAtB,mBAAyB;AAC1C,SAAO,OAAO,aAAa,WAAW,WAAW;AACnD;AAOO,SAAS,0BAA0B;AAAA,EACxC;AAAA,EACA;AAAA,EACA;AACF,GAIY;AACV,MAAI,CAAC,gBAAgB,WAAW,QAAQ,CAAC,OAAO,SAAU,QAAO;AACjE,QAAM,QAAQ,aAAa,UAAU,CAAC,aAAY,mCAAS,QAAO,QAAQ;AAC1E,SAAO,SAAS;AAClB;AAEA,MAAM,aAAa,CAAC,WAAA;;AAClBC,uBAAAA,4BAA2B,YAAO,cAAP,mBAAkB,MAAM;AAAA;AAQrD,SAAS,gBAAwB;AAC/B,QAAM,YAAa,WAA0D;AAC7E,MAAI,QAAO,uCAAW,gBAAe,WAAY,QAAO,UAAU,WAAA;AAClE,QAAM,MAAM,MAAc,KAAK,MAAM,KAAK,OAAA,IAAW,KAAO,EAAE,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG;AAC1F,SAAO,GAAG,IAAA,CAAK,GAAG,IAAA,CAAK,IAAI,IAAA,CAAK,KAAK,MAAM,MAAM,CAAC,CAAC,IAAI,IAAA,CAAK,IAAI,KAAK,GAAG,IAAA,CAAK,GAAG,IAAA,CAAK;AACvF;AAOO,SAAS,0BAA0B;AAAA,EACxC;AAAA,EACA;AAAA,EACA;AACF,GAI0C;;AACxC,QAAM,qBAAqB,2BAA2B;AAAA,IACpD,eAAc,YAAO,UAAP,mBAAc;AAAA,IAC5B;AAAA,IACA,YAAU,gBAAW,MAAM,MAAjB,mBAAoB,aAAY;AAAA,EAAA,CAC3C;AACD,MAAI,uBAAuB,OAAW,QAAO;AAC7C,SAAO;AAAA,IACL,MAAM;AAAA,IACN,OAAO,OAAO,KAAK,SAAS,cAAA;AAAA,IAC5B,SAAS,EAAE,IAAI,cAAA,GAAiB,MAAM,QAAQ,OAAO,CAAC,EAAE,MAAM,QAAQ,KAAA,CAAM,EAAA;AAAA,IAC5E;AAAA,IACA;AAAA,EAAA;AAEJ;AAiCO,MAAM,gCAAgC;AAgB7C,eAAsB,4BAA4B;AAAA,EAChD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,kBAAkB;AACpB,GAOmD;;AACjD,WAAS,OAAO,GAAG,QAAQ,iBAAiB,QAAQ,GAAG;AACrD,UAAM,SAAS,WAAA;AACf,QAAI,CAAC,OAAQ,QAAO;AACpB,UAAM,UAAU,0BAA0B,EAAE,QAAQ,UAAU,MAAM;AACpE,QAAI,QAAS,QAAO;AACpB,QAAI,SAAS,mBAAmB,CAAC,OAAO,OAAQ,QAAO;AACvD,UAAM,SAAS,WAAW,MAAM;AAChC,QAAI,CAAC,0BAA0B,EAAE,eAAc,YAAO,UAAP,mBAAc,UAAU,UAAU,OAAA,CAAQ,GAAG;AAC1F,aAAO;AAAA,IACT;AACA,UAAM,WAAW,WAAW,OAAO,OAAOC,cAAAA,qBAAqB,QAAQ,QAAQ;AAC/E,QAAI,aAAa,KAAM,QAAO;AAC9B,UAAM,OAAO,OAAOC,kBAAAA,8BAA8B,OAAO,CAAC,QAAQ,CAAC;AAAA,EACrE;AACA,SAAO;AACT;;;;;;;"}
|
|
1
|
+
{"version":3,"file":"edit-command.cjs","sources":["../../../../agent-runtime/src/edit-command.ts"],"sourcesContent":["import type { StatewireThread } from '@assistant-ui/react-statewire';\nimport { DEEP_AGENT_INTEREST_PROTOCOL } from './generated/runconfig-contract';\nimport {\n type DeepAgentMessageWindow,\n pageInterestRequests,\n readDeepAgentMessageWindow,\n} from './message-window';\nimport { hasStatewireThreadExtras } from './thread-extras';\n\n/**\n * Wire-accurate `run/edit` construction for deep-agent retry/edit, shared by\n * every surface that resubmits a user message.\n *\n * assistant-ui builds the command's `runAnchorMessageId` from the CONVERTED\n * thread messages (`resolveEditCommand`: `messages[index - 1].id`), but the\n * deep-agent host validates that anchor against the edited message's parent in\n * its RAW message list (`harness_sdk.linear_thread`: the entry before it in\n * `state[\"messages\"]`). Those two lists do not line up:\n *\n * - the converter chunks an assistant turn and its tool messages into ONE\n * thread message carrying only the FIRST wire id, so the anchor names the\n * head of the previous turn instead of its last tool message;\n * - `sanitizeLangChainMessages` drops hidden-from-frontend and unconvertible\n * entries, so a dropped tail message is invisible to the anchor too.\n *\n * Either way the host rejects the command with `wrong-anchor`\n * (`runAnchorMessageId … is not the source's parent`) AFTER the composer's\n * synchronous `send()` returned, so the retry/edit silently does nothing.\n *\n * Building the command here off the replicated raw state makes the anchor the\n * same value the host computes.\n *\n * The raw list is the loaded interest window, so the first loaded message's\n * parent is not in it while older messages exist — an edit there must not\n * anchor on `null` (the root, rejected `wrong-anchor`); a message below the\n * window has no parent on this client at all. {@link resolveDeepAgentEditCommand}\n * states older page interests until the parent is loaded.\n */\n\n/**\n * Host metadata frozen at submission, settling once the async capture does.\n * The message-metadata preparation surface — `prepareMessageMetadata`,\n * `discardPreparedMessageMetadata`, `sendPreparedEdit` and `sendCommand`'s\n * `preparedMetadata` option — is what the monorepo patches into\n * `@assistant-ui/react-statewire@0.31.0` (`patches/`). It is declared\n * structurally here rather than as `StatewireThread.Extras[...]` so that the\n * published sources also compile against the unpatched upstream typings, where\n * those members are absent and the edit path's optional calls never run.\n */\nexport type DeepAgentPreparedMessageMetadata = Promise<Record<string, unknown> | undefined>;\n\n/** The pieces of the statewire thread extras the edit path reads. */\nexport interface DeepAgentStatewireExtras {\n /** The library's dispatch; it resolves with the host's reply to the command. */\n sendCommand: (\n command: StatewireThread.Command,\n options?: { preparedMetadata?: DeepAgentPreparedMessageMetadata | undefined },\n ) => Promise<unknown> | void;\n /** Optional on older hosts; capture before loading an edit's history anchor. */\n prepareMessageMetadata?: (() => DeepAgentPreparedMessageMetadata) | undefined;\n discardPreparedMessageMetadata?:\n | ((prepared: DeepAgentPreparedMessageMetadata) => void)\n | undefined;\n sendPreparedEdit?:\n | ((sourceId: string, prepared: DeepAgentPreparedMessageMetadata) => void)\n | undefined;\n isPreparingSubmission?: boolean;\n /** The composed thread state — its `messages` are the raw wire messages of the loaded window. */\n state: { messages?: readonly { id?: unknown }[] } | null;\n /** The mounted documents (the interest window), absent on a wire without documents. */\n documents?: StatewireThread.Extras['documents'];\n invoke?: StatewireThread.Extras['invoke'];\n /** The live run, when one is in flight; `epoch` advances on each dispatch. */\n runs: { runId?: string | null; epoch?: number };\n /** The host's replicated run status; absent on a surface that predates it. */\n status?: StatewireThread.ThreadStatus;\n}\n\n/**\n * The statewire extras of the nearest aui thread, or `null` when the thread is\n * not a statewire thread (e.g. a read-only subagent transcript runtime).\n * Gated on {@link hasStatewireThreadExtras} for the same reason the approval\n * card is: the library's own hooks throw on a non-statewire thread.\n */\nexport function readDeepAgentStatewireExtras(extras: unknown): DeepAgentStatewireExtras | null {\n if (!hasStatewireThreadExtras(extras)) return null;\n return extras as DeepAgentStatewireExtras;\n}\n\n/**\n * The host's parent of `sourceId`: the raw wire message directly before it\n * (`null` when it is the first of the whole transcript). Returns `undefined`\n * when `sourceId` is not in the raw list at all — an optimistic bubble the\n * host has not settled yet, a message below the loaded window, or a surface\n * whose state this module cannot read — or when it is the first loaded\n * message while older ones exist (`hasOlder`), so the caller can load more or\n * fall back to the library's own path instead of asserting a wrong anchor.\n */\nexport function resolveWireAnchorMessageId({\n wireMessages,\n sourceId,\n hasOlder = false,\n}: {\n wireMessages: readonly { id?: unknown }[] | undefined;\n sourceId: string;\n hasOlder?: boolean;\n}): string | null | undefined {\n if (!wireMessages) return undefined;\n const index = wireMessages.findIndex((message) => message?.id === sourceId);\n if (index < 0) return undefined;\n if (index === 0) return hasOlder ? undefined : null;\n const parentId = wireMessages[index - 1]?.id;\n return typeof parentId === 'string' ? parentId : undefined;\n}\n\n/**\n * Whether `sourceId`'s parent lies below the loaded window: the message is\n * absent from the raw list, or its first entry, while older messages exist.\n * The caller loads older history before an edit can be anchored.\n */\nexport function isSourceBelowLoadedWindow({\n wireMessages,\n sourceId,\n window,\n}: {\n wireMessages: readonly { id?: unknown }[] | undefined;\n sourceId: string;\n window: DeepAgentMessageWindow | null;\n}): boolean {\n if (!wireMessages || window === null || !window.hasOlder) return false;\n const index = wireMessages.findIndex((message) => message?.id === sourceId);\n return index <= 0;\n}\n\nconst readWindow = (extras: DeepAgentStatewireExtras) =>\n readDeepAgentMessageWindow(extras.documents?.list());\n\n/**\n * A fresh command / message id (uniqueness, not secrecy). `crypto.randomUUID`\n * where the host runtime has it — every browser, Bun, Node and Hermes with\n * webcrypto — and a v4-shaped fallback elsewhere, so the shared path never\n * throws on a runtime without full webcrypto.\n */\nfunction mintCommandId(): string {\n const webcrypto = (globalThis as { crypto?: { randomUUID?: () => string } }).crypto;\n if (typeof webcrypto?.randomUUID === 'function') return webcrypto.randomUUID();\n const hex = (): string => Math.floor(Math.random() * 0x10000).toString(16).padStart(4, '0');\n return `${hex()}${hex()}-${hex()}-4${hex().slice(1)}-${hex()}-${hex()}${hex()}${hex()}`;\n}\n\n/**\n * The `run/edit` command for resubmitting `sourceId` with `text`, anchored on\n * the host's own parent. `null` when the anchor cannot be resolved from the\n * replicated state.\n */\nexport function buildDeepAgentEditCommand({\n extras,\n sourceId,\n text,\n}: {\n extras: DeepAgentStatewireExtras;\n sourceId: string;\n text: string;\n}): StatewireThread.RunEditCommand | null {\n const runAnchorMessageId = resolveWireAnchorMessageId({\n wireMessages: extras.state?.messages,\n sourceId,\n hasOlder: readWindow(extras)?.hasOlder ?? false,\n });\n if (runAnchorMessageId === undefined) return null;\n return {\n type: 'run/edit',\n runId: extras.runs.runId ?? mintCommandId(),\n message: { id: mintCommandId(), role: 'user', parts: [{ type: 'text', text }] },\n sourceId,\n runAnchorMessageId,\n };\n}\n\n/**\n * Whether a windowed statewire thread has older history than it has loaded:\n * `null` while that is not known yet (the head interest has not resolved; a\n * remembered transcript can show from cache before it does), and `false` for\n * a thread that is not a statewire thread, so a reader looking for an older\n * message waits for the window, or stops.\n */\nexport function deepAgentHistoryHasOlder(extras: unknown): boolean | null {\n const typed = readDeepAgentStatewireExtras(extras);\n if (typed === null) return false;\n return readWindow(typed)?.hasOlder ?? null;\n}\n\n/**\n * State one more page of older history for a windowed thread: the next page\n * past the loaded window's oldest message. Resolves `true` once the host has\n * mounted it (it answers the statement after mounting), `false` when there is\n * nothing older, the thread is not a windowed statewire thread, or no page can\n * be stated. For a reader that needs a message below the loaded window — a\n * search result revealing an older turn of a long conversation — and pages\n * until it renders.\n */\nexport async function loadOlderDeepAgentHistoryPage({\n readExtras,\n pageSize,\n}: {\n readExtras: () => DeepAgentStatewireExtras | null;\n pageSize?: number;\n}): Promise<boolean> {\n const extras = readExtras();\n if (!extras?.invoke) return false;\n const window = readWindow(extras);\n if (window === null || !window.hasOlder) return false;\n const requests = pageInterestRequests(window, pageSize);\n if (requests === null) return false;\n await extras.invoke(DEEP_AGENT_INTEREST_PROTOCOL, 'set', [requests]);\n return true;\n}\n\n/**\n * Pages of older history stated while looking for an edited message's host\n * parent. Each page is up to a window's worth of messages, so an edit reaches\n * this many windows below what was loaded before it gives up.\n */\nexport const MAX_EDIT_ANCHOR_HISTORY_LOADS = 5;\n\n/**\n * The anchored `run/edit` for `sourceId`, stating older page interests one at\n * a time when the message's parent lies below the loaded window. Every page\n * is read back from the live documents (`readExtras` — the host mounts them\n * before it answers the statement), so the anchor is always the host's own\n * parent id.\n *\n * Resolves `null` — the caller's cue for the library's composer path, whose\n * failure it reports — when the thread is not a statewire thread, the host has\n * not settled the message, the history is exhausted without it, or the page\n * budget ({@link MAX_EDIT_ANCHOR_HISTORY_LOADS}) is spent. A synchronous\n * resolution never awaits: the first read either anchors at once or decides\n * that pages are needed.\n */\nexport async function resolveDeepAgentEditCommand({\n readExtras,\n sourceId,\n text,\n pageSize,\n maxHistoryLoads = MAX_EDIT_ANCHOR_HISTORY_LOADS,\n}: {\n /** The live thread extras — re-read per page so each anchor attempt sees the widened window. */\n readExtras: () => DeepAgentStatewireExtras | null;\n sourceId: string;\n text: string;\n pageSize?: number;\n maxHistoryLoads?: number;\n}): Promise<StatewireThread.RunEditCommand | null> {\n for (let page = 0; page <= maxHistoryLoads; page += 1) {\n const extras = readExtras();\n if (!extras) return null;\n const command = buildDeepAgentEditCommand({ extras, sourceId, text });\n if (command) return command;\n if (page === maxHistoryLoads || !extras.invoke) return null;\n const window = readWindow(extras);\n if (!isSourceBelowLoadedWindow({ wireMessages: extras.state?.messages, sourceId, window })) {\n return null;\n }\n const requests = window === null ? null : pageInterestRequests(window, pageSize);\n if (requests === null) return null;\n await extras.invoke(DEEP_AGENT_INTEREST_PROTOCOL, 'set', [requests]);\n }\n return null;\n}\n"],"names":["hasStatewireThreadExtras","readDeepAgentMessageWindow","pageInterestRequests","DEEP_AGENT_INTEREST_PROTOCOL"],"mappings":"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"}
|
|
@@ -113,6 +113,14 @@ export declare function buildDeepAgentEditCommand({ extras, sourceId, text, }: {
|
|
|
113
113
|
sourceId: string;
|
|
114
114
|
text: string;
|
|
115
115
|
}): StatewireThread.RunEditCommand | null;
|
|
116
|
+
/**
|
|
117
|
+
* Whether a windowed statewire thread has older history than it has loaded:
|
|
118
|
+
* `null` while that is not known yet (the head interest has not resolved; a
|
|
119
|
+
* remembered transcript can show from cache before it does), and `false` for
|
|
120
|
+
* a thread that is not a statewire thread, so a reader looking for an older
|
|
121
|
+
* message waits for the window, or stops.
|
|
122
|
+
*/
|
|
123
|
+
export declare function deepAgentHistoryHasOlder(extras: unknown): boolean | null;
|
|
116
124
|
/**
|
|
117
125
|
* State one more page of older history for a windowed thread: the next page
|
|
118
126
|
* past the loaded window's oldest message. Resolves `true` once the host has
|
|
@@ -57,6 +57,25 @@ function buildDeepAgentEditCommand({
|
|
|
57
57
|
runAnchorMessageId
|
|
58
58
|
};
|
|
59
59
|
}
|
|
60
|
+
function deepAgentHistoryHasOlder(extras) {
|
|
61
|
+
var _a;
|
|
62
|
+
const typed = readDeepAgentStatewireExtras(extras);
|
|
63
|
+
if (typed === null) return false;
|
|
64
|
+
return ((_a = readWindow(typed)) == null ? void 0 : _a.hasOlder) ?? null;
|
|
65
|
+
}
|
|
66
|
+
async function loadOlderDeepAgentHistoryPage({
|
|
67
|
+
readExtras,
|
|
68
|
+
pageSize
|
|
69
|
+
}) {
|
|
70
|
+
const extras = readExtras();
|
|
71
|
+
if (!(extras == null ? void 0 : extras.invoke)) return false;
|
|
72
|
+
const window = readWindow(extras);
|
|
73
|
+
if (window === null || !window.hasOlder) return false;
|
|
74
|
+
const requests = pageInterestRequests(window, pageSize);
|
|
75
|
+
if (requests === null) return false;
|
|
76
|
+
await extras.invoke(DEEP_AGENT_INTEREST_PROTOCOL, "set", [requests]);
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
60
79
|
const MAX_EDIT_ANCHOR_HISTORY_LOADS = 5;
|
|
61
80
|
async function resolveDeepAgentEditCommand({
|
|
62
81
|
readExtras,
|
|
@@ -85,7 +104,9 @@ async function resolveDeepAgentEditCommand({
|
|
|
85
104
|
export {
|
|
86
105
|
MAX_EDIT_ANCHOR_HISTORY_LOADS,
|
|
87
106
|
buildDeepAgentEditCommand,
|
|
107
|
+
deepAgentHistoryHasOlder,
|
|
88
108
|
isSourceBelowLoadedWindow,
|
|
109
|
+
loadOlderDeepAgentHistoryPage,
|
|
89
110
|
readDeepAgentStatewireExtras,
|
|
90
111
|
resolveDeepAgentEditCommand,
|
|
91
112
|
resolveWireAnchorMessageId
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"edit-command.js","sources":["../../../../agent-runtime/src/edit-command.ts"],"sourcesContent":["import type { StatewireThread } from '@assistant-ui/react-statewire';\nimport { DEEP_AGENT_INTEREST_PROTOCOL } from './generated/runconfig-contract';\nimport {\n type DeepAgentMessageWindow,\n pageInterestRequests,\n readDeepAgentMessageWindow,\n} from './message-window';\nimport { hasStatewireThreadExtras } from './thread-extras';\n\n/**\n * Wire-accurate `run/edit` construction for deep-agent retry/edit, shared by\n * every surface that resubmits a user message.\n *\n * assistant-ui builds the command's `runAnchorMessageId` from the CONVERTED\n * thread messages (`resolveEditCommand`: `messages[index - 1].id`), but the\n * deep-agent host validates that anchor against the edited message's parent in\n * its RAW message list (`harness_sdk.linear_thread`: the entry before it in\n * `state[\"messages\"]`). Those two lists do not line up:\n *\n * - the converter chunks an assistant turn and its tool messages into ONE\n * thread message carrying only the FIRST wire id, so the anchor names the\n * head of the previous turn instead of its last tool message;\n * - `sanitizeLangChainMessages` drops hidden-from-frontend and unconvertible\n * entries, so a dropped tail message is invisible to the anchor too.\n *\n * Either way the host rejects the command with `wrong-anchor`\n * (`runAnchorMessageId … is not the source's parent`) AFTER the composer's\n * synchronous `send()` returned, so the retry/edit silently does nothing.\n *\n * Building the command here off the replicated raw state makes the anchor the\n * same value the host computes.\n *\n * The raw list is the loaded interest window, so the first loaded message's\n * parent is not in it while older messages exist — an edit there must not\n * anchor on `null` (the root, rejected `wrong-anchor`); a message below the\n * window has no parent on this client at all. {@link resolveDeepAgentEditCommand}\n * states older page interests until the parent is loaded.\n */\n\n/**\n * Host metadata frozen at submission, settling once the async capture does.\n * The message-metadata preparation surface — `prepareMessageMetadata`,\n * `discardPreparedMessageMetadata`, `sendPreparedEdit` and `sendCommand`'s\n * `preparedMetadata` option — is what the monorepo patches into\n * `@assistant-ui/react-statewire@0.31.0` (`patches/`). It is declared\n * structurally here rather than as `StatewireThread.Extras[...]` so that the\n * published sources also compile against the unpatched upstream typings, where\n * those members are absent and the edit path's optional calls never run.\n */\nexport type DeepAgentPreparedMessageMetadata = Promise<Record<string, unknown> | undefined>;\n\n/** The pieces of the statewire thread extras the edit path reads. */\nexport interface DeepAgentStatewireExtras {\n /** The library's dispatch; it resolves with the host's reply to the command. */\n sendCommand: (\n command: StatewireThread.Command,\n options?: { preparedMetadata?: DeepAgentPreparedMessageMetadata | undefined },\n ) => Promise<unknown> | void;\n /** Optional on older hosts; capture before loading an edit's history anchor. */\n prepareMessageMetadata?: (() => DeepAgentPreparedMessageMetadata) | undefined;\n discardPreparedMessageMetadata?:\n | ((prepared: DeepAgentPreparedMessageMetadata) => void)\n | undefined;\n sendPreparedEdit?:\n | ((sourceId: string, prepared: DeepAgentPreparedMessageMetadata) => void)\n | undefined;\n isPreparingSubmission?: boolean;\n /** The composed thread state — its `messages` are the raw wire messages of the loaded window. */\n state: { messages?: readonly { id?: unknown }[] } | null;\n /** The mounted documents (the interest window), absent on a wire without documents. */\n documents?: StatewireThread.Extras['documents'];\n invoke?: StatewireThread.Extras['invoke'];\n /** The live run, when one is in flight; `epoch` advances on each dispatch. */\n runs: { runId?: string | null; epoch?: number };\n /** The host's replicated run status; absent on a surface that predates it. */\n status?: StatewireThread.ThreadStatus;\n}\n\n/**\n * The statewire extras of the nearest aui thread, or `null` when the thread is\n * not a statewire thread (e.g. a read-only subagent transcript runtime).\n * Gated on {@link hasStatewireThreadExtras} for the same reason the approval\n * card is: the library's own hooks throw on a non-statewire thread.\n */\nexport function readDeepAgentStatewireExtras(extras: unknown): DeepAgentStatewireExtras | null {\n if (!hasStatewireThreadExtras(extras)) return null;\n return extras as DeepAgentStatewireExtras;\n}\n\n/**\n * The host's parent of `sourceId`: the raw wire message directly before it\n * (`null` when it is the first of the whole transcript). Returns `undefined`\n * when `sourceId` is not in the raw list at all — an optimistic bubble the\n * host has not settled yet, a message below the loaded window, or a surface\n * whose state this module cannot read — or when it is the first loaded\n * message while older ones exist (`hasOlder`), so the caller can load more or\n * fall back to the library's own path instead of asserting a wrong anchor.\n */\nexport function resolveWireAnchorMessageId({\n wireMessages,\n sourceId,\n hasOlder = false,\n}: {\n wireMessages: readonly { id?: unknown }[] | undefined;\n sourceId: string;\n hasOlder?: boolean;\n}): string | null | undefined {\n if (!wireMessages) return undefined;\n const index = wireMessages.findIndex((message) => message?.id === sourceId);\n if (index < 0) return undefined;\n if (index === 0) return hasOlder ? undefined : null;\n const parentId = wireMessages[index - 1]?.id;\n return typeof parentId === 'string' ? parentId : undefined;\n}\n\n/**\n * Whether `sourceId`'s parent lies below the loaded window: the message is\n * absent from the raw list, or its first entry, while older messages exist.\n * The caller loads older history before an edit can be anchored.\n */\nexport function isSourceBelowLoadedWindow({\n wireMessages,\n sourceId,\n window,\n}: {\n wireMessages: readonly { id?: unknown }[] | undefined;\n sourceId: string;\n window: DeepAgentMessageWindow | null;\n}): boolean {\n if (!wireMessages || window === null || !window.hasOlder) return false;\n const index = wireMessages.findIndex((message) => message?.id === sourceId);\n return index <= 0;\n}\n\nconst readWindow = (extras: DeepAgentStatewireExtras) =>\n readDeepAgentMessageWindow(extras.documents?.list());\n\n/**\n * A fresh command / message id (uniqueness, not secrecy). `crypto.randomUUID`\n * where the host runtime has it — every browser, Bun, Node and Hermes with\n * webcrypto — and a v4-shaped fallback elsewhere, so the shared path never\n * throws on a runtime without full webcrypto.\n */\nfunction mintCommandId(): string {\n const webcrypto = (globalThis as { crypto?: { randomUUID?: () => string } }).crypto;\n if (typeof webcrypto?.randomUUID === 'function') return webcrypto.randomUUID();\n const hex = (): string => Math.floor(Math.random() * 0x10000).toString(16).padStart(4, '0');\n return `${hex()}${hex()}-${hex()}-4${hex().slice(1)}-${hex()}-${hex()}${hex()}${hex()}`;\n}\n\n/**\n * The `run/edit` command for resubmitting `sourceId` with `text`, anchored on\n * the host's own parent. `null` when the anchor cannot be resolved from the\n * replicated state.\n */\nexport function buildDeepAgentEditCommand({\n extras,\n sourceId,\n text,\n}: {\n extras: DeepAgentStatewireExtras;\n sourceId: string;\n text: string;\n}): StatewireThread.RunEditCommand | null {\n const runAnchorMessageId = resolveWireAnchorMessageId({\n wireMessages: extras.state?.messages,\n sourceId,\n hasOlder: readWindow(extras)?.hasOlder ?? false,\n });\n if (runAnchorMessageId === undefined) return null;\n return {\n type: 'run/edit',\n runId: extras.runs.runId ?? mintCommandId(),\n message: { id: mintCommandId(), role: 'user', parts: [{ type: 'text', text }] },\n sourceId,\n runAnchorMessageId,\n };\n}\n\n/**\n * State one more page of older history for a windowed thread: the next page\n * past the loaded window's oldest message. Resolves `true` once the host has\n * mounted it (it answers the statement after mounting), `false` when there is\n * nothing older, the thread is not a windowed statewire thread, or no page can\n * be stated. For a reader that needs a message below the loaded window — a\n * search result revealing an older turn of a long conversation — and pages\n * until it renders.\n */\nexport async function loadOlderDeepAgentHistoryPage({\n readExtras,\n pageSize,\n}: {\n readExtras: () => DeepAgentStatewireExtras | null;\n pageSize?: number;\n}): Promise<boolean> {\n const extras = readExtras();\n if (!extras?.invoke) return false;\n const window = readWindow(extras);\n if (window === null || !window.hasOlder) return false;\n const requests = pageInterestRequests(window, pageSize);\n if (requests === null) return false;\n await extras.invoke(DEEP_AGENT_INTEREST_PROTOCOL, 'set', [requests]);\n return true;\n}\n\n/**\n * Pages of older history stated while looking for an edited message's host\n * parent. Each page is up to a window's worth of messages, so an edit reaches\n * this many windows below what was loaded before it gives up.\n */\nexport const MAX_EDIT_ANCHOR_HISTORY_LOADS = 5;\n\n/**\n * The anchored `run/edit` for `sourceId`, stating older page interests one at\n * a time when the message's parent lies below the loaded window. Every page\n * is read back from the live documents (`readExtras` — the host mounts them\n * before it answers the statement), so the anchor is always the host's own\n * parent id.\n *\n * Resolves `null` — the caller's cue for the library's composer path, whose\n * failure it reports — when the thread is not a statewire thread, the host has\n * not settled the message, the history is exhausted without it, or the page\n * budget ({@link MAX_EDIT_ANCHOR_HISTORY_LOADS}) is spent. A synchronous\n * resolution never awaits: the first read either anchors at once or decides\n * that pages are needed.\n */\nexport async function resolveDeepAgentEditCommand({\n readExtras,\n sourceId,\n text,\n pageSize,\n maxHistoryLoads = MAX_EDIT_ANCHOR_HISTORY_LOADS,\n}: {\n /** The live thread extras — re-read per page so each anchor attempt sees the widened window. */\n readExtras: () => DeepAgentStatewireExtras | null;\n sourceId: string;\n text: string;\n pageSize?: number;\n maxHistoryLoads?: number;\n}): Promise<StatewireThread.RunEditCommand | null> {\n for (let page = 0; page <= maxHistoryLoads; page += 1) {\n const extras = readExtras();\n if (!extras) return null;\n const command = buildDeepAgentEditCommand({ extras, sourceId, text });\n if (command) return command;\n if (page === maxHistoryLoads || !extras.invoke) return null;\n const window = readWindow(extras);\n if (!isSourceBelowLoadedWindow({ wireMessages: extras.state?.messages, sourceId, window })) {\n return null;\n }\n const requests = window === null ? null : pageInterestRequests(window, pageSize);\n if (requests === null) return null;\n await extras.invoke(DEEP_AGENT_INTEREST_PROTOCOL, 'set', [requests]);\n }\n return null;\n}\n"],"names":[],"mappings":";;;AAoFO,SAAS,6BAA6B,QAAkD;AAC7F,MAAI,CAAC,yBAAyB,MAAM,EAAG,QAAO;AAC9C,SAAO;AACT;AAWO,SAAS,2BAA2B;AAAA,EACzC;AAAA,EACA;AAAA,EACA,WAAW;AACb,GAI8B;;AAC5B,MAAI,CAAC,aAAc,QAAO;AAC1B,QAAM,QAAQ,aAAa,UAAU,CAAC,aAAY,mCAAS,QAAO,QAAQ;AAC1E,MAAI,QAAQ,EAAG,QAAO;AACtB,MAAI,UAAU,EAAG,QAAO,WAAW,SAAY;AAC/C,QAAM,YAAW,kBAAa,QAAQ,CAAC,MAAtB,mBAAyB;AAC1C,SAAO,OAAO,aAAa,WAAW,WAAW;AACnD;AAOO,SAAS,0BAA0B;AAAA,EACxC;AAAA,EACA;AAAA,EACA;AACF,GAIY;AACV,MAAI,CAAC,gBAAgB,WAAW,QAAQ,CAAC,OAAO,SAAU,QAAO;AACjE,QAAM,QAAQ,aAAa,UAAU,CAAC,aAAY,mCAAS,QAAO,QAAQ;AAC1E,SAAO,SAAS;AAClB;AAEA,MAAM,aAAa,CAAC,WAAA;;AAClB,qCAA2B,YAAO,cAAP,mBAAkB,MAAM;AAAA;AAQrD,SAAS,gBAAwB;AAC/B,QAAM,YAAa,WAA0D;AAC7E,MAAI,QAAO,uCAAW,gBAAe,WAAY,QAAO,UAAU,WAAA;AAClE,QAAM,MAAM,MAAc,KAAK,MAAM,KAAK,OAAA,IAAW,KAAO,EAAE,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG;AAC1F,SAAO,GAAG,IAAA,CAAK,GAAG,IAAA,CAAK,IAAI,IAAA,CAAK,KAAK,MAAM,MAAM,CAAC,CAAC,IAAI,IAAA,CAAK,IAAI,KAAK,GAAG,IAAA,CAAK,GAAG,IAAA,CAAK;AACvF;AAOO,SAAS,0BAA0B;AAAA,EACxC;AAAA,EACA;AAAA,EACA;AACF,GAI0C;;AACxC,QAAM,qBAAqB,2BAA2B;AAAA,IACpD,eAAc,YAAO,UAAP,mBAAc;AAAA,IAC5B;AAAA,IACA,YAAU,gBAAW,MAAM,MAAjB,mBAAoB,aAAY;AAAA,EAAA,CAC3C;AACD,MAAI,uBAAuB,OAAW,QAAO;AAC7C,SAAO;AAAA,IACL,MAAM;AAAA,IACN,OAAO,OAAO,KAAK,SAAS,cAAA;AAAA,IAC5B,SAAS,EAAE,IAAI,cAAA,GAAiB,MAAM,QAAQ,OAAO,CAAC,EAAE,MAAM,QAAQ,KAAA,CAAM,EAAA;AAAA,IAC5E;AAAA,IACA;AAAA,EAAA;AAEJ;AAiCO,MAAM,gCAAgC;AAgB7C,eAAsB,4BAA4B;AAAA,EAChD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,kBAAkB;AACpB,GAOmD;;AACjD,WAAS,OAAO,GAAG,QAAQ,iBAAiB,QAAQ,GAAG;AACrD,UAAM,SAAS,WAAA;AACf,QAAI,CAAC,OAAQ,QAAO;AACpB,UAAM,UAAU,0BAA0B,EAAE,QAAQ,UAAU,MAAM;AACpE,QAAI,QAAS,QAAO;AACpB,QAAI,SAAS,mBAAmB,CAAC,OAAO,OAAQ,QAAO;AACvD,UAAM,SAAS,WAAW,MAAM;AAChC,QAAI,CAAC,0BAA0B,EAAE,eAAc,YAAO,UAAP,mBAAc,UAAU,UAAU,OAAA,CAAQ,GAAG;AAC1F,aAAO;AAAA,IACT;AACA,UAAM,WAAW,WAAW,OAAO,OAAO,qBAAqB,QAAQ,QAAQ;AAC/E,QAAI,aAAa,KAAM,QAAO;AAC9B,UAAM,OAAO,OAAO,8BAA8B,OAAO,CAAC,QAAQ,CAAC;AAAA,EACrE;AACA,SAAO;AACT;"}
|
|
1
|
+
{"version":3,"file":"edit-command.js","sources":["../../../../agent-runtime/src/edit-command.ts"],"sourcesContent":["import type { StatewireThread } from '@assistant-ui/react-statewire';\nimport { DEEP_AGENT_INTEREST_PROTOCOL } from './generated/runconfig-contract';\nimport {\n type DeepAgentMessageWindow,\n pageInterestRequests,\n readDeepAgentMessageWindow,\n} from './message-window';\nimport { hasStatewireThreadExtras } from './thread-extras';\n\n/**\n * Wire-accurate `run/edit` construction for deep-agent retry/edit, shared by\n * every surface that resubmits a user message.\n *\n * assistant-ui builds the command's `runAnchorMessageId` from the CONVERTED\n * thread messages (`resolveEditCommand`: `messages[index - 1].id`), but the\n * deep-agent host validates that anchor against the edited message's parent in\n * its RAW message list (`harness_sdk.linear_thread`: the entry before it in\n * `state[\"messages\"]`). Those two lists do not line up:\n *\n * - the converter chunks an assistant turn and its tool messages into ONE\n * thread message carrying only the FIRST wire id, so the anchor names the\n * head of the previous turn instead of its last tool message;\n * - `sanitizeLangChainMessages` drops hidden-from-frontend and unconvertible\n * entries, so a dropped tail message is invisible to the anchor too.\n *\n * Either way the host rejects the command with `wrong-anchor`\n * (`runAnchorMessageId … is not the source's parent`) AFTER the composer's\n * synchronous `send()` returned, so the retry/edit silently does nothing.\n *\n * Building the command here off the replicated raw state makes the anchor the\n * same value the host computes.\n *\n * The raw list is the loaded interest window, so the first loaded message's\n * parent is not in it while older messages exist — an edit there must not\n * anchor on `null` (the root, rejected `wrong-anchor`); a message below the\n * window has no parent on this client at all. {@link resolveDeepAgentEditCommand}\n * states older page interests until the parent is loaded.\n */\n\n/**\n * Host metadata frozen at submission, settling once the async capture does.\n * The message-metadata preparation surface — `prepareMessageMetadata`,\n * `discardPreparedMessageMetadata`, `sendPreparedEdit` and `sendCommand`'s\n * `preparedMetadata` option — is what the monorepo patches into\n * `@assistant-ui/react-statewire@0.31.0` (`patches/`). It is declared\n * structurally here rather than as `StatewireThread.Extras[...]` so that the\n * published sources also compile against the unpatched upstream typings, where\n * those members are absent and the edit path's optional calls never run.\n */\nexport type DeepAgentPreparedMessageMetadata = Promise<Record<string, unknown> | undefined>;\n\n/** The pieces of the statewire thread extras the edit path reads. */\nexport interface DeepAgentStatewireExtras {\n /** The library's dispatch; it resolves with the host's reply to the command. */\n sendCommand: (\n command: StatewireThread.Command,\n options?: { preparedMetadata?: DeepAgentPreparedMessageMetadata | undefined },\n ) => Promise<unknown> | void;\n /** Optional on older hosts; capture before loading an edit's history anchor. */\n prepareMessageMetadata?: (() => DeepAgentPreparedMessageMetadata) | undefined;\n discardPreparedMessageMetadata?:\n | ((prepared: DeepAgentPreparedMessageMetadata) => void)\n | undefined;\n sendPreparedEdit?:\n | ((sourceId: string, prepared: DeepAgentPreparedMessageMetadata) => void)\n | undefined;\n isPreparingSubmission?: boolean;\n /** The composed thread state — its `messages` are the raw wire messages of the loaded window. */\n state: { messages?: readonly { id?: unknown }[] } | null;\n /** The mounted documents (the interest window), absent on a wire without documents. */\n documents?: StatewireThread.Extras['documents'];\n invoke?: StatewireThread.Extras['invoke'];\n /** The live run, when one is in flight; `epoch` advances on each dispatch. */\n runs: { runId?: string | null; epoch?: number };\n /** The host's replicated run status; absent on a surface that predates it. */\n status?: StatewireThread.ThreadStatus;\n}\n\n/**\n * The statewire extras of the nearest aui thread, or `null` when the thread is\n * not a statewire thread (e.g. a read-only subagent transcript runtime).\n * Gated on {@link hasStatewireThreadExtras} for the same reason the approval\n * card is: the library's own hooks throw on a non-statewire thread.\n */\nexport function readDeepAgentStatewireExtras(extras: unknown): DeepAgentStatewireExtras | null {\n if (!hasStatewireThreadExtras(extras)) return null;\n return extras as DeepAgentStatewireExtras;\n}\n\n/**\n * The host's parent of `sourceId`: the raw wire message directly before it\n * (`null` when it is the first of the whole transcript). Returns `undefined`\n * when `sourceId` is not in the raw list at all — an optimistic bubble the\n * host has not settled yet, a message below the loaded window, or a surface\n * whose state this module cannot read — or when it is the first loaded\n * message while older ones exist (`hasOlder`), so the caller can load more or\n * fall back to the library's own path instead of asserting a wrong anchor.\n */\nexport function resolveWireAnchorMessageId({\n wireMessages,\n sourceId,\n hasOlder = false,\n}: {\n wireMessages: readonly { id?: unknown }[] | undefined;\n sourceId: string;\n hasOlder?: boolean;\n}): string | null | undefined {\n if (!wireMessages) return undefined;\n const index = wireMessages.findIndex((message) => message?.id === sourceId);\n if (index < 0) return undefined;\n if (index === 0) return hasOlder ? undefined : null;\n const parentId = wireMessages[index - 1]?.id;\n return typeof parentId === 'string' ? parentId : undefined;\n}\n\n/**\n * Whether `sourceId`'s parent lies below the loaded window: the message is\n * absent from the raw list, or its first entry, while older messages exist.\n * The caller loads older history before an edit can be anchored.\n */\nexport function isSourceBelowLoadedWindow({\n wireMessages,\n sourceId,\n window,\n}: {\n wireMessages: readonly { id?: unknown }[] | undefined;\n sourceId: string;\n window: DeepAgentMessageWindow | null;\n}): boolean {\n if (!wireMessages || window === null || !window.hasOlder) return false;\n const index = wireMessages.findIndex((message) => message?.id === sourceId);\n return index <= 0;\n}\n\nconst readWindow = (extras: DeepAgentStatewireExtras) =>\n readDeepAgentMessageWindow(extras.documents?.list());\n\n/**\n * A fresh command / message id (uniqueness, not secrecy). `crypto.randomUUID`\n * where the host runtime has it — every browser, Bun, Node and Hermes with\n * webcrypto — and a v4-shaped fallback elsewhere, so the shared path never\n * throws on a runtime without full webcrypto.\n */\nfunction mintCommandId(): string {\n const webcrypto = (globalThis as { crypto?: { randomUUID?: () => string } }).crypto;\n if (typeof webcrypto?.randomUUID === 'function') return webcrypto.randomUUID();\n const hex = (): string => Math.floor(Math.random() * 0x10000).toString(16).padStart(4, '0');\n return `${hex()}${hex()}-${hex()}-4${hex().slice(1)}-${hex()}-${hex()}${hex()}${hex()}`;\n}\n\n/**\n * The `run/edit` command for resubmitting `sourceId` with `text`, anchored on\n * the host's own parent. `null` when the anchor cannot be resolved from the\n * replicated state.\n */\nexport function buildDeepAgentEditCommand({\n extras,\n sourceId,\n text,\n}: {\n extras: DeepAgentStatewireExtras;\n sourceId: string;\n text: string;\n}): StatewireThread.RunEditCommand | null {\n const runAnchorMessageId = resolveWireAnchorMessageId({\n wireMessages: extras.state?.messages,\n sourceId,\n hasOlder: readWindow(extras)?.hasOlder ?? false,\n });\n if (runAnchorMessageId === undefined) return null;\n return {\n type: 'run/edit',\n runId: extras.runs.runId ?? mintCommandId(),\n message: { id: mintCommandId(), role: 'user', parts: [{ type: 'text', text }] },\n sourceId,\n runAnchorMessageId,\n };\n}\n\n/**\n * Whether a windowed statewire thread has older history than it has loaded:\n * `null` while that is not known yet (the head interest has not resolved; a\n * remembered transcript can show from cache before it does), and `false` for\n * a thread that is not a statewire thread, so a reader looking for an older\n * message waits for the window, or stops.\n */\nexport function deepAgentHistoryHasOlder(extras: unknown): boolean | null {\n const typed = readDeepAgentStatewireExtras(extras);\n if (typed === null) return false;\n return readWindow(typed)?.hasOlder ?? null;\n}\n\n/**\n * State one more page of older history for a windowed thread: the next page\n * past the loaded window's oldest message. Resolves `true` once the host has\n * mounted it (it answers the statement after mounting), `false` when there is\n * nothing older, the thread is not a windowed statewire thread, or no page can\n * be stated. For a reader that needs a message below the loaded window — a\n * search result revealing an older turn of a long conversation — and pages\n * until it renders.\n */\nexport async function loadOlderDeepAgentHistoryPage({\n readExtras,\n pageSize,\n}: {\n readExtras: () => DeepAgentStatewireExtras | null;\n pageSize?: number;\n}): Promise<boolean> {\n const extras = readExtras();\n if (!extras?.invoke) return false;\n const window = readWindow(extras);\n if (window === null || !window.hasOlder) return false;\n const requests = pageInterestRequests(window, pageSize);\n if (requests === null) return false;\n await extras.invoke(DEEP_AGENT_INTEREST_PROTOCOL, 'set', [requests]);\n return true;\n}\n\n/**\n * Pages of older history stated while looking for an edited message's host\n * parent. Each page is up to a window's worth of messages, so an edit reaches\n * this many windows below what was loaded before it gives up.\n */\nexport const MAX_EDIT_ANCHOR_HISTORY_LOADS = 5;\n\n/**\n * The anchored `run/edit` for `sourceId`, stating older page interests one at\n * a time when the message's parent lies below the loaded window. Every page\n * is read back from the live documents (`readExtras` — the host mounts them\n * before it answers the statement), so the anchor is always the host's own\n * parent id.\n *\n * Resolves `null` — the caller's cue for the library's composer path, whose\n * failure it reports — when the thread is not a statewire thread, the host has\n * not settled the message, the history is exhausted without it, or the page\n * budget ({@link MAX_EDIT_ANCHOR_HISTORY_LOADS}) is spent. A synchronous\n * resolution never awaits: the first read either anchors at once or decides\n * that pages are needed.\n */\nexport async function resolveDeepAgentEditCommand({\n readExtras,\n sourceId,\n text,\n pageSize,\n maxHistoryLoads = MAX_EDIT_ANCHOR_HISTORY_LOADS,\n}: {\n /** The live thread extras — re-read per page so each anchor attempt sees the widened window. */\n readExtras: () => DeepAgentStatewireExtras | null;\n sourceId: string;\n text: string;\n pageSize?: number;\n maxHistoryLoads?: number;\n}): Promise<StatewireThread.RunEditCommand | null> {\n for (let page = 0; page <= maxHistoryLoads; page += 1) {\n const extras = readExtras();\n if (!extras) return null;\n const command = buildDeepAgentEditCommand({ extras, sourceId, text });\n if (command) return command;\n if (page === maxHistoryLoads || !extras.invoke) return null;\n const window = readWindow(extras);\n if (!isSourceBelowLoadedWindow({ wireMessages: extras.state?.messages, sourceId, window })) {\n return null;\n }\n const requests = window === null ? null : pageInterestRequests(window, pageSize);\n if (requests === null) return null;\n await extras.invoke(DEEP_AGENT_INTEREST_PROTOCOL, 'set', [requests]);\n }\n return null;\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -19,6 +19,23 @@ function subscribeHandoffArrivals(listener) {
|
|
|
19
19
|
listeners.delete(listener);
|
|
20
20
|
};
|
|
21
21
|
}
|
|
22
|
+
function settleHandoffArrivalCursor(threadId) {
|
|
23
|
+
const { arrivals } = registry();
|
|
24
|
+
const arrival = arrivals.get(threadId);
|
|
25
|
+
if (!arrival || arrival.cursor === null) return;
|
|
26
|
+
arrivals.set(threadId, { ...arrival, cursor: null });
|
|
27
|
+
publish();
|
|
28
|
+
}
|
|
29
|
+
function useHandoffArrivalCursor(threadId) {
|
|
30
|
+
return require$$0.useSyncExternalStore(
|
|
31
|
+
subscribeHandoffArrivals,
|
|
32
|
+
() => {
|
|
33
|
+
var _a;
|
|
34
|
+
return ((_a = readHandoffArrival(threadId)) == null ? void 0 : _a.cursor) ?? null;
|
|
35
|
+
},
|
|
36
|
+
() => null
|
|
37
|
+
);
|
|
38
|
+
}
|
|
22
39
|
function anchorHandoffArrival({
|
|
23
40
|
threadId,
|
|
24
41
|
afterMessageId
|
|
@@ -54,7 +71,9 @@ function useHandoffArrivalAfter(messageId) {
|
|
|
54
71
|
exports.HandoffArrivalContext = HandoffArrivalContext;
|
|
55
72
|
exports.anchorHandoffArrival = anchorHandoffArrival;
|
|
56
73
|
exports.readHandoffArrival = readHandoffArrival;
|
|
74
|
+
exports.settleHandoffArrivalCursor = settleHandoffArrivalCursor;
|
|
57
75
|
exports.subscribeHandoffArrivals = subscribeHandoffArrivals;
|
|
58
76
|
exports.useHandoffArrival = useHandoffArrival;
|
|
59
77
|
exports.useHandoffArrivalAfter = useHandoffArrivalAfter;
|
|
78
|
+
exports.useHandoffArrivalCursor = useHandoffArrivalCursor;
|
|
60
79
|
//# sourceMappingURL=handoff-arrival.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"handoff-arrival.cjs","sources":["../../../../agent-runtime/src/handoff-arrival.ts"],"sourcesContent":["/**\n * Where a handed-off conversation arrived on this surface: a transcript divider\n * (\"Continued from your phone\") after the last message that was already there\n * when the person picked it up.\n *\n * Ephemeral and client-side. A host records an arrival when its pick-up lands\n * (`recordHandoffArrival`); its transcript anchors it to the thread's last\n * message once that history has loaded (`useHandoffArrival`) and renders the\n * divider after that message (`useHandoffArrivalAfter`). Nothing is sent to\n * Athena or persisted; a reload forgets it.\n */\nimport { createContext, useContext, useEffect, useSyncExternalStore } from 'react';\nimport { continuedFromLabel } from './thread-handoff-protocol';\n\nexport interface HandoffArrival {\n threadId: string;\n /** \"Continued from your phone\". */\n label: string;\n /**\n * The last message present when the conversation arrived; the divider goes\n * right after it. `null` until the transcript has anchored it.\n */\n afterMessageId: string | null;\n}\n\n/** Enough for every conversation one person picks up in a session. */\nexport const MAX_HANDOFF_ARRIVALS = 20;\n\n/**\n * The arrivals live on `globalThis`, not in module scope, because two copies of\n * this module can run in one page: the SDK bundles agent-runtime into its dist\n * while a host (Spaces, the Chrome extension, Midas) imports it directly. The\n * host records through its copy and the SDK transcript reads through its own.\n */\nconst HANDOFF_ARRIVALS = Symbol.for('@athenaintel/agent-runtime/handoff-arrivals');\n\ninterface ArrivalRegistry {\n arrivals: Map<string, HandoffArrival>;\n listeners: Set<() => void>;\n}\n\ntype ArrivalRegistryHost = typeof globalThis & { [HANDOFF_ARRIVALS]?: ArrivalRegistry };\n\n// process-local: this client only — what this surface picked up; nothing is persisted\nfunction registry(): ArrivalRegistry {\n const host = globalThis as ArrivalRegistryHost;\n return (host[HANDOFF_ARRIVALS] ??= { arrivals: new Map(), listeners: new Set() });\n}\n\nfunction publish(): void {\n for (const listener of registry().listeners) listener();\n}\n\n/**\n * A pick-up landed `threadId` here from `source` (`mobile`, `chrome`, …)
|
|
1
|
+
{"version":3,"file":"handoff-arrival.cjs","sources":["../../../../agent-runtime/src/handoff-arrival.ts"],"sourcesContent":["/**\n * Where a handed-off conversation arrived on this surface: a transcript divider\n * (\"Continued from your phone\") after the last message that was already there\n * when the person picked it up.\n *\n * Ephemeral and client-side. A host records an arrival when its pick-up lands\n * (`recordHandoffArrival`); its transcript anchors it to the thread's last\n * message once that history has loaded (`useHandoffArrival`) and renders the\n * divider after that message (`useHandoffArrivalAfter`). Nothing is sent to\n * Athena or persisted; a reload forgets it.\n *\n * The arrival also carries where the source was reading (`cursor`, a message\n * id): the transcript opens at that message instead of the bottom, then\n * settles it (`settleHandoffArrivalCursor`) so a remount never scrolls again.\n */\nimport { createContext, useContext, useEffect, useSyncExternalStore } from 'react';\nimport { continuedFromLabel, parseHandoffCursor } from './thread-handoff-protocol';\n\nexport interface HandoffArrival {\n threadId: string;\n /** \"Continued from your phone\". */\n label: string;\n /**\n * The last message present when the conversation arrived; the divider goes\n * right after it. `null` until the transcript has anchored it.\n */\n afterMessageId: string | null;\n /**\n * The message the source was reading, which the transcript opens at. `null`\n * when the source was at the bottom, or once the transcript settled it.\n */\n cursor: string | null;\n}\n\n/** Enough for every conversation one person picks up in a session. */\nexport const MAX_HANDOFF_ARRIVALS = 20;\n\n/**\n * The arrivals live on `globalThis`, not in module scope, because two copies of\n * this module can run in one page: the SDK bundles agent-runtime into its dist\n * while a host (Spaces, the Chrome extension, Midas) imports it directly. The\n * host records through its copy and the SDK transcript reads through its own.\n */\nconst HANDOFF_ARRIVALS = Symbol.for('@athenaintel/agent-runtime/handoff-arrivals');\n\ninterface ArrivalRegistry {\n arrivals: Map<string, HandoffArrival>;\n listeners: Set<() => void>;\n}\n\ntype ArrivalRegistryHost = typeof globalThis & { [HANDOFF_ARRIVALS]?: ArrivalRegistry };\n\n// process-local: this client only — what this surface picked up; nothing is persisted\nfunction registry(): ArrivalRegistry {\n const host = globalThis as ArrivalRegistryHost;\n return (host[HANDOFF_ARRIVALS] ??= { arrivals: new Map(), listeners: new Set() });\n}\n\nfunction publish(): void {\n for (const listener of registry().listeners) listener();\n}\n\n/**\n * A pick-up landed `threadId` here from `source` (`mobile`, `chrome`, …),\n * where the source was reading `cursor` (a message id; absent or `null` at the\n * bottom). A second pick-up of the same thread moves the divider to the new\n * arrival and replaces the cursor.\n */\nexport function recordHandoffArrival({\n threadId,\n source,\n cursor,\n}: {\n threadId: string;\n source: string;\n cursor?: string | null;\n}): void {\n if (!threadId) return;\n const { arrivals } = registry();\n arrivals.delete(threadId);\n arrivals.set(threadId, {\n threadId,\n label: continuedFromLabel(source),\n afterMessageId: null,\n cursor: parseHandoffCursor(cursor),\n });\n while (arrivals.size > MAX_HANDOFF_ARRIVALS) {\n const oldest = arrivals.keys().next().value;\n if (oldest === undefined) break;\n arrivals.delete(oldest);\n }\n publish();\n}\n\n/** The arrival recorded for `threadId`, anchored or not. */\nexport function readHandoffArrival(threadId: string | null | undefined): HandoffArrival | null {\n return (threadId && registry().arrivals.get(threadId)) || null;\n}\n\nexport function subscribeHandoffArrivals(listener: () => void): () => void {\n const { listeners } = registry();\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n}\n\n/** Forgets the arrival of `threadId`: the pick-up never opened the conversation here. */\nexport function clearHandoffArrival(threadId: string): void {\n if (!registry().arrivals.delete(threadId)) return;\n publish();\n}\n\n/**\n * The transcript of `threadId` opened at its cursor (or gave up and stayed at\n * the bottom): forget the cursor so a remount never scrolls there again.\n */\nexport function settleHandoffArrivalCursor(threadId: string): void {\n const { arrivals } = registry();\n const arrival = arrivals.get(threadId);\n if (!arrival || arrival.cursor === null) return;\n arrivals.set(threadId, { ...arrival, cursor: null });\n publish();\n}\n\n/** Where the transcript of `threadId` should open: the cursor not yet settled, or `null`. */\nexport function useHandoffArrivalCursor(threadId: string | null | undefined): string | null {\n return useSyncExternalStore(\n subscribeHandoffArrivals,\n () => readHandoffArrival(threadId)?.cursor ?? null,\n () => null\n );\n}\n\n/** The first anchor wins: later messages are what came after the arrival. */\nexport function anchorHandoffArrival({\n threadId,\n afterMessageId,\n}: {\n threadId: string;\n afterMessageId: string;\n}): void {\n const { arrivals } = registry();\n const arrival = arrivals.get(threadId);\n if (!arrival || arrival.afterMessageId !== null) return;\n arrivals.set(threadId, { ...arrival, afterMessageId });\n publish();\n}\n\n/**\n * The divider for the transcript of `threadId`, once anchored. The transcript\n * passes its last message id and whether its history is still loading; the\n * first time history has loaded with a message in it, the arrival anchors\n * after that message. A thread already on screen anchors at once.\n */\nexport function useHandoffArrival({\n threadId,\n lastMessageId,\n loading,\n}: {\n threadId: string | null | undefined;\n lastMessageId: string | null | undefined;\n loading: boolean;\n}): HandoffArrival | null {\n const arrival = useSyncExternalStore(\n subscribeHandoffArrivals,\n () => readHandoffArrival(threadId),\n () => null\n );\n const unanchored = arrival !== null && arrival.afterMessageId === null;\n useEffect(() => {\n if (!threadId || !unanchored || loading || !lastMessageId) return;\n anchorHandoffArrival({ threadId, afterMessageId: lastMessageId });\n }, [threadId, unanchored, loading, lastMessageId]);\n return arrival?.afterMessageId ? arrival : null;\n}\n\n/** The anchored arrival of the transcript being rendered, for its messages. */\nexport const HandoffArrivalContext = createContext<HandoffArrival | null>(null);\n\n/** The divider label when it goes right after `messageId`; `null` otherwise. */\nexport function useHandoffArrivalAfter(messageId: string | null | undefined): string | null {\n const arrival = useContext(HandoffArrivalContext);\n return arrival && messageId && arrival.afterMessageId === messageId ? arrival.label : null;\n}\n\nexport function resetHandoffArrivalsForTests(): void {\n registry().arrivals.clear();\n publish();\n}\n"],"names":["useSyncExternalStore","useEffect","createContext","useContext"],"mappings":";;;AA2CA,MAAM,mBAAmB,OAAO,IAAI,6CAA6C;AAUjF,SAAS,WAA4B;AACnC,QAAM,OAAO;AACb,SAAQ,oDAA2B,EAAE,UAAU,oBAAI,OAAO,WAAW,oBAAI,MAAI;AAC/E;AAEA,SAAS,UAAgB;AACvB,aAAW,YAAY,WAAW,UAAW,UAAA;AAC/C;AAmCO,SAAS,mBAAmB,UAA4D;AAC7F,SAAQ,YAAY,SAAA,EAAW,SAAS,IAAI,QAAQ,KAAM;AAC5D;AAEO,SAAS,yBAAyB,UAAkC;AACzE,QAAM,EAAE,UAAA,IAAc,SAAA;AACtB,YAAU,IAAI,QAAQ;AACtB,SAAO,MAAM;AACX,cAAU,OAAO,QAAQ;AAAA,EAC3B;AACF;AAYO,SAAS,2BAA2B,UAAwB;AACjE,QAAM,EAAE,SAAA,IAAa,SAAA;AACrB,QAAM,UAAU,SAAS,IAAI,QAAQ;AACrC,MAAI,CAAC,WAAW,QAAQ,WAAW,KAAM;AACzC,WAAS,IAAI,UAAU,EAAE,GAAG,SAAS,QAAQ,MAAM;AACnD,UAAA;AACF;AAGO,SAAS,wBAAwB,UAAoD;AAC1F,SAAOA,WAAAA;AAAAA,IACL;AAAA,IACA,MAAA;;AAAM,uCAAmB,QAAQ,MAA3B,mBAA8B,WAAU;AAAA;AAAA,IAC9C,MAAM;AAAA,EAAA;AAEV;AAGO,SAAS,qBAAqB;AAAA,EACnC;AAAA,EACA;AACF,GAGS;AACP,QAAM,EAAE,SAAA,IAAa,SAAA;AACrB,QAAM,UAAU,SAAS,IAAI,QAAQ;AACrC,MAAI,CAAC,WAAW,QAAQ,mBAAmB,KAAM;AACjD,WAAS,IAAI,UAAU,EAAE,GAAG,SAAS,gBAAgB;AACrD,UAAA;AACF;AAQO,SAAS,kBAAkB;AAAA,EAChC;AAAA,EACA;AAAA,EACA;AACF,GAI0B;AACxB,QAAM,UAAUA,WAAAA;AAAAA,IACd;AAAA,IACA,MAAM,mBAAmB,QAAQ;AAAA,IACjC,MAAM;AAAA,EAAA;AAER,QAAM,aAAa,YAAY,QAAQ,QAAQ,mBAAmB;AAClEC,aAAAA,UAAU,MAAM;AACd,QAAI,CAAC,YAAY,CAAC,cAAc,WAAW,CAAC,cAAe;AAC3D,yBAAqB,EAAE,UAAU,gBAAgB,cAAA,CAAe;AAAA,EAClE,GAAG,CAAC,UAAU,YAAY,SAAS,aAAa,CAAC;AACjD,UAAO,mCAAS,kBAAiB,UAAU;AAC7C;AAGO,MAAM,wBAAwBC,WAAAA,cAAqC,IAAI;AAGvE,SAAS,uBAAuB,WAAqD;AAC1F,QAAM,UAAUC,WAAAA,WAAW,qBAAqB;AAChD,SAAO,WAAW,aAAa,QAAQ,mBAAmB,YAAY,QAAQ,QAAQ;AACxF;;;;;;;;;"}
|
|
@@ -7,22 +7,37 @@ export interface HandoffArrival {
|
|
|
7
7
|
* right after it. `null` until the transcript has anchored it.
|
|
8
8
|
*/
|
|
9
9
|
afterMessageId: string | null;
|
|
10
|
+
/**
|
|
11
|
+
* The message the source was reading, which the transcript opens at. `null`
|
|
12
|
+
* when the source was at the bottom, or once the transcript settled it.
|
|
13
|
+
*/
|
|
14
|
+
cursor: string | null;
|
|
10
15
|
}
|
|
11
16
|
/** Enough for every conversation one person picks up in a session. */
|
|
12
17
|
export declare const MAX_HANDOFF_ARRIVALS = 20;
|
|
13
18
|
/**
|
|
14
|
-
* A pick-up landed `threadId` here from `source` (`mobile`, `chrome`, …)
|
|
15
|
-
*
|
|
19
|
+
* A pick-up landed `threadId` here from `source` (`mobile`, `chrome`, …),
|
|
20
|
+
* where the source was reading `cursor` (a message id; absent or `null` at the
|
|
21
|
+
* bottom). A second pick-up of the same thread moves the divider to the new
|
|
22
|
+
* arrival and replaces the cursor.
|
|
16
23
|
*/
|
|
17
|
-
export declare function recordHandoffArrival({ threadId, source, }: {
|
|
24
|
+
export declare function recordHandoffArrival({ threadId, source, cursor, }: {
|
|
18
25
|
threadId: string;
|
|
19
26
|
source: string;
|
|
27
|
+
cursor?: string | null;
|
|
20
28
|
}): void;
|
|
21
29
|
/** The arrival recorded for `threadId`, anchored or not. */
|
|
22
30
|
export declare function readHandoffArrival(threadId: string | null | undefined): HandoffArrival | null;
|
|
23
31
|
export declare function subscribeHandoffArrivals(listener: () => void): () => void;
|
|
24
32
|
/** Forgets the arrival of `threadId`: the pick-up never opened the conversation here. */
|
|
25
33
|
export declare function clearHandoffArrival(threadId: string): void;
|
|
34
|
+
/**
|
|
35
|
+
* The transcript of `threadId` opened at its cursor (or gave up and stayed at
|
|
36
|
+
* the bottom): forget the cursor so a remount never scrolls there again.
|
|
37
|
+
*/
|
|
38
|
+
export declare function settleHandoffArrivalCursor(threadId: string): void;
|
|
39
|
+
/** Where the transcript of `threadId` should open: the cursor not yet settled, or `null`. */
|
|
40
|
+
export declare function useHandoffArrivalCursor(threadId: string | null | undefined): string | null;
|
|
26
41
|
/** The first anchor wins: later messages are what came after the arrival. */
|
|
27
42
|
export declare function anchorHandoffArrival({ threadId, afterMessageId, }: {
|
|
28
43
|
threadId: string;
|
|
@@ -17,6 +17,23 @@ function subscribeHandoffArrivals(listener) {
|
|
|
17
17
|
listeners.delete(listener);
|
|
18
18
|
};
|
|
19
19
|
}
|
|
20
|
+
function settleHandoffArrivalCursor(threadId) {
|
|
21
|
+
const { arrivals } = registry();
|
|
22
|
+
const arrival = arrivals.get(threadId);
|
|
23
|
+
if (!arrival || arrival.cursor === null) return;
|
|
24
|
+
arrivals.set(threadId, { ...arrival, cursor: null });
|
|
25
|
+
publish();
|
|
26
|
+
}
|
|
27
|
+
function useHandoffArrivalCursor(threadId) {
|
|
28
|
+
return useSyncExternalStore(
|
|
29
|
+
subscribeHandoffArrivals,
|
|
30
|
+
() => {
|
|
31
|
+
var _a;
|
|
32
|
+
return ((_a = readHandoffArrival(threadId)) == null ? void 0 : _a.cursor) ?? null;
|
|
33
|
+
},
|
|
34
|
+
() => null
|
|
35
|
+
);
|
|
36
|
+
}
|
|
20
37
|
function anchorHandoffArrival({
|
|
21
38
|
threadId,
|
|
22
39
|
afterMessageId
|
|
@@ -53,8 +70,10 @@ export {
|
|
|
53
70
|
HandoffArrivalContext,
|
|
54
71
|
anchorHandoffArrival,
|
|
55
72
|
readHandoffArrival,
|
|
73
|
+
settleHandoffArrivalCursor,
|
|
56
74
|
subscribeHandoffArrivals,
|
|
57
75
|
useHandoffArrival,
|
|
58
|
-
useHandoffArrivalAfter
|
|
76
|
+
useHandoffArrivalAfter,
|
|
77
|
+
useHandoffArrivalCursor
|
|
59
78
|
};
|
|
60
79
|
//# sourceMappingURL=handoff-arrival.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"handoff-arrival.js","sources":["../../../../agent-runtime/src/handoff-arrival.ts"],"sourcesContent":["/**\n * Where a handed-off conversation arrived on this surface: a transcript divider\n * (\"Continued from your phone\") after the last message that was already there\n * when the person picked it up.\n *\n * Ephemeral and client-side. A host records an arrival when its pick-up lands\n * (`recordHandoffArrival`); its transcript anchors it to the thread's last\n * message once that history has loaded (`useHandoffArrival`) and renders the\n * divider after that message (`useHandoffArrivalAfter`). Nothing is sent to\n * Athena or persisted; a reload forgets it.\n */\nimport { createContext, useContext, useEffect, useSyncExternalStore } from 'react';\nimport { continuedFromLabel } from './thread-handoff-protocol';\n\nexport interface HandoffArrival {\n threadId: string;\n /** \"Continued from your phone\". */\n label: string;\n /**\n * The last message present when the conversation arrived; the divider goes\n * right after it. `null` until the transcript has anchored it.\n */\n afterMessageId: string | null;\n}\n\n/** Enough for every conversation one person picks up in a session. */\nexport const MAX_HANDOFF_ARRIVALS = 20;\n\n/**\n * The arrivals live on `globalThis`, not in module scope, because two copies of\n * this module can run in one page: the SDK bundles agent-runtime into its dist\n * while a host (Spaces, the Chrome extension, Midas) imports it directly. The\n * host records through its copy and the SDK transcript reads through its own.\n */\nconst HANDOFF_ARRIVALS = Symbol.for('@athenaintel/agent-runtime/handoff-arrivals');\n\ninterface ArrivalRegistry {\n arrivals: Map<string, HandoffArrival>;\n listeners: Set<() => void>;\n}\n\ntype ArrivalRegistryHost = typeof globalThis & { [HANDOFF_ARRIVALS]?: ArrivalRegistry };\n\n// process-local: this client only — what this surface picked up; nothing is persisted\nfunction registry(): ArrivalRegistry {\n const host = globalThis as ArrivalRegistryHost;\n return (host[HANDOFF_ARRIVALS] ??= { arrivals: new Map(), listeners: new Set() });\n}\n\nfunction publish(): void {\n for (const listener of registry().listeners) listener();\n}\n\n/**\n * A pick-up landed `threadId` here from `source` (`mobile`, `chrome`, …)
|
|
1
|
+
{"version":3,"file":"handoff-arrival.js","sources":["../../../../agent-runtime/src/handoff-arrival.ts"],"sourcesContent":["/**\n * Where a handed-off conversation arrived on this surface: a transcript divider\n * (\"Continued from your phone\") after the last message that was already there\n * when the person picked it up.\n *\n * Ephemeral and client-side. A host records an arrival when its pick-up lands\n * (`recordHandoffArrival`); its transcript anchors it to the thread's last\n * message once that history has loaded (`useHandoffArrival`) and renders the\n * divider after that message (`useHandoffArrivalAfter`). Nothing is sent to\n * Athena or persisted; a reload forgets it.\n *\n * The arrival also carries where the source was reading (`cursor`, a message\n * id): the transcript opens at that message instead of the bottom, then\n * settles it (`settleHandoffArrivalCursor`) so a remount never scrolls again.\n */\nimport { createContext, useContext, useEffect, useSyncExternalStore } from 'react';\nimport { continuedFromLabel, parseHandoffCursor } from './thread-handoff-protocol';\n\nexport interface HandoffArrival {\n threadId: string;\n /** \"Continued from your phone\". */\n label: string;\n /**\n * The last message present when the conversation arrived; the divider goes\n * right after it. `null` until the transcript has anchored it.\n */\n afterMessageId: string | null;\n /**\n * The message the source was reading, which the transcript opens at. `null`\n * when the source was at the bottom, or once the transcript settled it.\n */\n cursor: string | null;\n}\n\n/** Enough for every conversation one person picks up in a session. */\nexport const MAX_HANDOFF_ARRIVALS = 20;\n\n/**\n * The arrivals live on `globalThis`, not in module scope, because two copies of\n * this module can run in one page: the SDK bundles agent-runtime into its dist\n * while a host (Spaces, the Chrome extension, Midas) imports it directly. The\n * host records through its copy and the SDK transcript reads through its own.\n */\nconst HANDOFF_ARRIVALS = Symbol.for('@athenaintel/agent-runtime/handoff-arrivals');\n\ninterface ArrivalRegistry {\n arrivals: Map<string, HandoffArrival>;\n listeners: Set<() => void>;\n}\n\ntype ArrivalRegistryHost = typeof globalThis & { [HANDOFF_ARRIVALS]?: ArrivalRegistry };\n\n// process-local: this client only — what this surface picked up; nothing is persisted\nfunction registry(): ArrivalRegistry {\n const host = globalThis as ArrivalRegistryHost;\n return (host[HANDOFF_ARRIVALS] ??= { arrivals: new Map(), listeners: new Set() });\n}\n\nfunction publish(): void {\n for (const listener of registry().listeners) listener();\n}\n\n/**\n * A pick-up landed `threadId` here from `source` (`mobile`, `chrome`, …),\n * where the source was reading `cursor` (a message id; absent or `null` at the\n * bottom). A second pick-up of the same thread moves the divider to the new\n * arrival and replaces the cursor.\n */\nexport function recordHandoffArrival({\n threadId,\n source,\n cursor,\n}: {\n threadId: string;\n source: string;\n cursor?: string | null;\n}): void {\n if (!threadId) return;\n const { arrivals } = registry();\n arrivals.delete(threadId);\n arrivals.set(threadId, {\n threadId,\n label: continuedFromLabel(source),\n afterMessageId: null,\n cursor: parseHandoffCursor(cursor),\n });\n while (arrivals.size > MAX_HANDOFF_ARRIVALS) {\n const oldest = arrivals.keys().next().value;\n if (oldest === undefined) break;\n arrivals.delete(oldest);\n }\n publish();\n}\n\n/** The arrival recorded for `threadId`, anchored or not. */\nexport function readHandoffArrival(threadId: string | null | undefined): HandoffArrival | null {\n return (threadId && registry().arrivals.get(threadId)) || null;\n}\n\nexport function subscribeHandoffArrivals(listener: () => void): () => void {\n const { listeners } = registry();\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n}\n\n/** Forgets the arrival of `threadId`: the pick-up never opened the conversation here. */\nexport function clearHandoffArrival(threadId: string): void {\n if (!registry().arrivals.delete(threadId)) return;\n publish();\n}\n\n/**\n * The transcript of `threadId` opened at its cursor (or gave up and stayed at\n * the bottom): forget the cursor so a remount never scrolls there again.\n */\nexport function settleHandoffArrivalCursor(threadId: string): void {\n const { arrivals } = registry();\n const arrival = arrivals.get(threadId);\n if (!arrival || arrival.cursor === null) return;\n arrivals.set(threadId, { ...arrival, cursor: null });\n publish();\n}\n\n/** Where the transcript of `threadId` should open: the cursor not yet settled, or `null`. */\nexport function useHandoffArrivalCursor(threadId: string | null | undefined): string | null {\n return useSyncExternalStore(\n subscribeHandoffArrivals,\n () => readHandoffArrival(threadId)?.cursor ?? null,\n () => null\n );\n}\n\n/** The first anchor wins: later messages are what came after the arrival. */\nexport function anchorHandoffArrival({\n threadId,\n afterMessageId,\n}: {\n threadId: string;\n afterMessageId: string;\n}): void {\n const { arrivals } = registry();\n const arrival = arrivals.get(threadId);\n if (!arrival || arrival.afterMessageId !== null) return;\n arrivals.set(threadId, { ...arrival, afterMessageId });\n publish();\n}\n\n/**\n * The divider for the transcript of `threadId`, once anchored. The transcript\n * passes its last message id and whether its history is still loading; the\n * first time history has loaded with a message in it, the arrival anchors\n * after that message. A thread already on screen anchors at once.\n */\nexport function useHandoffArrival({\n threadId,\n lastMessageId,\n loading,\n}: {\n threadId: string | null | undefined;\n lastMessageId: string | null | undefined;\n loading: boolean;\n}): HandoffArrival | null {\n const arrival = useSyncExternalStore(\n subscribeHandoffArrivals,\n () => readHandoffArrival(threadId),\n () => null\n );\n const unanchored = arrival !== null && arrival.afterMessageId === null;\n useEffect(() => {\n if (!threadId || !unanchored || loading || !lastMessageId) return;\n anchorHandoffArrival({ threadId, afterMessageId: lastMessageId });\n }, [threadId, unanchored, loading, lastMessageId]);\n return arrival?.afterMessageId ? arrival : null;\n}\n\n/** The anchored arrival of the transcript being rendered, for its messages. */\nexport const HandoffArrivalContext = createContext<HandoffArrival | null>(null);\n\n/** The divider label when it goes right after `messageId`; `null` otherwise. */\nexport function useHandoffArrivalAfter(messageId: string | null | undefined): string | null {\n const arrival = useContext(HandoffArrivalContext);\n return arrival && messageId && arrival.afterMessageId === messageId ? arrival.label : null;\n}\n\nexport function resetHandoffArrivalsForTests(): void {\n registry().arrivals.clear();\n publish();\n}\n"],"names":[],"mappings":";AA2CA,MAAM,mBAAmB,OAAO,IAAI,6CAA6C;AAUjF,SAAS,WAA4B;AACnC,QAAM,OAAO;AACb,SAAQ,oDAA2B,EAAE,UAAU,oBAAI,OAAO,WAAW,oBAAI,MAAI;AAC/E;AAEA,SAAS,UAAgB;AACvB,aAAW,YAAY,WAAW,UAAW,UAAA;AAC/C;AAmCO,SAAS,mBAAmB,UAA4D;AAC7F,SAAQ,YAAY,SAAA,EAAW,SAAS,IAAI,QAAQ,KAAM;AAC5D;AAEO,SAAS,yBAAyB,UAAkC;AACzE,QAAM,EAAE,UAAA,IAAc,SAAA;AACtB,YAAU,IAAI,QAAQ;AACtB,SAAO,MAAM;AACX,cAAU,OAAO,QAAQ;AAAA,EAC3B;AACF;AAYO,SAAS,2BAA2B,UAAwB;AACjE,QAAM,EAAE,SAAA,IAAa,SAAA;AACrB,QAAM,UAAU,SAAS,IAAI,QAAQ;AACrC,MAAI,CAAC,WAAW,QAAQ,WAAW,KAAM;AACzC,WAAS,IAAI,UAAU,EAAE,GAAG,SAAS,QAAQ,MAAM;AACnD,UAAA;AACF;AAGO,SAAS,wBAAwB,UAAoD;AAC1F,SAAO;AAAA,IACL;AAAA,IACA,MAAA;;AAAM,uCAAmB,QAAQ,MAA3B,mBAA8B,WAAU;AAAA;AAAA,IAC9C,MAAM;AAAA,EAAA;AAEV;AAGO,SAAS,qBAAqB;AAAA,EACnC;AAAA,EACA;AACF,GAGS;AACP,QAAM,EAAE,SAAA,IAAa,SAAA;AACrB,QAAM,UAAU,SAAS,IAAI,QAAQ;AACrC,MAAI,CAAC,WAAW,QAAQ,mBAAmB,KAAM;AACjD,WAAS,IAAI,UAAU,EAAE,GAAG,SAAS,gBAAgB;AACrD,UAAA;AACF;AAQO,SAAS,kBAAkB;AAAA,EAChC;AAAA,EACA;AAAA,EACA;AACF,GAI0B;AACxB,QAAM,UAAU;AAAA,IACd;AAAA,IACA,MAAM,mBAAmB,QAAQ;AAAA,IACjC,MAAM;AAAA,EAAA;AAER,QAAM,aAAa,YAAY,QAAQ,QAAQ,mBAAmB;AAClE,YAAU,MAAM;AACd,QAAI,CAAC,YAAY,CAAC,cAAc,WAAW,CAAC,cAAe;AAC3D,yBAAqB,EAAE,UAAU,gBAAgB,cAAA,CAAe;AAAA,EAClE,GAAG,CAAC,UAAU,YAAY,SAAS,aAAa,CAAC;AACjD,UAAO,mCAAS,kBAAiB,UAAU;AAC7C;AAGO,MAAM,wBAAwB,cAAqC,IAAI;AAGvE,SAAS,uBAAuB,WAAqD;AAC1F,QAAM,UAAU,WAAW,qBAAqB;AAChD,SAAO,WAAW,aAAa,QAAQ,mBAAmB,YAAY,QAAQ,QAAQ;AACxF;"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const HANDOFF_CURSOR_PATTERN = /^[A-Za-z0-9._:-]{1,256}$/;
|
|
4
|
+
const parseHandoffCursor = (value) => value && HANDOFF_CURSOR_PATTERN.test(value) ? value : null;
|
|
5
|
+
exports.parseHandoffCursor = parseHandoffCursor;
|
|
6
|
+
//# sourceMappingURL=handoff-cursor-id.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handoff-cursor-id.cjs","sources":["../../../../agent-runtime/src/handoff-cursor-id.ts"],"sourcesContent":["/**\n * A handover's reading position: the id of the last message the source's\n * transcript showed (see `handoff-cursor`). An opaque id from the person's own\n * thread, never content; agora accepts this shape only.\n *\n * Kept free of zod (unlike `thread-handoff-protocol`, which re-exports it): the\n * React SDK bundles `handoff-cursor`, and a bundled zod would land under the\n * SDK's `dist/node_modules`, which npm never packs.\n */\nexport const MAX_HANDOFF_CURSOR_LENGTH = 256;\nconst HANDOFF_CURSOR_PATTERN = /^[A-Za-z0-9._:-]{1,256}$/;\n\n/** A message id a handover can carry as its reading position, or `null`. */\nexport const parseHandoffCursor = (value: string | null | undefined): string | null =>\n value && HANDOFF_CURSOR_PATTERN.test(value) ? value : null;\n"],"names":[],"mappings":";;AAUA,MAAM,yBAAyB;AAGxB,MAAM,qBAAqB,CAAC,UACjC,SAAS,uBAAuB,KAAK,KAAK,IAAI,QAAQ;;"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A handover's reading position: the id of the last message the source's
|
|
3
|
+
* transcript showed (see `handoff-cursor`). An opaque id from the person's own
|
|
4
|
+
* thread, never content; agora accepts this shape only.
|
|
5
|
+
*
|
|
6
|
+
* Kept free of zod (unlike `thread-handoff-protocol`, which re-exports it): the
|
|
7
|
+
* React SDK bundles `handoff-cursor`, and a bundled zod would land under the
|
|
8
|
+
* SDK's `dist/node_modules`, which npm never packs.
|
|
9
|
+
*/
|
|
10
|
+
export declare const MAX_HANDOFF_CURSOR_LENGTH = 256;
|
|
11
|
+
/** A message id a handover can carry as its reading position, or `null`. */
|
|
12
|
+
export declare const parseHandoffCursor: (value: string | null | undefined) => string | null;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handoff-cursor-id.js","sources":["../../../../agent-runtime/src/handoff-cursor-id.ts"],"sourcesContent":["/**\n * A handover's reading position: the id of the last message the source's\n * transcript showed (see `handoff-cursor`). An opaque id from the person's own\n * thread, never content; agora accepts this shape only.\n *\n * Kept free of zod (unlike `thread-handoff-protocol`, which re-exports it): the\n * React SDK bundles `handoff-cursor`, and a bundled zod would land under the\n * SDK's `dist/node_modules`, which npm never packs.\n */\nexport const MAX_HANDOFF_CURSOR_LENGTH = 256;\nconst HANDOFF_CURSOR_PATTERN = /^[A-Za-z0-9._:-]{1,256}$/;\n\n/** A message id a handover can carry as its reading position, or `null`. */\nexport const parseHandoffCursor = (value: string | null | undefined): string | null =>\n value && HANDOFF_CURSOR_PATTERN.test(value) ? value : null;\n"],"names":[],"mappings":"AAUA,MAAM,yBAAyB;AAGxB,MAAM,qBAAqB,CAAC,UACjC,SAAS,uBAAuB,KAAK,KAAK,IAAI,QAAQ;"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const core = require("@assistant-ui/core");
|
|
4
|
+
const handoffCursorId = require("./handoff-cursor-id.cjs");
|
|
5
|
+
const READING_CURSOR_BOTTOM_TOLERANCE_PX = 48;
|
|
6
|
+
const READING_LINE_FRACTION = 1 / 3;
|
|
7
|
+
function readingCursorFromRows({
|
|
8
|
+
top,
|
|
9
|
+
bottom,
|
|
10
|
+
rows
|
|
11
|
+
}) {
|
|
12
|
+
var _a;
|
|
13
|
+
const line = top + (bottom - top) * READING_LINE_FRACTION;
|
|
14
|
+
let lastFull = null;
|
|
15
|
+
let atLine = null;
|
|
16
|
+
for (const row of rows) {
|
|
17
|
+
if (row.bottom <= row.top) continue;
|
|
18
|
+
if (row.top >= top - 1 && row.bottom <= bottom + 1) lastFull = row;
|
|
19
|
+
if (row.top <= line && row.bottom > line) atLine = row;
|
|
20
|
+
}
|
|
21
|
+
return handoffCursorId.parseHandoffCursor((_a = lastFull ?? atLine) == null ? void 0 : _a.id);
|
|
22
|
+
}
|
|
23
|
+
const MESSAGE_ROW_ID_ATTRIBUTE = "data-message-id";
|
|
24
|
+
function readingCursorOf({
|
|
25
|
+
viewport,
|
|
26
|
+
coveredBottomPx = 0,
|
|
27
|
+
idAttribute = MESSAGE_ROW_ID_ATTRIBUTE
|
|
28
|
+
}) {
|
|
29
|
+
const fromBottom = viewport.scrollHeight - viewport.scrollTop - viewport.clientHeight;
|
|
30
|
+
if (fromBottom <= READING_CURSOR_BOTTOM_TOLERANCE_PX) return null;
|
|
31
|
+
const box = viewport.getBoundingClientRect();
|
|
32
|
+
const rows = Array.from(viewport.querySelectorAll(`[${idAttribute}]`), (row) => {
|
|
33
|
+
const rect = row.getBoundingClientRect();
|
|
34
|
+
return { id: row.getAttribute(idAttribute) ?? "", top: rect.top, bottom: rect.bottom };
|
|
35
|
+
});
|
|
36
|
+
return readingCursorFromRows({
|
|
37
|
+
top: box.top,
|
|
38
|
+
bottom: Math.max(box.top, box.bottom - coveredBottomPx),
|
|
39
|
+
rows
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
function handoffCursorMessageId({
|
|
43
|
+
messages,
|
|
44
|
+
cursor
|
|
45
|
+
}) {
|
|
46
|
+
for (const message of messages) {
|
|
47
|
+
if (message.id === cursor) return message.id;
|
|
48
|
+
}
|
|
49
|
+
for (const message of messages) {
|
|
50
|
+
const sources = core.getExternalStoreMessages(message);
|
|
51
|
+
if (!Array.isArray(sources)) continue;
|
|
52
|
+
for (const source of sources) {
|
|
53
|
+
if (source && typeof source === "object" && source.id === cursor) {
|
|
54
|
+
return message.id;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
const HANDOFF_CURSOR_MAX_HISTORY_PAGES = 20;
|
|
61
|
+
const HANDOFF_CURSOR_WAIT_MS = 3e4;
|
|
62
|
+
const HANDOFF_CURSOR_HIGHLIGHT_MS = 2400;
|
|
63
|
+
const HANDOFF_CURSOR_SETTLE_MS = 1500;
|
|
64
|
+
function handoffCursorRevealStep({
|
|
65
|
+
found,
|
|
66
|
+
loading,
|
|
67
|
+
hasOlder,
|
|
68
|
+
pagesLoaded,
|
|
69
|
+
elapsedMs
|
|
70
|
+
}) {
|
|
71
|
+
if (found) return "reveal";
|
|
72
|
+
if (elapsedMs >= HANDOFF_CURSOR_WAIT_MS) return "bottom";
|
|
73
|
+
if (loading || hasOlder === null) return "wait";
|
|
74
|
+
if (hasOlder && pagesLoaded < HANDOFF_CURSOR_MAX_HISTORY_PAGES) return "page";
|
|
75
|
+
return "bottom";
|
|
76
|
+
}
|
|
77
|
+
exports.HANDOFF_CURSOR_HIGHLIGHT_MS = HANDOFF_CURSOR_HIGHLIGHT_MS;
|
|
78
|
+
exports.HANDOFF_CURSOR_MAX_HISTORY_PAGES = HANDOFF_CURSOR_MAX_HISTORY_PAGES;
|
|
79
|
+
exports.HANDOFF_CURSOR_SETTLE_MS = HANDOFF_CURSOR_SETTLE_MS;
|
|
80
|
+
exports.HANDOFF_CURSOR_WAIT_MS = HANDOFF_CURSOR_WAIT_MS;
|
|
81
|
+
exports.MESSAGE_ROW_ID_ATTRIBUTE = MESSAGE_ROW_ID_ATTRIBUTE;
|
|
82
|
+
exports.READING_CURSOR_BOTTOM_TOLERANCE_PX = READING_CURSOR_BOTTOM_TOLERANCE_PX;
|
|
83
|
+
exports.READING_LINE_FRACTION = READING_LINE_FRACTION;
|
|
84
|
+
exports.handoffCursorMessageId = handoffCursorMessageId;
|
|
85
|
+
exports.handoffCursorRevealStep = handoffCursorRevealStep;
|
|
86
|
+
exports.readingCursorFromRows = readingCursorFromRows;
|
|
87
|
+
exports.readingCursorOf = readingCursorOf;
|
|
88
|
+
//# sourceMappingURL=handoff-cursor.cjs.map
|