@assistant-ui/react-google-adk 0.0.34 → 0.0.35

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.
@@ -1,7 +1,7 @@
1
1
  //#region src/sdkIdentity.ts
2
2
  const ADK_SDK = {
3
3
  name: "@assistant-ui/react-google-adk",
4
- version: "0.0.34"
4
+ version: "0.0.35"
5
5
  };
6
6
  //#endregion
7
7
  export { ADK_SDK };
@@ -7,6 +7,28 @@ export type UseAdkMessagesOptions = {
7
7
  onAgentTransfer?: OnAdkAgentTransferCallback;
8
8
  };
9
9
  };
10
+ type UseAdkMessagesInternalOptions = UseAdkMessagesOptions & {
11
+ onMessages?: (messages: AdkMessage[], runConfig: unknown) => void;
12
+ };
13
+ declare const useAdkMessagesInternal: ({ stream, eventHandlers, onMessages, }: UseAdkMessagesInternalOptions) => {
14
+ messages: AdkMessage[];
15
+ stateDelta: Record<string, unknown>;
16
+ agentInfo: {
17
+ name?: string | undefined;
18
+ branch?: string | undefined;
19
+ };
20
+ longRunningToolIds: string[];
21
+ artifactDelta: Record<string, number>;
22
+ toolConfirmations: AdkToolConfirmation[];
23
+ authRequests: AdkAuthRequest[];
24
+ escalated: boolean;
25
+ messageMetadata: Map<string, AdkMessageMetadata>;
26
+ sendMessage: (newMessages: AdkMessage[], config: AdkSendMessageConfig) => Promise<void>;
27
+ cancel: () => void;
28
+ setMessages: (msgs: AdkMessage[]) => void;
29
+ replaceMessages: (msgs: AdkMessage[]) => void;
30
+ applySnapshot: (snapshot: AdkThreadSnapshot) => void;
31
+ };
10
32
  export declare const useAdkMessages: ({ stream, eventHandlers, }: UseAdkMessagesOptions) => {
11
33
  messages: AdkMessage[];
12
34
  stateDelta: Record<string, unknown>;
@@ -26,4 +48,5 @@ export declare const useAdkMessages: ({ stream, eventHandlers, }: UseAdkMessages
26
48
  replaceMessages: (msgs: AdkMessage[]) => void;
27
49
  applySnapshot: (snapshot: AdkThreadSnapshot) => void;
28
50
  };
51
+ export { useAdkMessagesInternal };
29
52
  //# sourceMappingURL=useAdkMessages.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"useAdkMessages.d.ts","sourceRoot":"","sources":["../src/useAdkMessages.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAEV,UAAU,EACV,kBAAkB,EAClB,oBAAoB,EACpB,iBAAiB,EACjB,mBAAmB,EACnB,cAAc,EACd,iBAAiB,EACjB,kBAAkB,EAClB,wBAAwB,EACxB,0BAA0B,EAC3B,mBAAgB;AAEjB,MAAM,MAAM,qBAAqB,GAAG;IAClC,MAAM,EAAE,iBAAiB,CAAC;IAC1B,aAAa,CAAC,EAAE;QACd,OAAO,CAAC,EAAE,kBAAkB,CAAC;QAC7B,aAAa,CAAC,EAAE,wBAAwB,CAAC;QACzC,eAAe,CAAC,EAAE,0BAA0B,CAAC;KAC9C,CAAC;CACH,CAAC;AAYF,eAAO,MAAM,cAAc,GAAI,4BAG5B,qBAAqB;;;;eAIb,MAAM,GAAG,SAAS;iBAChB,MAAM,GAAG,SAAS;;;;;;;;+BAqFP,UAAU,EAAE,UAAU,oBAAoB;;wBAvDhB,UAAU,EAAE;4BAmCnD,UAAU,EAAE;8BAnBR,iBAAiB;CAuO/B,CAAC"}
1
+ {"version":3,"file":"useAdkMessages.d.ts","sourceRoot":"","sources":["../src/useAdkMessages.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAEV,UAAU,EACV,kBAAkB,EAClB,oBAAoB,EACpB,iBAAiB,EACjB,mBAAmB,EACnB,cAAc,EACd,iBAAiB,EACjB,kBAAkB,EAClB,wBAAwB,EACxB,0BAA0B,EAC3B,mBAAgB;AAEjB,MAAM,MAAM,qBAAqB,GAAG;IAClC,MAAM,EAAE,iBAAiB,CAAC;IAC1B,aAAa,CAAC,EAAE;QACd,OAAO,CAAC,EAAE,kBAAkB,CAAC;QAC7B,aAAa,CAAC,EAAE,wBAAwB,CAAC;QACzC,eAAe,CAAC,EAAE,0BAA0B,CAAC;KAC9C,CAAC;CACH,CAAC;AAEF,KAAK,6BAA6B,GAAG,qBAAqB,GAAG;IAC3D,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,UAAU,EAAE,EAAE,SAAS,EAAE,OAAO,KAAK,IAAI,CAAC;CACnE,CAAC;AAYF,QAAA,MAAM,sBAAsB,GAAI,wCAI7B,6BAA6B;;;;eAIrB,MAAM,GAAG,SAAS;iBAChB,MAAM,GAAG,SAAS;;;;;;;;+BAqFP,UAAU,EAAE,UAAU,oBAAoB;;wBAvDhB,UAAU,EAAE;4BAmCnD,UAAU,EAAE;8BAnBR,iBAAiB;CAmP/B,CAAC;AAEF,eAAO,MAAM,cAAc,GAAI,4BAG5B,qBAAqB;;;;eAvSb,MAAM,GAAG,SAAS;iBAChB,MAAM,GAAG,SAAS;;;;;;;;+BAqFP,UAAU,EAAE,UAAU,oBAAoB;;wBAvDhB,UAAU,EAAE;4BAmCnD,UAAU,EAAE;8BAnBR,iBAAiB;CA4P5B,CAAC;AAEL,OAAO,EAAE,sBAAsB,EAAE,CAAC"}
@@ -10,7 +10,7 @@ import { useCallback, useInsertionEffect, useMemo, useRef, useState } from "reac
10
10
  const invokeAdkRuntimeCallback = (name, callback, ...args) => {
11
11
  invokeUserCallback("react-google-adk", name, callback, ...args);
12
12
  };
13
- const useAdkMessages = ({ stream, eventHandlers }) => {
13
+ const useAdkMessagesInternal = ({ stream, eventHandlers, onMessages }) => {
14
14
  const [messages, _setMessages] = useState([]);
15
15
  const [stateDelta, setStateDelta] = useState({});
16
16
  const [agentInfo, setAgentInfo] = useState({});
@@ -102,6 +102,9 @@ const useAdkMessages = ({ stream, eventHandlers }) => {
102
102
  for await (const event of abortableIterable(response, abortController.signal)) {
103
103
  if (abortController.signal.aborted || abortControllerRef.current !== abortController) break;
104
104
  const updatedMessages = accumulator.processEvent(event);
105
+ const affectedMessageCount = Math.max(event.content?.parts?.length ?? 0, 1);
106
+ const affectedMessages = updatedMessages.slice(-affectedMessageCount);
107
+ if (affectedMessages.length > 0) onMessages?.(affectedMessages, config.runConfig);
105
108
  setMessagesImmediate(updatedMessages);
106
109
  setStateDelta({
107
110
  ...stateDeltaRef.current,
@@ -154,7 +157,8 @@ const useAdkMessages = ({ stream, eventHandlers }) => {
154
157
  stream,
155
158
  onError,
156
159
  onCustomEvent,
157
- onAgentTransfer
160
+ onAgentTransfer,
161
+ onMessages
158
162
  ]);
159
163
  const cancel = useCallback(() => {
160
164
  if (abortControllerRef.current) abortControllerRef.current.abort();
@@ -177,6 +181,10 @@ const useAdkMessages = ({ stream, eventHandlers }) => {
177
181
  applySnapshot
178
182
  };
179
183
  };
184
+ const useAdkMessages = ({ stream, eventHandlers }) => useAdkMessagesInternal({
185
+ stream,
186
+ ...eventHandlers !== void 0 && { eventHandlers }
187
+ });
180
188
  /**
181
189
  * Transport sends every human and tool message of one `send` call as a single
182
190
  * ADK `Content`, and ADK parses that event's function responses before running
@@ -255,6 +263,6 @@ const messageToEvent = (msg) => {
255
263
  return result;
256
264
  };
257
265
  //#endregion
258
- export { messageToEvent, messagesToEvents, useAdkMessages };
266
+ export { messageToEvent, messagesToEvents, useAdkMessages, useAdkMessagesInternal };
259
267
 
260
268
  //# sourceMappingURL=useAdkMessages.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"useAdkMessages.js","names":[],"sources":["../src/useAdkMessages.ts"],"sourcesContent":["import { isRecord } from \"@assistant-ui/core/internal\";\nimport {\n useState,\n useCallback,\n useInsertionEffect,\n useRef,\n useMemo,\n} from \"react\";\nimport { generateId } from \"@assistant-ui/core\";\nimport { useReplaySafeEffect } from \"@assistant-ui/store/internal\";\nimport { useAui } from \"@assistant-ui/store\";\nimport {\n abortableIterable,\n invokeUserCallback,\n openAbortableIterable,\n} from \"@assistant-ui/core/internal\";\nimport { AdkEventAccumulator } from \"./AdkEventAccumulator\";\nimport { contentToParts } from \"./contentToParts\";\nimport { toAdkFunctionResponse } from \"./toAdkFunctionResponse\";\nimport type {\n AdkEvent,\n AdkMessage,\n AdkMessageMetadata,\n AdkSendMessageConfig,\n AdkStreamCallback,\n AdkToolConfirmation,\n AdkAuthRequest,\n AdkThreadSnapshot,\n OnAdkErrorCallback,\n OnAdkCustomEventCallback,\n OnAdkAgentTransferCallback,\n} from \"./types\";\n\nexport type UseAdkMessagesOptions = {\n stream: AdkStreamCallback;\n eventHandlers?: {\n onError?: OnAdkErrorCallback;\n onCustomEvent?: OnAdkCustomEventCallback;\n onAgentTransfer?: OnAdkAgentTransferCallback;\n };\n};\n\ntype AdkRuntimeCallbackName = \"onError\" | \"onCustomEvent\" | \"onAgentTransfer\";\n\nconst invokeAdkRuntimeCallback = <TArgs extends readonly unknown[]>(\n name: AdkRuntimeCallbackName,\n callback: ((...args: TArgs) => unknown) | undefined,\n ...args: TArgs\n): void => {\n void invokeUserCallback(\"react-google-adk\", name, callback, ...args);\n};\n\nexport const useAdkMessages = ({\n stream,\n eventHandlers,\n}: UseAdkMessagesOptions) => {\n const [messages, _setMessages] = useState<AdkMessage[]>([]);\n const [stateDelta, setStateDelta] = useState<Record<string, unknown>>({});\n const [agentInfo, setAgentInfo] = useState<{\n name?: string | undefined;\n branch?: string | undefined;\n }>({});\n const [longRunningToolIds, _setLongRunningToolIds] = useState<string[]>([]);\n const [artifactDelta, setArtifactDelta] = useState<Record<string, number>>(\n {},\n );\n const [toolConfirmations, setToolConfirmations] = useState<\n AdkToolConfirmation[]\n >([]);\n const [authRequests, setAuthRequests] = useState<AdkAuthRequest[]>([]);\n const [escalated, setEscalated] = useState(false);\n const [messageMetadata, setMessageMetadata] = useState<\n Map<string, AdkMessageMetadata>\n >(new Map());\n // setMessagesImmediate and setLongRunningToolIds are the only writers of their state and publish these refs with it, so neither ref trails a commit.\n const messagesRef = useRef(messages);\n const longRunningToolIdsRef = useRef(longRunningToolIds);\n const stateDeltaRef = useRef(stateDelta);\n useInsertionEffect(() => {\n stateDeltaRef.current = stateDelta;\n }, [stateDelta]);\n const artifactDeltaRef = useRef(artifactDelta);\n useInsertionEffect(() => {\n artifactDeltaRef.current = artifactDelta;\n }, [artifactDelta]);\n const messageMetadataRef = useRef(messageMetadata);\n useInsertionEffect(() => {\n messageMetadataRef.current = messageMetadata;\n }, [messageMetadata]);\n\n const setMessagesImmediate = useCallback((msgs: AdkMessage[]) => {\n messagesRef.current = msgs;\n _setMessages(msgs);\n }, []);\n const setLongRunningToolIds = useCallback((ids: string[]) => {\n longRunningToolIdsRef.current = ids;\n _setLongRunningToolIds(ids);\n }, []);\n\n /**\n * Swap the thread over to a loaded snapshot in one commit. Unlike\n * {@link replaceMessages} this never passes through a cleared state, so a\n * refetch that lands while a confirmation is on screen replaces it rather\n * than blanking it first.\n */\n const applySnapshot = useCallback(\n (snapshot: AdkThreadSnapshot) => {\n setMessagesImmediate(snapshot.messages);\n setLongRunningToolIds(snapshot.longRunningToolIds ?? []);\n setToolConfirmations(snapshot.toolConfirmations ?? []);\n setAuthRequests(snapshot.authRequests ?? []);\n setEscalated(snapshot.escalated ?? false);\n setMessageMetadata(snapshot.messageMetadata ?? new Map());\n setStateDelta(snapshot.stateDelta ?? {});\n setArtifactDelta(snapshot.artifactDelta ?? {});\n setAgentInfo(snapshot.agentInfo ?? {});\n },\n [setLongRunningToolIds, setMessagesImmediate],\n );\n\n // Replace the message list AND reset derived per-turn HITL state.\n // Used by truncation paths (edit, reload) so that stale interrupt\n // markers and per-message metadata from the removed messages don't leak\n // into the next turn.\n const replaceMessages = useCallback(\n (msgs: AdkMessage[]) => {\n setMessagesImmediate(msgs);\n setLongRunningToolIds([]);\n setToolConfirmations([]);\n setAuthRequests([]);\n setEscalated(false);\n setMessageMetadata(new Map());\n },\n [setLongRunningToolIds, setMessagesImmediate],\n );\n\n const abortControllerRef = useRef<AbortController | null>(null);\n\n const { onError, onCustomEvent, onAgentTransfer } = useMemo(\n () => eventHandlers ?? {},\n [eventHandlers],\n );\n\n const aui = useAui();\n const sendMessage = useCallback(\n async (newMessages: AdkMessage[], config: AdkSendMessageConfig) => {\n const newMessagesWithId = newMessages.map((m) =>\n m.id ? m : { ...m, id: generateId() },\n ) as AdkMessage[];\n\n // A staged message is already in the thread under its own id, and the\n // merged event below re-emits the whole batch under the first one. Seeding\n // with the originals would leave every later staged id beside the merged\n // copy of itself.\n const resentIds = new Set(newMessagesWithId.map((m) => m.id));\n // The optimistic event for a tool-only batch carries no author, so the accumulator cannot settle the calls this send answers.\n const answeredToolCallIds = new Set(\n newMessagesWithId.flatMap((m) =>\n m.type === \"tool\" ? [m.tool_call_id] : [],\n ),\n );\n const accumulator = new AdkEventAccumulator(\n messagesRef.current.filter((m) => !resentIds.has(m.id)),\n longRunningToolIdsRef.current.filter(\n (id) => !answeredToolCallIds.has(id),\n ),\n );\n for (const event of messagesToEvents(newMessagesWithId)) {\n accumulator.processEvent(event);\n }\n const initialMessages = accumulator.getMessages();\n const initialMessageIds = new Set(initialMessages.map((m) => m.id));\n const initialLongRunningToolIds = accumulator.getLongRunningToolIds();\n setMessagesImmediate(initialMessages);\n setLongRunningToolIds(initialLongRunningToolIds);\n setToolConfirmations(accumulator.getToolConfirmations());\n setAuthRequests(accumulator.getAuthRequests());\n let lastTransferToAgent: string | undefined;\n\n // Google ADK replaces active runs, while React LangGraph queues sends.\n abortControllerRef.current?.abort();\n const abortController = new AbortController();\n abortControllerRef.current = abortController;\n\n try {\n const response = await openAbortableIterable(\n stream(newMessagesWithId, {\n ...config,\n abortSignal: abortController.signal,\n initialize: async () => {\n return await aui.threadListItem.initialize();\n },\n }),\n abortController.signal,\n );\n if (!response) return;\n\n for await (const event of abortableIterable(\n response,\n abortController.signal,\n )) {\n if (\n abortController.signal.aborted ||\n abortControllerRef.current !== abortController\n ) {\n break;\n }\n const updatedMessages = accumulator.processEvent(event);\n setMessagesImmediate(updatedMessages);\n setStateDelta({\n ...stateDeltaRef.current,\n ...accumulator.getStateDelta(),\n });\n setAgentInfo(accumulator.getAgentInfo());\n setLongRunningToolIds(accumulator.getLongRunningToolIds());\n setArtifactDelta({\n ...artifactDeltaRef.current,\n ...accumulator.getArtifactDelta(),\n });\n setToolConfirmations(accumulator.getToolConfirmations());\n setAuthRequests(accumulator.getAuthRequests());\n setEscalated(accumulator.isEscalated());\n {\n const newMeta = accumulator.getMessageMetadata();\n if (newMeta.size > 0) {\n setMessageMetadata(\n new Map([...messageMetadataRef.current, ...newMeta]),\n );\n }\n }\n\n const transfer = accumulator.getLastTransferToAgent();\n if (transfer && transfer !== lastTransferToAgent) {\n lastTransferToAgent = transfer;\n invokeAdkRuntimeCallback(\n \"onAgentTransfer\",\n onAgentTransfer,\n transfer,\n );\n }\n\n // Fire custom event callback for events with customMetadata\n if (event.customMetadata && onCustomEvent) {\n for (const [key, value] of Object.entries(event.customMetadata)) {\n invokeAdkRuntimeCallback(\n \"onCustomEvent\",\n onCustomEvent,\n key,\n value,\n );\n }\n }\n\n if (event.errorCode || event.errorMessage) {\n invokeAdkRuntimeCallback(\n \"onError\",\n onError,\n event.errorMessage ?? event.errorCode,\n );\n }\n }\n } catch (error) {\n if (\n !abortController.signal.aborted &&\n abortControllerRef.current === abortController &&\n !(error instanceof Error && error.name === \"AbortError\")\n ) {\n throw error;\n }\n } finally {\n if (abortControllerRef.current === abortController) {\n if (abortController.signal.aborted) {\n setLongRunningToolIds(\n accumulator\n .getLongRunningToolIds()\n .filter((id) => initialLongRunningToolIds.includes(id)),\n );\n const updatedMessages = messagesRef.current;\n const lastAssistantMessage = updatedMessages.findLast(\n (m) => m.type === \"ai\",\n );\n if (\n lastAssistantMessage &&\n !initialMessageIds.has(lastAssistantMessage.id) &&\n !lastAssistantMessage.status\n ) {\n setMessagesImmediate(\n updatedMessages.map((m) =>\n m === lastAssistantMessage\n ? {\n ...lastAssistantMessage,\n status: { type: \"incomplete\", reason: \"cancelled\" },\n }\n : m,\n ),\n );\n }\n }\n abortControllerRef.current = null;\n }\n }\n },\n [\n aui,\n setMessagesImmediate,\n setLongRunningToolIds,\n stream,\n onError,\n onCustomEvent,\n onAgentTransfer,\n ],\n );\n\n const cancel = useCallback(() => {\n if (abortControllerRef.current) {\n abortControllerRef.current.abort();\n }\n }, []);\n\n useReplaySafeEffect(() => cancel, []);\n\n return {\n messages,\n stateDelta,\n agentInfo,\n longRunningToolIds,\n artifactDelta,\n toolConfirmations,\n authRequests,\n escalated,\n messageMetadata,\n sendMessage,\n cancel,\n setMessages: setMessagesImmediate,\n replaceMessages,\n applySnapshot,\n };\n};\n\n/**\n * Transport sends every human and tool message of one `send` call as a single\n * ADK `Content`, and ADK parses that event's function responses before running\n * any tool, so the batch runs whole or not at all. The optimistic projection\n * has to sit on the same boundary, so a run of those messages becomes one\n * synthetic event whose parts come from the same per-message conversion.\n *\n * The transport drops `ai` messages from that `Content`, so one interleaved\n * between two replies does not split the batch on the wire and must not split\n * it here either. It still becomes its own event, placed after the merged one,\n * so the optimistic projection keeps the assistant turn.\n *\n * @internal — exported for unit tests.\n */\nexport const messagesToEvents = (messages: AdkMessage[]): AdkEvent[] => {\n // A reload sends no messages at all, and the empty user content the transport\n // puts on the wire for it is not part of the optimistic view: projecting one\n // would put an empty user bubble above every regenerated turn.\n if (messages.length === 0) return [];\n\n const events: AdkEvent[] = [];\n const run: AdkMessage[] = [];\n let runIndex = 0;\n\n for (const msg of messages) {\n if (msg.type === \"ai\") {\n events.push(messageToEvent(msg));\n } else {\n if (run.length === 0) runIndex = events.length;\n run.push(msg);\n }\n }\n\n const parts = run.flatMap((m) => messageToEvent(m).content?.parts ?? []);\n const human = run.find((m) => m.type === \"human\");\n\n // A batch that contributes no part still reaches the wire: the transport\n // sends an empty user `Content`, which a reload replays as an empty human\n // message. Emitting it here keeps the optimistic view equal to that replay.\n if (parts.length === 0) parts.push({ text: \"\" });\n\n const event: AdkEvent = { id: (human ?? run[0])?.id ?? generateId() };\n if (human || run.length === 0) event.author = \"user\";\n event.content = { role: \"user\", parts };\n events.splice(run.length > 0 ? runIndex : events.length, 0, event);\n\n return events;\n};\n\n/** @internal — exported for unit tests. */\nexport const messageToEvent = (msg: AdkMessage): AdkEvent => {\n if (msg.type === \"human\") {\n return {\n id: msg.id ?? generateId(),\n author: \"user\",\n content: { role: \"user\", parts: contentToParts(msg.content) },\n };\n }\n\n if (msg.type === \"tool\") {\n let response: unknown;\n try {\n response = JSON.parse(msg.content);\n } catch {\n response = msg.content;\n }\n return {\n id: msg.id ?? generateId(),\n content: {\n role: \"user\",\n parts: [\n {\n functionResponse: {\n name: msg.name,\n id: msg.tool_call_id,\n response: toAdkFunctionResponse(response, msg.status === \"error\"),\n },\n },\n ],\n },\n };\n }\n\n const result: AdkEvent = { id: msg.id ?? generateId() };\n if (msg.author != null) result.author = msg.author;\n result.content = {\n role: \"model\",\n parts: [\n ...contentToParts(msg.content),\n ...(msg.tool_calls ?? []).filter(isRecord).map((tc) => ({\n functionCall: { name: tc.name, id: tc.id, args: { ...tc.args } },\n })),\n ],\n };\n return result;\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"useAdkMessages.js","names":[],"sources":["../src/useAdkMessages.ts"],"sourcesContent":["import { isRecord } from \"@assistant-ui/core/internal\";\nimport {\n useState,\n useCallback,\n useInsertionEffect,\n useRef,\n useMemo,\n} from \"react\";\nimport { generateId } from \"@assistant-ui/core\";\nimport { useReplaySafeEffect } from \"@assistant-ui/store/internal\";\nimport { useAui } from \"@assistant-ui/store\";\nimport {\n abortableIterable,\n invokeUserCallback,\n openAbortableIterable,\n} from \"@assistant-ui/core/internal\";\nimport { AdkEventAccumulator } from \"./AdkEventAccumulator\";\nimport { contentToParts } from \"./contentToParts\";\nimport { toAdkFunctionResponse } from \"./toAdkFunctionResponse\";\nimport type {\n AdkEvent,\n AdkMessage,\n AdkMessageMetadata,\n AdkSendMessageConfig,\n AdkStreamCallback,\n AdkToolConfirmation,\n AdkAuthRequest,\n AdkThreadSnapshot,\n OnAdkErrorCallback,\n OnAdkCustomEventCallback,\n OnAdkAgentTransferCallback,\n} from \"./types\";\n\nexport type UseAdkMessagesOptions = {\n stream: AdkStreamCallback;\n eventHandlers?: {\n onError?: OnAdkErrorCallback;\n onCustomEvent?: OnAdkCustomEventCallback;\n onAgentTransfer?: OnAdkAgentTransferCallback;\n };\n};\n\ntype UseAdkMessagesInternalOptions = UseAdkMessagesOptions & {\n onMessages?: (messages: AdkMessage[], runConfig: unknown) => void;\n};\n\ntype AdkRuntimeCallbackName = \"onError\" | \"onCustomEvent\" | \"onAgentTransfer\";\n\nconst invokeAdkRuntimeCallback = <TArgs extends readonly unknown[]>(\n name: AdkRuntimeCallbackName,\n callback: ((...args: TArgs) => unknown) | undefined,\n ...args: TArgs\n): void => {\n void invokeUserCallback(\"react-google-adk\", name, callback, ...args);\n};\n\nconst useAdkMessagesInternal = ({\n stream,\n eventHandlers,\n onMessages,\n}: UseAdkMessagesInternalOptions) => {\n const [messages, _setMessages] = useState<AdkMessage[]>([]);\n const [stateDelta, setStateDelta] = useState<Record<string, unknown>>({});\n const [agentInfo, setAgentInfo] = useState<{\n name?: string | undefined;\n branch?: string | undefined;\n }>({});\n const [longRunningToolIds, _setLongRunningToolIds] = useState<string[]>([]);\n const [artifactDelta, setArtifactDelta] = useState<Record<string, number>>(\n {},\n );\n const [toolConfirmations, setToolConfirmations] = useState<\n AdkToolConfirmation[]\n >([]);\n const [authRequests, setAuthRequests] = useState<AdkAuthRequest[]>([]);\n const [escalated, setEscalated] = useState(false);\n const [messageMetadata, setMessageMetadata] = useState<\n Map<string, AdkMessageMetadata>\n >(new Map());\n // setMessagesImmediate and setLongRunningToolIds are the only writers of their state and publish these refs with it, so neither ref trails a commit.\n const messagesRef = useRef(messages);\n const longRunningToolIdsRef = useRef(longRunningToolIds);\n const stateDeltaRef = useRef(stateDelta);\n useInsertionEffect(() => {\n stateDeltaRef.current = stateDelta;\n }, [stateDelta]);\n const artifactDeltaRef = useRef(artifactDelta);\n useInsertionEffect(() => {\n artifactDeltaRef.current = artifactDelta;\n }, [artifactDelta]);\n const messageMetadataRef = useRef(messageMetadata);\n useInsertionEffect(() => {\n messageMetadataRef.current = messageMetadata;\n }, [messageMetadata]);\n\n const setMessagesImmediate = useCallback((msgs: AdkMessage[]) => {\n messagesRef.current = msgs;\n _setMessages(msgs);\n }, []);\n const setLongRunningToolIds = useCallback((ids: string[]) => {\n longRunningToolIdsRef.current = ids;\n _setLongRunningToolIds(ids);\n }, []);\n\n /**\n * Swap the thread over to a loaded snapshot in one commit. Unlike\n * {@link replaceMessages} this never passes through a cleared state, so a\n * refetch that lands while a confirmation is on screen replaces it rather\n * than blanking it first.\n */\n const applySnapshot = useCallback(\n (snapshot: AdkThreadSnapshot) => {\n setMessagesImmediate(snapshot.messages);\n setLongRunningToolIds(snapshot.longRunningToolIds ?? []);\n setToolConfirmations(snapshot.toolConfirmations ?? []);\n setAuthRequests(snapshot.authRequests ?? []);\n setEscalated(snapshot.escalated ?? false);\n setMessageMetadata(snapshot.messageMetadata ?? new Map());\n setStateDelta(snapshot.stateDelta ?? {});\n setArtifactDelta(snapshot.artifactDelta ?? {});\n setAgentInfo(snapshot.agentInfo ?? {});\n },\n [setLongRunningToolIds, setMessagesImmediate],\n );\n\n // Replace the message list AND reset derived per-turn HITL state.\n // Used by truncation paths (edit, reload) so that stale interrupt\n // markers and per-message metadata from the removed messages don't leak\n // into the next turn.\n const replaceMessages = useCallback(\n (msgs: AdkMessage[]) => {\n setMessagesImmediate(msgs);\n setLongRunningToolIds([]);\n setToolConfirmations([]);\n setAuthRequests([]);\n setEscalated(false);\n setMessageMetadata(new Map());\n },\n [setLongRunningToolIds, setMessagesImmediate],\n );\n\n const abortControllerRef = useRef<AbortController | null>(null);\n\n const { onError, onCustomEvent, onAgentTransfer } = useMemo(\n () => eventHandlers ?? {},\n [eventHandlers],\n );\n\n const aui = useAui();\n const sendMessage = useCallback(\n async (newMessages: AdkMessage[], config: AdkSendMessageConfig) => {\n const newMessagesWithId = newMessages.map((m) =>\n m.id ? m : { ...m, id: generateId() },\n ) as AdkMessage[];\n\n // A staged message is already in the thread under its own id, and the\n // merged event below re-emits the whole batch under the first one. Seeding\n // with the originals would leave every later staged id beside the merged\n // copy of itself.\n const resentIds = new Set(newMessagesWithId.map((m) => m.id));\n // The optimistic event for a tool-only batch carries no author, so the accumulator cannot settle the calls this send answers.\n const answeredToolCallIds = new Set(\n newMessagesWithId.flatMap((m) =>\n m.type === \"tool\" ? [m.tool_call_id] : [],\n ),\n );\n const accumulator = new AdkEventAccumulator(\n messagesRef.current.filter((m) => !resentIds.has(m.id)),\n longRunningToolIdsRef.current.filter(\n (id) => !answeredToolCallIds.has(id),\n ),\n );\n for (const event of messagesToEvents(newMessagesWithId)) {\n accumulator.processEvent(event);\n }\n const initialMessages = accumulator.getMessages();\n const initialMessageIds = new Set(initialMessages.map((m) => m.id));\n const initialLongRunningToolIds = accumulator.getLongRunningToolIds();\n setMessagesImmediate(initialMessages);\n setLongRunningToolIds(initialLongRunningToolIds);\n setToolConfirmations(accumulator.getToolConfirmations());\n setAuthRequests(accumulator.getAuthRequests());\n let lastTransferToAgent: string | undefined;\n\n // Google ADK replaces active runs, while React LangGraph queues sends.\n abortControllerRef.current?.abort();\n const abortController = new AbortController();\n abortControllerRef.current = abortController;\n\n try {\n const response = await openAbortableIterable(\n stream(newMessagesWithId, {\n ...config,\n abortSignal: abortController.signal,\n initialize: async () => {\n return await aui.threadListItem.initialize();\n },\n }),\n abortController.signal,\n );\n if (!response) return;\n\n for await (const event of abortableIterable(\n response,\n abortController.signal,\n )) {\n if (\n abortController.signal.aborted ||\n abortControllerRef.current !== abortController\n ) {\n break;\n }\n const updatedMessages = accumulator.processEvent(event);\n // Each event part can append at most one message, and a function call\n // stays on the current assistant message until a later part finalizes\n // it, so every message touched by this event is within this tail.\n const affectedMessageCount = Math.max(\n event.content?.parts?.length ?? 0,\n 1,\n );\n const affectedMessages = updatedMessages.slice(-affectedMessageCount);\n if (affectedMessages.length > 0) {\n onMessages?.(affectedMessages, config.runConfig);\n }\n setMessagesImmediate(updatedMessages);\n setStateDelta({\n ...stateDeltaRef.current,\n ...accumulator.getStateDelta(),\n });\n setAgentInfo(accumulator.getAgentInfo());\n setLongRunningToolIds(accumulator.getLongRunningToolIds());\n setArtifactDelta({\n ...artifactDeltaRef.current,\n ...accumulator.getArtifactDelta(),\n });\n setToolConfirmations(accumulator.getToolConfirmations());\n setAuthRequests(accumulator.getAuthRequests());\n setEscalated(accumulator.isEscalated());\n {\n const newMeta = accumulator.getMessageMetadata();\n if (newMeta.size > 0) {\n setMessageMetadata(\n new Map([...messageMetadataRef.current, ...newMeta]),\n );\n }\n }\n\n const transfer = accumulator.getLastTransferToAgent();\n if (transfer && transfer !== lastTransferToAgent) {\n lastTransferToAgent = transfer;\n invokeAdkRuntimeCallback(\n \"onAgentTransfer\",\n onAgentTransfer,\n transfer,\n );\n }\n\n // Fire custom event callback for events with customMetadata\n if (event.customMetadata && onCustomEvent) {\n for (const [key, value] of Object.entries(event.customMetadata)) {\n invokeAdkRuntimeCallback(\n \"onCustomEvent\",\n onCustomEvent,\n key,\n value,\n );\n }\n }\n\n if (event.errorCode || event.errorMessage) {\n invokeAdkRuntimeCallback(\n \"onError\",\n onError,\n event.errorMessage ?? event.errorCode,\n );\n }\n }\n } catch (error) {\n if (\n !abortController.signal.aborted &&\n abortControllerRef.current === abortController &&\n !(error instanceof Error && error.name === \"AbortError\")\n ) {\n throw error;\n }\n } finally {\n if (abortControllerRef.current === abortController) {\n if (abortController.signal.aborted) {\n setLongRunningToolIds(\n accumulator\n .getLongRunningToolIds()\n .filter((id) => initialLongRunningToolIds.includes(id)),\n );\n const updatedMessages = messagesRef.current;\n const lastAssistantMessage = updatedMessages.findLast(\n (m) => m.type === \"ai\",\n );\n if (\n lastAssistantMessage &&\n !initialMessageIds.has(lastAssistantMessage.id) &&\n !lastAssistantMessage.status\n ) {\n setMessagesImmediate(\n updatedMessages.map((m) =>\n m === lastAssistantMessage\n ? {\n ...lastAssistantMessage,\n status: { type: \"incomplete\", reason: \"cancelled\" },\n }\n : m,\n ),\n );\n }\n }\n abortControllerRef.current = null;\n }\n }\n },\n [\n aui,\n setMessagesImmediate,\n setLongRunningToolIds,\n stream,\n onError,\n onCustomEvent,\n onAgentTransfer,\n onMessages,\n ],\n );\n\n const cancel = useCallback(() => {\n if (abortControllerRef.current) {\n abortControllerRef.current.abort();\n }\n }, []);\n\n useReplaySafeEffect(() => cancel, []);\n\n return {\n messages,\n stateDelta,\n agentInfo,\n longRunningToolIds,\n artifactDelta,\n toolConfirmations,\n authRequests,\n escalated,\n messageMetadata,\n sendMessage,\n cancel,\n setMessages: setMessagesImmediate,\n replaceMessages,\n applySnapshot,\n };\n};\n\nexport const useAdkMessages = ({\n stream,\n eventHandlers,\n}: UseAdkMessagesOptions) =>\n useAdkMessagesInternal({\n stream,\n ...(eventHandlers !== undefined && { eventHandlers }),\n });\n\nexport { useAdkMessagesInternal };\n\n/**\n * Transport sends every human and tool message of one `send` call as a single\n * ADK `Content`, and ADK parses that event's function responses before running\n * any tool, so the batch runs whole or not at all. The optimistic projection\n * has to sit on the same boundary, so a run of those messages becomes one\n * synthetic event whose parts come from the same per-message conversion.\n *\n * The transport drops `ai` messages from that `Content`, so one interleaved\n * between two replies does not split the batch on the wire and must not split\n * it here either. It still becomes its own event, placed after the merged one,\n * so the optimistic projection keeps the assistant turn.\n *\n * @internal — exported for unit tests.\n */\nexport const messagesToEvents = (messages: AdkMessage[]): AdkEvent[] => {\n // A reload sends no messages at all, and the empty user content the transport\n // puts on the wire for it is not part of the optimistic view: projecting one\n // would put an empty user bubble above every regenerated turn.\n if (messages.length === 0) return [];\n\n const events: AdkEvent[] = [];\n const run: AdkMessage[] = [];\n let runIndex = 0;\n\n for (const msg of messages) {\n if (msg.type === \"ai\") {\n events.push(messageToEvent(msg));\n } else {\n if (run.length === 0) runIndex = events.length;\n run.push(msg);\n }\n }\n\n const parts = run.flatMap((m) => messageToEvent(m).content?.parts ?? []);\n const human = run.find((m) => m.type === \"human\");\n\n // A batch that contributes no part still reaches the wire: the transport\n // sends an empty user `Content`, which a reload replays as an empty human\n // message. Emitting it here keeps the optimistic view equal to that replay.\n if (parts.length === 0) parts.push({ text: \"\" });\n\n const event: AdkEvent = { id: (human ?? run[0])?.id ?? generateId() };\n if (human || run.length === 0) event.author = \"user\";\n event.content = { role: \"user\", parts };\n events.splice(run.length > 0 ? runIndex : events.length, 0, event);\n\n return events;\n};\n\n/** @internal — exported for unit tests. */\nexport const messageToEvent = (msg: AdkMessage): AdkEvent => {\n if (msg.type === \"human\") {\n return {\n id: msg.id ?? generateId(),\n author: \"user\",\n content: { role: \"user\", parts: contentToParts(msg.content) },\n };\n }\n\n if (msg.type === \"tool\") {\n let response: unknown;\n try {\n response = JSON.parse(msg.content);\n } catch {\n response = msg.content;\n }\n return {\n id: msg.id ?? generateId(),\n content: {\n role: \"user\",\n parts: [\n {\n functionResponse: {\n name: msg.name,\n id: msg.tool_call_id,\n response: toAdkFunctionResponse(response, msg.status === \"error\"),\n },\n },\n ],\n },\n };\n }\n\n const result: AdkEvent = { id: msg.id ?? generateId() };\n if (msg.author != null) result.author = msg.author;\n result.content = {\n role: \"model\",\n parts: [\n ...contentToParts(msg.content),\n ...(msg.tool_calls ?? []).filter(isRecord).map((tc) => ({\n functionCall: { name: tc.name, id: tc.id, args: { ...tc.args } },\n })),\n ],\n };\n return result;\n};\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"useAdkRuntime.d.ts","sourceRoot":"","sources":["../src/useAdkRuntime.ts"],"names":[],"mappings":"AAOA,OAAO,EAEL,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,KAAK,0BAA0B,EAC/B,KAAK,eAAe,EACpB,KAAK,oBAAoB,EACzB,KAAK,sBAAsB,EAK5B,MAAM,oBAAoB,CAAC;AAa5B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACtD,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,oBAAoB,CAAC;AAClE,OAAO,KAAK,EACV,UAAU,EACV,iBAAiB,EAEjB,iBAAiB,EACjB,kBAAkB,EAClB,wBAAwB,EACxB,0BAA0B,EAC3B,mBAAgB;AAmBjB,MAAM,MAAM,oBAAoB,GAAG,0BAA0B,GAAG;IAC9D,MAAM,EAAE,iBAAiB,CAAC;IAC1B;;;;;;;OAOG;IACH,gBAAgB,CAAC,EAAE,CAAC,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,KAAK,IAAI,CAAC,GAAG,SAAS,CAAC;IACxE,0BAA0B,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACjD,0BAA0B,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACjD,eAAe,CAAC,EAAE,CAChB,QAAQ,EAAE,MAAM,EAChB,cAAc,EAAE,UAAU,EAAE,KACzB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC5B;;;;;OAKG;IACH,IAAI,CAAC,EAAE,CACL,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,WAAW,GAAG,SAAS,CAAA;KAAE,KAC3C,OAAO,CAAC,iBAAiB,CAAC,CAAC;IAChC,MAAM,CAAC,EAAE,MAAM,OAAO,CAAC;QAAE,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC/C,MAAM,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7C,QAAQ,CAAC,EACL;QACE,WAAW,CAAC,EAAE,iBAAiB,CAAC;QAChC,MAAM,CAAC,EAAE,sBAAsB,CAAC;QAChC,SAAS,CAAC,EAAE,gBAAgB,CAAC;QAC7B,KAAK,CAAC,EAAE,oBAAoB,CAAC;QAC7B,QAAQ,CAAC,EAAE,eAAe,CAAC;KAC5B,GACD,SAAS,CAAC;IACd,aAAa,CAAC,EACV;QACE,OAAO,CAAC,EAAE,kBAAkB,CAAC;QAC7B,aAAa,CAAC,EAAE,wBAAwB,CAAC;QACzC,eAAe,CAAC,EAAE,0BAA0B,CAAC;KAC9C,GACD,SAAS,CAAC;IACd,KAAK,CAAC,EAAE,cAAc,GAAG,SAAS,CAAC;IACnC;;;OAGG;IACH,cAAc,CAAC,EAAE,uBAAuB,GAAG,SAAS,CAAC;CACtD,CAAC;AAiYF,eAAO,MAAM,aAAa,GAAI,mFAO3B,oBAAoB,kDAsBtB,CAAC"}
1
+ {"version":3,"file":"useAdkRuntime.d.ts","sourceRoot":"","sources":["../src/useAdkRuntime.ts"],"names":[],"mappings":"AAOA,OAAO,EAEL,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,KAAK,0BAA0B,EAC/B,KAAK,eAAe,EACpB,KAAK,oBAAoB,EACzB,KAAK,sBAAsB,EAK5B,MAAM,oBAAoB,CAAC;AAc5B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACtD,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,oBAAoB,CAAC;AAClE,OAAO,KAAK,EACV,UAAU,EACV,iBAAiB,EAEjB,iBAAiB,EACjB,kBAAkB,EAClB,wBAAwB,EACxB,0BAA0B,EAC3B,mBAAgB;AAmBjB,MAAM,MAAM,oBAAoB,GAAG,0BAA0B,GAAG;IAC9D,MAAM,EAAE,iBAAiB,CAAC;IAC1B;;;;;;;OAOG;IACH,gBAAgB,CAAC,EAAE,CAAC,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,KAAK,IAAI,CAAC,GAAG,SAAS,CAAC;IACxE,0BAA0B,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACjD,0BAA0B,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACjD,eAAe,CAAC,EAAE,CAChB,QAAQ,EAAE,MAAM,EAChB,cAAc,EAAE,UAAU,EAAE,KACzB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC5B;;;;;OAKG;IACH,IAAI,CAAC,EAAE,CACL,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,WAAW,GAAG,SAAS,CAAA;KAAE,KAC3C,OAAO,CAAC,iBAAiB,CAAC,CAAC;IAChC,MAAM,CAAC,EAAE,MAAM,OAAO,CAAC;QAAE,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC/C,MAAM,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7C,QAAQ,CAAC,EACL;QACE,WAAW,CAAC,EAAE,iBAAiB,CAAC;QAChC,MAAM,CAAC,EAAE,sBAAsB,CAAC;QAChC,SAAS,CAAC,EAAE,gBAAgB,CAAC;QAC7B,KAAK,CAAC,EAAE,oBAAoB,CAAC;QAC7B,QAAQ,CAAC,EAAE,eAAe,CAAC;KAC5B,GACD,SAAS,CAAC;IACd,aAAa,CAAC,EACV;QACE,OAAO,CAAC,EAAE,kBAAkB,CAAC;QAC7B,aAAa,CAAC,EAAE,wBAAwB,CAAC;QACzC,eAAe,CAAC,EAAE,0BAA0B,CAAC;KAC9C,GACD,SAAS,CAAC;IACd,KAAK,CAAC,EAAE,cAAc,GAAG,SAAS,CAAC;IACnC;;;OAGG;IACH,cAAc,CAAC,EAAE,uBAAuB,GAAG,SAAS,CAAC;CACtD,CAAC;AAkdF,eAAO,MAAM,aAAa,GAAI,mFAO3B,oBAAoB,kDAsBtB,CAAC"}
@@ -2,9 +2,9 @@ import { projectAdkToolApprovals, toAdkToolConfirmationReply } from "./adkToolAp
2
2
  import { adkExtras } from "./adkExtras.js";
3
3
  import { convertAdkMessage, createAdkMessageConverter } from "./convertAdkMessages.js";
4
4
  import { getMessageContent, getPendingCancellations, toAdkUserMessage, truncateAdkMessages } from "./convertToAdkMessages.js";
5
- import { useAdkMessages } from "./useAdkMessages.js";
5
+ import { useAdkMessagesInternal } from "./useAdkMessages.js";
6
6
  import { ADK_SDK } from "./sdkIdentity.js";
7
- import { createAbortableThreadLoad, createCloudThreadListAdapterCreateFallback } from "@assistant-ui/core/internal";
7
+ import { createAbortableThreadLoad, createCloudThreadListAdapterCreateFallback, isRecord } from "@assistant-ui/core/internal";
8
8
  import { generateId, pickExternalStoreSharedOptions } from "@assistant-ui/core";
9
9
  import { useCloudThreadListAdapter, useExternalMessageConverter, useExternalStoreRuntime, useRemoteThreadListRuntime } from "@assistant-ui/core/react";
10
10
  import { useAui } from "@assistant-ui/store";
@@ -14,9 +14,47 @@ import { useCallback, useInsertionEffect, useMemo, useRef, useState } from "reac
14
14
  const useAdkRuntimeImpl = (options) => {
15
15
  const { autoCancelPendingToolCalls, adapters: { attachments, dictation, feedback, speech, voice } = {}, unstable_allowCancellation, stream, load, getCheckpointId, eventHandlers } = options;
16
16
  const aui = useAui();
17
- const { messages, stateDelta, agentInfo, longRunningToolIds, artifactDelta, toolConfirmations, authRequests, escalated, messageMetadata, sendMessage, cancel, setMessages, replaceMessages, applySnapshot } = useAdkMessages({
17
+ const runConfigByToolCallIdRef = useRef(/* @__PURE__ */ new Map());
18
+ const rememberMessageOwnership = useCallback((newMessages, runConfig) => {
19
+ const toolOwnership = runConfigByToolCallIdRef.current;
20
+ for (const message of newMessages) {
21
+ if (message.type !== "ai") continue;
22
+ for (const toolCall of message.tool_calls ?? []) {
23
+ if (!isRecord(toolCall)) continue;
24
+ if (!toolOwnership.has(toolCall.id)) toolOwnership.set(toolCall.id, runConfig);
25
+ }
26
+ }
27
+ }, []);
28
+ const seedMessageOwnership = useCallback((history) => {
29
+ const currentOwnership = runConfigByToolCallIdRef.current;
30
+ const nextOwnership = /* @__PURE__ */ new Map();
31
+ for (const message of history) {
32
+ if (message.type !== "ai") continue;
33
+ for (const toolCall of message.tool_calls ?? []) {
34
+ if (!isRecord(toolCall)) continue;
35
+ nextOwnership.set(toolCall.id, currentOwnership.has(toolCall.id) ? currentOwnership.get(toolCall.id) : void 0);
36
+ }
37
+ }
38
+ runConfigByToolCallIdRef.current = nextOwnership;
39
+ }, []);
40
+ const pruneMessageOwnership = useCallback((history) => {
41
+ const toolCallIds = /* @__PURE__ */ new Set();
42
+ for (const message of history) {
43
+ if (message.type !== "ai") continue;
44
+ for (const toolCall of message.tool_calls ?? []) {
45
+ if (!isRecord(toolCall)) continue;
46
+ toolCallIds.add(toolCall.id);
47
+ }
48
+ }
49
+ for (const id of runConfigByToolCallIdRef.current.keys()) if (!toolCallIds.has(id)) runConfigByToolCallIdRef.current.delete(id);
50
+ }, []);
51
+ const getToolRunConfig = useCallback((toolCallId) => {
52
+ return runConfigByToolCallIdRef.current.get(toolCallId);
53
+ }, []);
54
+ const { messages, stateDelta, agentInfo, longRunningToolIds, artifactDelta, toolConfirmations, authRequests, escalated, messageMetadata, sendMessage, cancel, setMessages, replaceMessages: replaceAdkMessages, applySnapshot: applyAdkSnapshot } = useAdkMessagesInternal({
18
55
  stream,
19
- ...eventHandlers && { eventHandlers }
56
+ ...eventHandlers && { eventHandlers },
57
+ onMessages: rememberMessageOwnership
20
58
  });
21
59
  const loadRef = useRef(load);
22
60
  useInsertionEffect(() => {
@@ -27,6 +65,14 @@ const useAdkRuntimeImpl = (options) => {
27
65
  useInsertionEffect(() => {
28
66
  messagesRef.current = messages;
29
67
  }, [messages]);
68
+ const applySnapshot = useCallback((snapshot) => {
69
+ seedMessageOwnership(snapshot.messages);
70
+ applyAdkSnapshot(snapshot);
71
+ }, [applyAdkSnapshot, seedMessageOwnership]);
72
+ const replaceMessages = useCallback((nextMessages) => {
73
+ pruneMessageOwnership(nextMessages);
74
+ replaceAdkMessages(nextMessages);
75
+ }, [pruneMessageOwnership, replaceAdkMessages]);
30
76
  const [isLoadingThread, setIsLoadingThread] = useState(() => load !== void 0 && aui.threadListItem.getState().externalId != null);
31
77
  const [isRunning, setIsRunning] = useState(false);
32
78
  const [toolStatuses, setToolStatuses] = useState({});
@@ -38,10 +84,14 @@ const useAdkRuntimeImpl = (options) => {
38
84
  }, [effectiveIsRunning]);
39
85
  const runGenerationRef = useRef(0);
40
86
  const handleSendMessage = async (msgs, config) => {
87
+ const continuationConfig = msgs.length > 0 && msgs.every((msg) => msg.type === "tool") && config.runConfig === void 0 ? {
88
+ ...config,
89
+ runConfig: getToolRunConfig(msgs[0].tool_call_id)
90
+ } : config;
41
91
  const generation = ++runGenerationRef.current;
42
92
  try {
43
93
  setIsRunning(true);
44
- await sendMessage(msgs, config);
94
+ await sendMessage(msgs, continuationConfig);
45
95
  } finally {
46
96
  if (runGenerationRef.current === generation) setIsRunning(false);
47
97
  }
@@ -1 +1 @@
1
- {"version":3,"file":"useAdkRuntime.js","names":[],"sources":["../src/useAdkRuntime.ts"],"sourcesContent":["import {\n useCallback,\n useInsertionEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport {\n pickExternalStoreSharedOptions,\n type AttachmentAdapter,\n type DictationAdapter,\n type ExternalStoreSharedOptions,\n type FeedbackAdapter,\n type RealtimeVoiceAdapter,\n type SpeechSynthesisAdapter,\n type AppendMessage,\n type ToolCallMessagePart,\n type ToolExecutionStatus,\n generateId,\n} from \"@assistant-ui/core\";\nimport {\n createAbortableThreadLoad,\n createCloudThreadListAdapterCreateFallback,\n} from \"@assistant-ui/core/internal\";\nimport {\n useCloudThreadListAdapter,\n useRemoteThreadListRuntime,\n useExternalMessageConverter,\n useExternalStoreRuntime,\n} from \"@assistant-ui/core/react\";\nimport { useAui } from \"@assistant-ui/store\";\nimport { useReplaySafeEffect } from \"@assistant-ui/store/internal\";\nimport type { AssistantCloud } from \"assistant-cloud\";\nimport type { RemoteThreadListAdapter } from \"@assistant-ui/core\";\nimport type {\n AdkMessage,\n AdkThreadSnapshot,\n AdkSendMessageConfig,\n AdkStreamCallback,\n OnAdkErrorCallback,\n OnAdkCustomEventCallback,\n OnAdkAgentTransferCallback,\n} from \"./types\";\nimport { useAdkMessages } from \"./useAdkMessages\";\nimport {\n convertAdkMessage,\n createAdkMessageConverter,\n} from \"./convertAdkMessages\";\nimport {\n getMessageContent,\n getPendingCancellations,\n toAdkUserMessage,\n truncateAdkMessages,\n} from \"./convertToAdkMessages\";\nimport {\n projectAdkToolApprovals,\n toAdkToolConfirmationReply,\n} from \"./adkToolApproval\";\nimport { adkExtras } from \"./adkExtras\";\nimport { ADK_SDK } from \"./sdkIdentity\";\n\nexport type UseAdkRuntimeOptions = ExternalStoreSharedOptions & {\n stream: AdkStreamCallback;\n /**\n * Called whenever the active thread's canonical (remote) ID changes, so the\n * value can be treated as a managed/controlled variable (e.g. synced to a URL\n * query param). Only the settled remote ID is emitted: while a freshly created\n * thread is still optimistic the value is `undefined`, and the real ID is\n * emitted once the thread is initialized; the transient local ID is never\n * surfaced.\n */\n onThreadIdChange?: ((threadId: string | undefined) => void) | undefined;\n autoCancelPendingToolCalls?: boolean | undefined;\n unstable_allowCancellation?: boolean | undefined;\n getCheckpointId?: (\n threadId: string,\n parentMessages: AdkMessage[],\n ) => Promise<string | null>;\n /**\n * Loads a thread's stored state. Called when the thread opens, and again for\n * `threads.reloadMainThread()`, which refetches in place rather than\n * remounting the runtime; the signal aborts a load the runtime no longer\n * needs.\n */\n load?: (\n threadId: string,\n options?: { signal?: AbortSignal | undefined },\n ) => Promise<AdkThreadSnapshot>;\n create?: () => Promise<{ externalId: string }>;\n delete?: (threadId: string) => Promise<void>;\n adapters?:\n | {\n attachments?: AttachmentAdapter;\n speech?: SpeechSynthesisAdapter;\n dictation?: DictationAdapter;\n voice?: RealtimeVoiceAdapter;\n feedback?: FeedbackAdapter;\n }\n | undefined;\n eventHandlers?:\n | {\n onError?: OnAdkErrorCallback;\n onCustomEvent?: OnAdkCustomEventCallback;\n onAgentTransfer?: OnAdkAgentTransferCallback;\n }\n | undefined;\n cloud?: AssistantCloud | undefined;\n /**\n * A `RemoteThreadListAdapter` to use instead of the cloud adapter.\n * Use with `createAdkSessionAdapter` for ADK session-backed persistence.\n */\n sessionAdapter?: RemoteThreadListAdapter | undefined;\n};\n\nconst useAdkRuntimeImpl = (options: UseAdkRuntimeOptions) => {\n const {\n autoCancelPendingToolCalls,\n adapters: { attachments, dictation, feedback, speech, voice } = {},\n unstable_allowCancellation,\n stream,\n load,\n getCheckpointId,\n eventHandlers,\n } = options;\n const aui = useAui();\n const {\n messages,\n stateDelta,\n agentInfo,\n longRunningToolIds,\n artifactDelta,\n toolConfirmations,\n authRequests,\n escalated,\n messageMetadata,\n sendMessage,\n cancel,\n setMessages,\n replaceMessages,\n applySnapshot,\n } = useAdkMessages({\n stream,\n ...(eventHandlers && { eventHandlers }),\n });\n\n const loadRef = useRef(load);\n useInsertionEffect(() => {\n loadRef.current = load;\n }, [load]);\n const [loadController] = useState(createAbortableThreadLoad);\n const messagesRef = useRef(messages);\n useInsertionEffect(() => {\n messagesRef.current = messages;\n }, [messages]);\n const [isLoadingThread, setIsLoadingThread] = useState(\n () =>\n load !== undefined && aui.threadListItem.getState().externalId != null,\n );\n\n const [isRunning, setIsRunning] = useState(false);\n const [toolStatuses, setToolStatuses] = useState<\n Record<string, ToolExecutionStatus>\n >({});\n const hasExecutingTools = Object.values(toolStatuses).some(\n (s) => s?.type === \"executing\",\n );\n const effectiveIsRunning = isRunning || hasExecutingTools;\n const isRunningRef = useRef(effectiveIsRunning);\n useInsertionEffect(() => {\n isRunningRef.current = effectiveIsRunning;\n }, [effectiveIsRunning]);\n const runGenerationRef = useRef(0);\n\n const handleSendMessage = async (\n msgs: AdkMessage[],\n config: AdkSendMessageConfig,\n ) => {\n const generation = ++runGenerationRef.current;\n try {\n setIsRunning(true);\n await sendMessage(msgs, config);\n } finally {\n if (runGenerationRef.current === generation) setIsRunning(false);\n }\n };\n\n const { approvals: toolApprovals, key: toolApprovalsKey } =\n projectAdkToolApprovals(messages);\n // The messageConverter memo below reads this during render, where the ref\n // must carry the same render's approvals; a commit-scoped write would feed\n // the memo the previous commit's approvals whenever the key changes. No\n // callback reads it — approval replies project from the committed messages.\n const toolApprovalsRef = useRef(toolApprovals);\n toolApprovalsRef.current = toolApprovals;\n\n const longRunningToolIdsRef = useRef(longRunningToolIds);\n useInsertionEffect(() => {\n longRunningToolIdsRef.current = longRunningToolIds;\n }, [longRunningToolIds]);\n // ADK resolves every call it did not mark long-running itself, and yields\n // that call to the client one or more events before its own response, so\n // only a long-running call is the client's to execute.\n const isClientToolCall = useCallback(\n (toolCall: ToolCallMessagePart) =>\n longRunningToolIdsRef.current.includes(toolCall.toolCallId),\n [],\n );\n\n const messageConverter = useMemo(\n () =>\n toolApprovalsKey === \"\"\n ? convertAdkMessage\n : createAdkMessageConverter(toolApprovalsRef.current),\n [toolApprovalsKey],\n );\n\n const threadMessages = useExternalMessageConverter({\n callback: messageConverter,\n messages,\n isRunning: effectiveIsRunning,\n });\n\n const threadMessagesRef = useRef(threadMessages);\n useInsertionEffect(() => {\n threadMessagesRef.current = threadMessages;\n }, [threadMessages]);\n\n // Staging assigns adkMessagesRef.current directly, so the effect must key on\n // the committed messages alone; a dep-less publication would clobber the\n // optimistic value on any unrelated commit.\n const adkMessagesRef = useRef(messages);\n useInsertionEffect(() => {\n adkMessagesRef.current = messages;\n }, [messages]);\n\n const stagedMessagesRef = useRef(\n new Map<\n string,\n {\n message: AdkMessage & { id: string };\n runConfig: AppendMessage[\"runConfig\"];\n }\n >(),\n );\n const [stagedMessageCount, setStagedMessageCount] = useState(0);\n const hasStagedMessages = stagedMessageCount > 0;\n\n const getStagedRun = (parentId: string | null) => {\n if (!parentId || !stagedMessagesRef.current.has(parentId)) return null;\n\n const staged: AdkMessage[] = [];\n for (const message of adkMessagesRef.current) {\n if (message.id && stagedMessagesRef.current.has(message.id)) {\n staged.push(stagedMessagesRef.current.get(message.id)!.message);\n }\n if (message.id === parentId) break;\n }\n\n return {\n messages: staged,\n runConfig: stagedMessagesRef.current.get(parentId)!.runConfig,\n };\n };\n\n const stageUserMessage = (msg: AppendMessage) => {\n const stagedMessage = toAdkUserMessage(msg);\n stagedMessagesRef.current.set(stagedMessage.id, {\n message: stagedMessage,\n runConfig: msg.runConfig,\n });\n setStagedMessageCount(stagedMessagesRef.current.size);\n const nextMessages = [...adkMessagesRef.current, stagedMessage];\n adkMessagesRef.current = nextMessages;\n setMessages(nextMessages);\n };\n\n // The scoped client, not `aui` itself: useAui returns a render-bound\n // instance, so depending on it would re-run the load on every render.\n const threadListItem =\n aui.threadListItem.source !== null ? aui.threadListItem : undefined;\n\n const runLoad = useCallback(\n (purpose: \"initial\" | \"reload\" = \"initial\") => {\n const loadFn = loadRef.current;\n if (!loadFn || !threadListItem) return Promise.resolve();\n\n const externalId = threadListItem.getState().externalId;\n if (externalId == null) return Promise.resolve();\n\n // The initial load is already fetching what a refetch would ask for, and\n // taking it over strands the thread's history if the refetch then fails.\n // Aborting a load the runtime no longer needs is not a failure.\n // A refetch reports the failure to whoever awaited it; the initial load\n // has no caller to tell.\n return loadController.run({\n purpose,\n load: async (signal) => {\n const messagesAtLoadStart = messagesRef.current;\n if (purpose === \"initial\") setIsLoadingThread(true);\n\n const snapshot = await loadFn(externalId, { signal });\n if (signal.aborted) return;\n // A snapshot the session assembled before a run cannot speak for what\n // that run has since produced, and an ADK id cannot correlate a\n // message sent optimistically with the one the session stored for it,\n // so there is nothing here that could merge the two. A refetch that\n // raced a run therefore defers to the run, whether the run started\n // during the load or was already streaming when it began.\n if (\n purpose === \"reload\" &&\n (isRunningRef.current ||\n messagesRef.current !== messagesAtLoadStart)\n )\n return;\n applySnapshot(snapshot);\n },\n onSettled: () => {\n setIsLoadingThread(false);\n },\n onInitialError: (error) => {\n console.warn(\"Failed to load ADK session:\", error);\n },\n });\n },\n [threadListItem, loadController, applySnapshot],\n );\n\n useReplaySafeEffect(() => {\n runLoad();\n return () => {\n // Whatever is current, not this effect's own controller: a refetch swaps\n // the ref, and one in flight at unmount must be aborted too.\n loadController.abort();\n setIsLoadingThread(false);\n };\n }, [threadListItem]);\n\n const runtime = useExternalStoreRuntime({\n ...pickExternalStoreSharedOptions(options),\n isRunning,\n isLoading: isLoadingThread,\n messages: threadMessages,\n unstable_enableToolInvocations: true,\n unstable_isClientToolCall: isClientToolCall,\n setToolStatuses,\n adapters: { attachments, dictation, feedback, speech, voice },\n extras: adkExtras.provide({\n agentInfo,\n stateDelta,\n artifactDelta,\n longRunningToolIds,\n toolConfirmations,\n authRequests,\n escalated,\n messageMetadata,\n send: handleSendMessage,\n }),\n onNew: async (msg) => {\n if (!(msg.startRun ?? msg.role === \"user\")) {\n stageUserMessage(msg);\n return;\n }\n\n const cancellations =\n autoCancelPendingToolCalls !== false\n ? getPendingCancellations(messages, longRunningToolIds)\n : [];\n\n return handleSendMessage(\n [\n ...cancellations,\n {\n id: generateId(),\n type: \"human\",\n content: getMessageContent(msg),\n },\n ],\n { runConfig: msg.runConfig },\n );\n },\n onEdit: getCheckpointId\n ? async (msg) => {\n const truncated = truncateAdkMessages(\n threadMessagesRef.current,\n msg.parentId,\n );\n replaceMessages(truncated);\n if (!(msg.startRun ?? msg.role === \"user\")) {\n const stagedMessage = toAdkUserMessage(msg);\n stagedMessagesRef.current.set(stagedMessage.id, {\n message: stagedMessage,\n runConfig: msg.runConfig,\n });\n setStagedMessageCount(stagedMessagesRef.current.size);\n const nextMessages = [...truncated, stagedMessage];\n adkMessagesRef.current = nextMessages;\n setMessages(nextMessages);\n return;\n }\n const externalId = aui.threadListItem.getState().externalId;\n const checkpointId = externalId\n ? await getCheckpointId(externalId, truncated)\n : null;\n return handleSendMessage(\n [\n {\n id: generateId(),\n type: \"human\",\n content: getMessageContent(msg),\n },\n ],\n {\n runConfig: msg.runConfig,\n ...(checkpointId && { checkpointId }),\n },\n );\n }\n : undefined,\n ...(getCheckpointId || hasStagedMessages\n ? {\n onReload: async (parentId, config) => {\n const stagedRun = getStagedRun(parentId);\n if (stagedRun) {\n for (const message of stagedRun.messages) {\n stagedMessagesRef.current.delete(message.id);\n }\n setStagedMessageCount(stagedMessagesRef.current.size);\n return handleSendMessage(stagedRun.messages, {\n runConfig: config.runConfig ?? stagedRun.runConfig,\n });\n }\n\n if (!getCheckpointId)\n throw new Error(\"Runtime does not support reloading messages.\");\n\n const truncated = truncateAdkMessages(\n threadMessagesRef.current,\n parentId,\n );\n replaceMessages(truncated);\n const externalId = aui.threadListItem.getState().externalId;\n const checkpointId = externalId\n ? await getCheckpointId(externalId, truncated)\n : null;\n return handleSendMessage([], {\n runConfig: config.runConfig,\n ...(checkpointId && { checkpointId }),\n });\n },\n }\n : {}),\n onAddToolResult: async ({\n toolCallId,\n toolName,\n result,\n isError,\n artifact,\n }) => {\n await handleSendMessage(\n [\n {\n id: generateId(),\n type: \"tool\",\n name: toolName,\n tool_call_id: toolCallId,\n content: JSON.stringify(result),\n artifact,\n status: isError ? \"error\" : \"success\",\n },\n ],\n {},\n );\n },\n onRespondToToolApproval: async (options) => {\n await handleSendMessage(\n [\n toAdkToolConfirmationReply(\n options,\n projectAdkToolApprovals(adkMessagesRef.current).approvals,\n ),\n ],\n {},\n );\n },\n onCancel: unstable_allowCancellation\n ? async () => {\n cancel();\n }\n : undefined,\n ...(load !== undefined && {\n onRefetchThread: () => runLoad(\"reload\"),\n }),\n });\n\n return runtime;\n};\n\nexport const useAdkRuntime = ({\n cloud,\n sessionAdapter,\n create,\n delete: deleteFn,\n onThreadIdChange,\n ...options\n}: UseAdkRuntimeOptions) => {\n const aui = useAui();\n const cloudAdapter = useCloudThreadListAdapter({\n sdk: ADK_SDK,\n cloud,\n create: createCloudThreadListAdapterCreateFallback(\n create,\n aui.threadListItem,\n ),\n delete: deleteFn,\n });\n\n const adapter = sessionAdapter ?? cloudAdapter;\n\n return useRemoteThreadListRuntime({\n runtimeHook: function RuntimeHook() {\n return useAdkRuntimeImpl(options);\n },\n adapter,\n allowNesting: true,\n onThreadIdChange,\n });\n};\n"],"mappings":";;;;;;;;;;;;;AAkHA,MAAM,qBAAqB,YAAkC;CAC3D,MAAM,EACJ,4BACA,UAAU,EAAE,aAAa,WAAW,UAAU,QAAQ,UAAU,CAAC,GACjE,4BACA,QACA,MACA,iBACA,kBACE;CACJ,MAAM,MAAM,OAAO;CACnB,MAAM,EACJ,UACA,YACA,WACA,oBACA,eACA,mBACA,cACA,WACA,iBACA,aACA,QACA,aACA,iBACA,kBACE,eAAe;EACjB;EACA,GAAI,iBAAiB,EAAE,cAAc;CACvC,CAAC;CAED,MAAM,UAAU,OAAO,IAAI;CAC3B,yBAAyB;EACvB,QAAQ,UAAU;CACpB,GAAG,CAAC,IAAI,CAAC;CACT,MAAM,CAAC,kBAAkB,SAAS,yBAAyB;CAC3D,MAAM,cAAc,OAAO,QAAQ;CACnC,yBAAyB;EACvB,YAAY,UAAU;CACxB,GAAG,CAAC,QAAQ,CAAC;CACb,MAAM,CAAC,iBAAiB,sBAAsB,eAE1C,SAAS,KAAA,KAAa,IAAI,eAAe,SAAS,CAAC,CAAC,cAAc,IACtE;CAEA,MAAM,CAAC,WAAW,gBAAgB,SAAS,KAAK;CAChD,MAAM,CAAC,cAAc,mBAAmB,SAEtC,CAAC,CAAC;CACJ,MAAM,oBAAoB,OAAO,OAAO,YAAY,CAAC,CAAC,MACnD,MAAM,GAAG,SAAS,WACrB;CACA,MAAM,qBAAqB,aAAa;CACxC,MAAM,eAAe,OAAO,kBAAkB;CAC9C,yBAAyB;EACvB,aAAa,UAAU;CACzB,GAAG,CAAC,kBAAkB,CAAC;CACvB,MAAM,mBAAmB,OAAO,CAAC;CAEjC,MAAM,oBAAoB,OACxB,MACA,WACG;EACH,MAAM,aAAa,EAAE,iBAAiB;EACtC,IAAI;GACF,aAAa,IAAI;GACjB,MAAM,YAAY,MAAM,MAAM;EAChC,UAAU;GACR,IAAI,iBAAiB,YAAY,YAAY,aAAa,KAAK;EACjE;CACF;CAEA,MAAM,EAAE,WAAW,eAAe,KAAK,qBACrC,wBAAwB,QAAQ;CAKlC,MAAM,mBAAmB,OAAO,aAAa;CAC7C,iBAAiB,UAAU;CAE3B,MAAM,wBAAwB,OAAO,kBAAkB;CACvD,yBAAyB;EACvB,sBAAsB,UAAU;CAClC,GAAG,CAAC,kBAAkB,CAAC;CAIvB,MAAM,mBAAmB,aACtB,aACC,sBAAsB,QAAQ,SAAS,SAAS,UAAU,GAC5D,CAAC,CACH;CAEA,MAAM,mBAAmB,cAErB,qBAAqB,KACjB,oBACA,0BAA0B,iBAAiB,OAAO,GACxD,CAAC,gBAAgB,CACnB;CAEA,MAAM,iBAAiB,4BAA4B;EACjD,UAAU;EACV;EACA,WAAW;CACb,CAAC;CAED,MAAM,oBAAoB,OAAO,cAAc;CAC/C,yBAAyB;EACvB,kBAAkB,UAAU;CAC9B,GAAG,CAAC,cAAc,CAAC;CAKnB,MAAM,iBAAiB,OAAO,QAAQ;CACtC,yBAAyB;EACvB,eAAe,UAAU;CAC3B,GAAG,CAAC,QAAQ,CAAC;CAEb,MAAM,oBAAoB,uBACxB,IAAI,IAMF,CACJ;CACA,MAAM,CAAC,oBAAoB,yBAAyB,SAAS,CAAC;CAC9D,MAAM,oBAAoB,qBAAqB;CAE/C,MAAM,gBAAgB,aAA4B;EAChD,IAAI,CAAC,YAAY,CAAC,kBAAkB,QAAQ,IAAI,QAAQ,GAAG,OAAO;EAElE,MAAM,SAAuB,CAAC;EAC9B,KAAK,MAAM,WAAW,eAAe,SAAS;GAC5C,IAAI,QAAQ,MAAM,kBAAkB,QAAQ,IAAI,QAAQ,EAAE,GACxD,OAAO,KAAK,kBAAkB,QAAQ,IAAI,QAAQ,EAAE,CAAC,CAAE,OAAO;GAEhE,IAAI,QAAQ,OAAO,UAAU;EAC/B;EAEA,OAAO;GACL,UAAU;GACV,WAAW,kBAAkB,QAAQ,IAAI,QAAQ,CAAC,CAAE;EACtD;CACF;CAEA,MAAM,oBAAoB,QAAuB;EAC/C,MAAM,gBAAgB,iBAAiB,GAAG;EAC1C,kBAAkB,QAAQ,IAAI,cAAc,IAAI;GAC9C,SAAS;GACT,WAAW,IAAI;EACjB,CAAC;EACD,sBAAsB,kBAAkB,QAAQ,IAAI;EACpD,MAAM,eAAe,CAAC,GAAG,eAAe,SAAS,aAAa;EAC9D,eAAe,UAAU;EACzB,YAAY,YAAY;CAC1B;CAIA,MAAM,iBACJ,IAAI,eAAe,WAAW,OAAO,IAAI,iBAAiB,KAAA;CAE5D,MAAM,UAAU,aACb,UAAgC,cAAc;EAC7C,MAAM,SAAS,QAAQ;EACvB,IAAI,CAAC,UAAU,CAAC,gBAAgB,OAAO,QAAQ,QAAQ;EAEvD,MAAM,aAAa,eAAe,SAAS,CAAC,CAAC;EAC7C,IAAI,cAAc,MAAM,OAAO,QAAQ,QAAQ;EAO/C,OAAO,eAAe,IAAI;GACxB;GACA,MAAM,OAAO,WAAW;IACtB,MAAM,sBAAsB,YAAY;IACxC,IAAI,YAAY,WAAW,mBAAmB,IAAI;IAElD,MAAM,WAAW,MAAM,OAAO,YAAY,EAAE,OAAO,CAAC;IACpD,IAAI,OAAO,SAAS;IAOpB,IACE,YAAY,aACX,aAAa,WACZ,YAAY,YAAY,sBAE1B;IACF,cAAc,QAAQ;GACxB;GACA,iBAAiB;IACf,mBAAmB,KAAK;GAC1B;GACA,iBAAiB,UAAU;IACzB,QAAQ,KAAK,+BAA+B,KAAK;GACnD;EACF,CAAC;CACH,GACA;EAAC;EAAgB;EAAgB;CAAa,CAChD;CAEA,0BAA0B;EACxB,QAAQ;EACR,aAAa;GAGX,eAAe,MAAM;GACrB,mBAAmB,KAAK;EAC1B;CACF,GAAG,CAAC,cAAc,CAAC;CA+JnB,OA7JgB,wBAAwB;EACtC,GAAG,+BAA+B,OAAO;EACzC;EACA,WAAW;EACX,UAAU;EACV,gCAAgC;EAChC,2BAA2B;EAC3B;EACA,UAAU;GAAE;GAAa;GAAW;GAAU;GAAQ;EAAM;EAC5D,QAAQ,UAAU,QAAQ;GACxB;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA,MAAM;EACR,CAAC;EACD,OAAO,OAAO,QAAQ;GACpB,IAAI,EAAE,IAAI,YAAY,IAAI,SAAS,SAAS;IAC1C,iBAAiB,GAAG;IACpB;GACF;GAEA,MAAM,gBACJ,+BAA+B,QAC3B,wBAAwB,UAAU,kBAAkB,IACpD,CAAC;GAEP,OAAO,kBACL,CACE,GAAG,eACH;IACE,IAAI,WAAW;IACf,MAAM;IACN,SAAS,kBAAkB,GAAG;GAChC,CACF,GACA,EAAE,WAAW,IAAI,UAAU,CAC7B;EACF;EACA,QAAQ,kBACJ,OAAO,QAAQ;GACb,MAAM,YAAY,oBAChB,kBAAkB,SAClB,IAAI,QACN;GACA,gBAAgB,SAAS;GACzB,IAAI,EAAE,IAAI,YAAY,IAAI,SAAS,SAAS;IAC1C,MAAM,gBAAgB,iBAAiB,GAAG;IAC1C,kBAAkB,QAAQ,IAAI,cAAc,IAAI;KAC9C,SAAS;KACT,WAAW,IAAI;IACjB,CAAC;IACD,sBAAsB,kBAAkB,QAAQ,IAAI;IACpD,MAAM,eAAe,CAAC,GAAG,WAAW,aAAa;IACjD,eAAe,UAAU;IACzB,YAAY,YAAY;IACxB;GACF;GACA,MAAM,aAAa,IAAI,eAAe,SAAS,CAAC,CAAC;GACjD,MAAM,eAAe,aACjB,MAAM,gBAAgB,YAAY,SAAS,IAC3C;GACJ,OAAO,kBACL,CACE;IACE,IAAI,WAAW;IACf,MAAM;IACN,SAAS,kBAAkB,GAAG;GAChC,CACF,GACA;IACE,WAAW,IAAI;IACf,GAAI,gBAAgB,EAAE,aAAa;GACrC,CACF;EACF,IACA,KAAA;EACJ,GAAI,mBAAmB,oBACnB,EACE,UAAU,OAAO,UAAU,WAAW;GACpC,MAAM,YAAY,aAAa,QAAQ;GACvC,IAAI,WAAW;IACb,KAAK,MAAM,WAAW,UAAU,UAC9B,kBAAkB,QAAQ,OAAO,QAAQ,EAAE;IAE7C,sBAAsB,kBAAkB,QAAQ,IAAI;IACpD,OAAO,kBAAkB,UAAU,UAAU,EAC3C,WAAW,OAAO,aAAa,UAAU,UAC3C,CAAC;GACH;GAEA,IAAI,CAAC,iBACH,MAAM,IAAI,MAAM,8CAA8C;GAEhE,MAAM,YAAY,oBAChB,kBAAkB,SAClB,QACF;GACA,gBAAgB,SAAS;GACzB,MAAM,aAAa,IAAI,eAAe,SAAS,CAAC,CAAC;GACjD,MAAM,eAAe,aACjB,MAAM,gBAAgB,YAAY,SAAS,IAC3C;GACJ,OAAO,kBAAkB,CAAC,GAAG;IAC3B,WAAW,OAAO;IAClB,GAAI,gBAAgB,EAAE,aAAa;GACrC,CAAC;EACH,EACF,IACA,CAAC;EACL,iBAAiB,OAAO,EACtB,YACA,UACA,QACA,SACA,eACI;GACJ,MAAM,kBACJ,CACE;IACE,IAAI,WAAW;IACf,MAAM;IACN,MAAM;IACN,cAAc;IACd,SAAS,KAAK,UAAU,MAAM;IAC9B;IACA,QAAQ,UAAU,UAAU;GAC9B,CACF,GACA,CAAC,CACH;EACF;EACA,yBAAyB,OAAO,YAAY;GAC1C,MAAM,kBACJ,CACE,2BACE,SACA,wBAAwB,eAAe,OAAO,CAAC,CAAC,SAClD,CACF,GACA,CAAC,CACH;EACF;EACA,UAAU,6BACN,YAAY;GACV,OAAO;EACT,IACA,KAAA;EACJ,GAAI,SAAS,KAAA,KAAa,EACxB,uBAAuB,QAAQ,QAAQ,EACzC;CACF,CAEa;AACf;AAEA,MAAa,iBAAiB,EAC5B,OACA,gBACA,QACA,QAAQ,UACR,kBACA,GAAG,cACuB;CAC1B,MAAM,MAAM,OAAO;CACnB,MAAM,eAAe,0BAA0B;EAC7C,KAAK;EACL;EACA,QAAQ,2CACN,QACA,IAAI,cACN;EACA,QAAQ;CACV,CAAC;CAID,OAAO,2BAA2B;EAChC,aAAa,SAAS,cAAc;GAClC,OAAO,kBAAkB,OAAO;EAClC;EACA,SANc,kBAAkB;EAOhC,cAAc;EACd;CACF,CAAC;AACH"}
1
+ {"version":3,"file":"useAdkRuntime.js","names":[],"sources":["../src/useAdkRuntime.ts"],"sourcesContent":["import {\n useCallback,\n useInsertionEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport {\n pickExternalStoreSharedOptions,\n type AttachmentAdapter,\n type DictationAdapter,\n type ExternalStoreSharedOptions,\n type FeedbackAdapter,\n type RealtimeVoiceAdapter,\n type SpeechSynthesisAdapter,\n type AppendMessage,\n type ToolCallMessagePart,\n type ToolExecutionStatus,\n generateId,\n} from \"@assistant-ui/core\";\nimport {\n createAbortableThreadLoad,\n createCloudThreadListAdapterCreateFallback,\n isRecord,\n} from \"@assistant-ui/core/internal\";\nimport {\n useCloudThreadListAdapter,\n useRemoteThreadListRuntime,\n useExternalMessageConverter,\n useExternalStoreRuntime,\n} from \"@assistant-ui/core/react\";\nimport { useAui } from \"@assistant-ui/store\";\nimport { useReplaySafeEffect } from \"@assistant-ui/store/internal\";\nimport type { AssistantCloud } from \"assistant-cloud\";\nimport type { RemoteThreadListAdapter } from \"@assistant-ui/core\";\nimport type {\n AdkMessage,\n AdkThreadSnapshot,\n AdkSendMessageConfig,\n AdkStreamCallback,\n OnAdkErrorCallback,\n OnAdkCustomEventCallback,\n OnAdkAgentTransferCallback,\n} from \"./types\";\nimport { useAdkMessagesInternal } from \"./useAdkMessages\";\nimport {\n convertAdkMessage,\n createAdkMessageConverter,\n} from \"./convertAdkMessages\";\nimport {\n getMessageContent,\n getPendingCancellations,\n toAdkUserMessage,\n truncateAdkMessages,\n} from \"./convertToAdkMessages\";\nimport {\n projectAdkToolApprovals,\n toAdkToolConfirmationReply,\n} from \"./adkToolApproval\";\nimport { adkExtras } from \"./adkExtras\";\nimport { ADK_SDK } from \"./sdkIdentity\";\n\nexport type UseAdkRuntimeOptions = ExternalStoreSharedOptions & {\n stream: AdkStreamCallback;\n /**\n * Called whenever the active thread's canonical (remote) ID changes, so the\n * value can be treated as a managed/controlled variable (e.g. synced to a URL\n * query param). Only the settled remote ID is emitted: while a freshly created\n * thread is still optimistic the value is `undefined`, and the real ID is\n * emitted once the thread is initialized; the transient local ID is never\n * surfaced.\n */\n onThreadIdChange?: ((threadId: string | undefined) => void) | undefined;\n autoCancelPendingToolCalls?: boolean | undefined;\n unstable_allowCancellation?: boolean | undefined;\n getCheckpointId?: (\n threadId: string,\n parentMessages: AdkMessage[],\n ) => Promise<string | null>;\n /**\n * Loads a thread's stored state. Called when the thread opens, and again for\n * `threads.reloadMainThread()`, which refetches in place rather than\n * remounting the runtime; the signal aborts a load the runtime no longer\n * needs.\n */\n load?: (\n threadId: string,\n options?: { signal?: AbortSignal | undefined },\n ) => Promise<AdkThreadSnapshot>;\n create?: () => Promise<{ externalId: string }>;\n delete?: (threadId: string) => Promise<void>;\n adapters?:\n | {\n attachments?: AttachmentAdapter;\n speech?: SpeechSynthesisAdapter;\n dictation?: DictationAdapter;\n voice?: RealtimeVoiceAdapter;\n feedback?: FeedbackAdapter;\n }\n | undefined;\n eventHandlers?:\n | {\n onError?: OnAdkErrorCallback;\n onCustomEvent?: OnAdkCustomEventCallback;\n onAgentTransfer?: OnAdkAgentTransferCallback;\n }\n | undefined;\n cloud?: AssistantCloud | undefined;\n /**\n * A `RemoteThreadListAdapter` to use instead of the cloud adapter.\n * Use with `createAdkSessionAdapter` for ADK session-backed persistence.\n */\n sessionAdapter?: RemoteThreadListAdapter | undefined;\n};\n\nconst useAdkRuntimeImpl = (options: UseAdkRuntimeOptions) => {\n const {\n autoCancelPendingToolCalls,\n adapters: { attachments, dictation, feedback, speech, voice } = {},\n unstable_allowCancellation,\n stream,\n load,\n getCheckpointId,\n eventHandlers,\n } = options;\n const aui = useAui();\n const runConfigByToolCallIdRef = useRef(new Map<string, unknown>());\n\n const rememberMessageOwnership = useCallback(\n (newMessages: AdkMessage[], runConfig: unknown) => {\n const toolOwnership = runConfigByToolCallIdRef.current;\n for (const message of newMessages) {\n if (message.type !== \"ai\") continue;\n for (const toolCall of message.tool_calls ?? []) {\n if (!isRecord(toolCall)) continue;\n if (!toolOwnership.has(toolCall.id)) {\n toolOwnership.set(toolCall.id, runConfig);\n }\n }\n }\n },\n [],\n );\n\n const seedMessageOwnership = useCallback((history: AdkMessage[]) => {\n const currentOwnership = runConfigByToolCallIdRef.current;\n const nextOwnership = new Map<string, unknown>();\n for (const message of history) {\n if (message.type !== \"ai\") continue;\n for (const toolCall of message.tool_calls ?? []) {\n if (!isRecord(toolCall)) continue;\n // Loaded ids must remain present even without a local owner because\n // streamed event windows use has() to avoid attributing them later.\n nextOwnership.set(\n toolCall.id,\n currentOwnership.has(toolCall.id)\n ? currentOwnership.get(toolCall.id)\n : undefined,\n );\n }\n }\n runConfigByToolCallIdRef.current = nextOwnership;\n }, []);\n\n const pruneMessageOwnership = useCallback((history: AdkMessage[]) => {\n const toolCallIds = new Set<string>();\n for (const message of history) {\n if (message.type !== \"ai\") continue;\n for (const toolCall of message.tool_calls ?? []) {\n if (!isRecord(toolCall)) continue;\n toolCallIds.add(toolCall.id);\n }\n }\n for (const id of runConfigByToolCallIdRef.current.keys()) {\n if (!toolCallIds.has(id)) runConfigByToolCallIdRef.current.delete(id);\n }\n }, []);\n\n const getToolRunConfig = useCallback((toolCallId: string) => {\n return runConfigByToolCallIdRef.current.get(toolCallId);\n }, []);\n\n const {\n messages,\n stateDelta,\n agentInfo,\n longRunningToolIds,\n artifactDelta,\n toolConfirmations,\n authRequests,\n escalated,\n messageMetadata,\n sendMessage,\n cancel,\n setMessages,\n replaceMessages: replaceAdkMessages,\n applySnapshot: applyAdkSnapshot,\n } = useAdkMessagesInternal({\n stream,\n ...(eventHandlers && { eventHandlers }),\n onMessages: rememberMessageOwnership,\n });\n\n const loadRef = useRef(load);\n useInsertionEffect(() => {\n loadRef.current = load;\n }, [load]);\n const [loadController] = useState(createAbortableThreadLoad);\n const messagesRef = useRef(messages);\n useInsertionEffect(() => {\n messagesRef.current = messages;\n }, [messages]);\n const applySnapshot = useCallback(\n (snapshot: AdkThreadSnapshot) => {\n seedMessageOwnership(snapshot.messages);\n applyAdkSnapshot(snapshot);\n },\n [applyAdkSnapshot, seedMessageOwnership],\n );\n const replaceMessages = useCallback(\n (nextMessages: AdkMessage[]) => {\n pruneMessageOwnership(nextMessages);\n replaceAdkMessages(nextMessages);\n },\n [pruneMessageOwnership, replaceAdkMessages],\n );\n const [isLoadingThread, setIsLoadingThread] = useState(\n () =>\n load !== undefined && aui.threadListItem.getState().externalId != null,\n );\n\n const [isRunning, setIsRunning] = useState(false);\n const [toolStatuses, setToolStatuses] = useState<\n Record<string, ToolExecutionStatus>\n >({});\n const hasExecutingTools = Object.values(toolStatuses).some(\n (s) => s?.type === \"executing\",\n );\n const effectiveIsRunning = isRunning || hasExecutingTools;\n const isRunningRef = useRef(effectiveIsRunning);\n useInsertionEffect(() => {\n isRunningRef.current = effectiveIsRunning;\n }, [effectiveIsRunning]);\n const runGenerationRef = useRef(0);\n\n const handleSendMessage = async (\n msgs: AdkMessage[],\n config: AdkSendMessageConfig,\n ) => {\n const isToolContinuation =\n msgs.length > 0 && msgs.every((msg) => msg.type === \"tool\");\n const continuationConfig =\n isToolContinuation && config.runConfig === undefined\n ? {\n ...config,\n runConfig: getToolRunConfig(msgs[0]!.tool_call_id),\n }\n : config;\n\n const generation = ++runGenerationRef.current;\n try {\n setIsRunning(true);\n await sendMessage(msgs, continuationConfig);\n } finally {\n if (runGenerationRef.current === generation) setIsRunning(false);\n }\n };\n\n const { approvals: toolApprovals, key: toolApprovalsKey } =\n projectAdkToolApprovals(messages);\n // The messageConverter memo below reads this during render, where the ref\n // must carry the same render's approvals; a commit-scoped write would feed\n // the memo the previous commit's approvals whenever the key changes. No\n // callback reads it — approval replies project from the committed messages.\n const toolApprovalsRef = useRef(toolApprovals);\n toolApprovalsRef.current = toolApprovals;\n\n const longRunningToolIdsRef = useRef(longRunningToolIds);\n useInsertionEffect(() => {\n longRunningToolIdsRef.current = longRunningToolIds;\n }, [longRunningToolIds]);\n // ADK resolves every call it did not mark long-running itself, and yields\n // that call to the client one or more events before its own response, so\n // only a long-running call is the client's to execute.\n const isClientToolCall = useCallback(\n (toolCall: ToolCallMessagePart) =>\n longRunningToolIdsRef.current.includes(toolCall.toolCallId),\n [],\n );\n\n const messageConverter = useMemo(\n () =>\n toolApprovalsKey === \"\"\n ? convertAdkMessage\n : createAdkMessageConverter(toolApprovalsRef.current),\n [toolApprovalsKey],\n );\n\n const threadMessages = useExternalMessageConverter({\n callback: messageConverter,\n messages,\n isRunning: effectiveIsRunning,\n });\n\n const threadMessagesRef = useRef(threadMessages);\n useInsertionEffect(() => {\n threadMessagesRef.current = threadMessages;\n }, [threadMessages]);\n\n // Staging assigns adkMessagesRef.current directly, so the effect must key on\n // the committed messages alone; a dep-less publication would clobber the\n // optimistic value on any unrelated commit.\n const adkMessagesRef = useRef(messages);\n useInsertionEffect(() => {\n adkMessagesRef.current = messages;\n }, [messages]);\n\n const stagedMessagesRef = useRef(\n new Map<\n string,\n {\n message: AdkMessage & { id: string };\n runConfig: AppendMessage[\"runConfig\"];\n }\n >(),\n );\n const [stagedMessageCount, setStagedMessageCount] = useState(0);\n const hasStagedMessages = stagedMessageCount > 0;\n\n const getStagedRun = (parentId: string | null) => {\n if (!parentId || !stagedMessagesRef.current.has(parentId)) return null;\n\n const staged: AdkMessage[] = [];\n for (const message of adkMessagesRef.current) {\n if (message.id && stagedMessagesRef.current.has(message.id)) {\n staged.push(stagedMessagesRef.current.get(message.id)!.message);\n }\n if (message.id === parentId) break;\n }\n\n return {\n messages: staged,\n runConfig: stagedMessagesRef.current.get(parentId)!.runConfig,\n };\n };\n\n const stageUserMessage = (msg: AppendMessage) => {\n const stagedMessage = toAdkUserMessage(msg);\n stagedMessagesRef.current.set(stagedMessage.id, {\n message: stagedMessage,\n runConfig: msg.runConfig,\n });\n setStagedMessageCount(stagedMessagesRef.current.size);\n const nextMessages = [...adkMessagesRef.current, stagedMessage];\n adkMessagesRef.current = nextMessages;\n setMessages(nextMessages);\n };\n\n // The scoped client, not `aui` itself: useAui returns a render-bound\n // instance, so depending on it would re-run the load on every render.\n const threadListItem =\n aui.threadListItem.source !== null ? aui.threadListItem : undefined;\n\n const runLoad = useCallback(\n (purpose: \"initial\" | \"reload\" = \"initial\") => {\n const loadFn = loadRef.current;\n if (!loadFn || !threadListItem) return Promise.resolve();\n\n const externalId = threadListItem.getState().externalId;\n if (externalId == null) return Promise.resolve();\n\n // The initial load is already fetching what a refetch would ask for, and\n // taking it over strands the thread's history if the refetch then fails.\n // Aborting a load the runtime no longer needs is not a failure.\n // A refetch reports the failure to whoever awaited it; the initial load\n // has no caller to tell.\n return loadController.run({\n purpose,\n load: async (signal) => {\n const messagesAtLoadStart = messagesRef.current;\n if (purpose === \"initial\") setIsLoadingThread(true);\n\n const snapshot = await loadFn(externalId, { signal });\n if (signal.aborted) return;\n // A snapshot the session assembled before a run cannot speak for what\n // that run has since produced, and an ADK id cannot correlate a\n // message sent optimistically with the one the session stored for it,\n // so there is nothing here that could merge the two. A refetch that\n // raced a run therefore defers to the run, whether the run started\n // during the load or was already streaming when it began.\n if (\n purpose === \"reload\" &&\n (isRunningRef.current ||\n messagesRef.current !== messagesAtLoadStart)\n )\n return;\n applySnapshot(snapshot);\n },\n onSettled: () => {\n setIsLoadingThread(false);\n },\n onInitialError: (error) => {\n console.warn(\"Failed to load ADK session:\", error);\n },\n });\n },\n [threadListItem, loadController, applySnapshot],\n );\n\n useReplaySafeEffect(() => {\n runLoad();\n return () => {\n // Whatever is current, not this effect's own controller: a refetch swaps\n // the ref, and one in flight at unmount must be aborted too.\n loadController.abort();\n setIsLoadingThread(false);\n };\n }, [threadListItem]);\n\n const runtime = useExternalStoreRuntime({\n ...pickExternalStoreSharedOptions(options),\n isRunning,\n isLoading: isLoadingThread,\n messages: threadMessages,\n unstable_enableToolInvocations: true,\n unstable_isClientToolCall: isClientToolCall,\n setToolStatuses,\n adapters: { attachments, dictation, feedback, speech, voice },\n extras: adkExtras.provide({\n agentInfo,\n stateDelta,\n artifactDelta,\n longRunningToolIds,\n toolConfirmations,\n authRequests,\n escalated,\n messageMetadata,\n send: handleSendMessage,\n }),\n onNew: async (msg) => {\n if (!(msg.startRun ?? msg.role === \"user\")) {\n stageUserMessage(msg);\n return;\n }\n\n const cancellations =\n autoCancelPendingToolCalls !== false\n ? getPendingCancellations(messages, longRunningToolIds)\n : [];\n\n return handleSendMessage(\n [\n ...cancellations,\n {\n id: generateId(),\n type: \"human\",\n content: getMessageContent(msg),\n },\n ],\n { runConfig: msg.runConfig },\n );\n },\n onEdit: getCheckpointId\n ? async (msg) => {\n const truncated = truncateAdkMessages(\n threadMessagesRef.current,\n msg.parentId,\n );\n replaceMessages(truncated);\n if (!(msg.startRun ?? msg.role === \"user\")) {\n const stagedMessage = toAdkUserMessage(msg);\n stagedMessagesRef.current.set(stagedMessage.id, {\n message: stagedMessage,\n runConfig: msg.runConfig,\n });\n setStagedMessageCount(stagedMessagesRef.current.size);\n const nextMessages = [...truncated, stagedMessage];\n adkMessagesRef.current = nextMessages;\n setMessages(nextMessages);\n return;\n }\n const externalId = aui.threadListItem.getState().externalId;\n const checkpointId = externalId\n ? await getCheckpointId(externalId, truncated)\n : null;\n return handleSendMessage(\n [\n {\n id: generateId(),\n type: \"human\",\n content: getMessageContent(msg),\n },\n ],\n {\n runConfig: msg.runConfig,\n ...(checkpointId && { checkpointId }),\n },\n );\n }\n : undefined,\n ...(getCheckpointId || hasStagedMessages\n ? {\n onReload: async (parentId, config) => {\n const stagedRun = getStagedRun(parentId);\n if (stagedRun) {\n for (const message of stagedRun.messages) {\n stagedMessagesRef.current.delete(message.id);\n }\n setStagedMessageCount(stagedMessagesRef.current.size);\n return handleSendMessage(stagedRun.messages, {\n runConfig: config.runConfig ?? stagedRun.runConfig,\n });\n }\n\n if (!getCheckpointId)\n throw new Error(\"Runtime does not support reloading messages.\");\n\n const truncated = truncateAdkMessages(\n threadMessagesRef.current,\n parentId,\n );\n replaceMessages(truncated);\n const externalId = aui.threadListItem.getState().externalId;\n const checkpointId = externalId\n ? await getCheckpointId(externalId, truncated)\n : null;\n return handleSendMessage([], {\n runConfig: config.runConfig,\n ...(checkpointId && { checkpointId }),\n });\n },\n }\n : {}),\n onAddToolResult: async ({\n toolCallId,\n toolName,\n result,\n isError,\n artifact,\n }) => {\n await handleSendMessage(\n [\n {\n id: generateId(),\n type: \"tool\",\n name: toolName,\n tool_call_id: toolCallId,\n content: JSON.stringify(result),\n artifact,\n status: isError ? \"error\" : \"success\",\n },\n ],\n {},\n );\n },\n onRespondToToolApproval: async (options) => {\n await handleSendMessage(\n [\n toAdkToolConfirmationReply(\n options,\n projectAdkToolApprovals(adkMessagesRef.current).approvals,\n ),\n ],\n {},\n );\n },\n onCancel: unstable_allowCancellation\n ? async () => {\n cancel();\n }\n : undefined,\n ...(load !== undefined && {\n onRefetchThread: () => runLoad(\"reload\"),\n }),\n });\n\n return runtime;\n};\n\nexport const useAdkRuntime = ({\n cloud,\n sessionAdapter,\n create,\n delete: deleteFn,\n onThreadIdChange,\n ...options\n}: UseAdkRuntimeOptions) => {\n const aui = useAui();\n const cloudAdapter = useCloudThreadListAdapter({\n sdk: ADK_SDK,\n cloud,\n create: createCloudThreadListAdapterCreateFallback(\n create,\n aui.threadListItem,\n ),\n delete: deleteFn,\n });\n\n const adapter = sessionAdapter ?? cloudAdapter;\n\n return useRemoteThreadListRuntime({\n runtimeHook: function RuntimeHook() {\n return useAdkRuntimeImpl(options);\n },\n adapter,\n allowNesting: true,\n onThreadIdChange,\n });\n};\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@assistant-ui/react-google-adk",
3
- "version": "0.0.34",
3
+ "version": "0.0.35",
4
4
  "description": "Google ADK adapter for assistant-ui",
5
5
  "keywords": [
6
6
  "google-adk",
@@ -34,10 +34,10 @@
34
34
  ],
35
35
  "sideEffects": false,
36
36
  "dependencies": {
37
- "@assistant-ui/core": "^0.3.23",
37
+ "@assistant-ui/core": "^0.3.24",
38
38
  "@assistant-ui/store": "^0.3.17",
39
39
  "assistant-cloud": "*",
40
- "assistant-stream": "^0.3.47"
40
+ "assistant-stream": "^0.3.48"
41
41
  },
42
42
  "peerDependencies": {
43
43
  "@google/adk": ">=0.5.0",
@@ -78,6 +78,7 @@
78
78
  "scripts": {
79
79
  "build": "aui-build",
80
80
  "test": "vitest run",
81
+ "test:coverage": "vitest run --coverage --coverage.reporter=text --coverage.reporter=html --coverage.reporter=json-summary",
81
82
  "typecheck": "tsc --noEmit",
82
83
  "test:peer-react18": "vitest run --config vitest.peer-react18.config.ts",
83
84
  "test:types:peer-react18": "aui-react18-types",
@@ -17,6 +17,7 @@ import {
17
17
  messageToEvent,
18
18
  messagesToEvents,
19
19
  useAdkMessages,
20
+ useAdkMessagesInternal,
20
21
  } from "./useAdkMessages";
21
22
  import { projectAdkToolApprovals } from "./adkToolApproval";
22
23
  import { createAdkStream } from "./AdkClient";
@@ -212,6 +213,47 @@ describe("ADK runtime callbacks", () => {
212
213
  });
213
214
 
214
215
  describe("ADK stream lifecycle", () => {
216
+ it("reports streamed tool calls with the run config that produced them", async () => {
217
+ const runConfig = { custom: { model: "model-a" } };
218
+ const onMessages = vi.fn();
219
+ const stream: AdkStreamCallback = async function* () {
220
+ yield {
221
+ id: "event-1",
222
+ author: "agent",
223
+ content: {
224
+ role: "model",
225
+ parts: [
226
+ { functionCall: { id: "tool-1", name: "lookup", args: {} } },
227
+ { functionCall: { id: "tool-2", name: "search", args: {} } },
228
+ ],
229
+ },
230
+ };
231
+ };
232
+ const { result } = renderHook(() =>
233
+ useAdkMessagesInternal({ stream, onMessages }),
234
+ );
235
+
236
+ await act(async () => {
237
+ await result.current.sendMessage(
238
+ [{ id: "user-1", type: "human", content: "look it up" }],
239
+ { runConfig },
240
+ );
241
+ });
242
+
243
+ expect(onMessages).toHaveBeenLastCalledWith(
244
+ expect.arrayContaining([
245
+ expect.objectContaining({
246
+ type: "ai",
247
+ tool_calls: [
248
+ expect.objectContaining({ id: "tool-1" }),
249
+ expect.objectContaining({ id: "tool-2" }),
250
+ ],
251
+ }),
252
+ ]),
253
+ runConfig,
254
+ );
255
+ });
256
+
215
257
  it("settles a superseded send while its stream is still opening", async () => {
216
258
  const signals: AbortSignal[] = [];
217
259
  const parked = new Promise<AsyncGenerator<AdkEvent>>(() => {});
@@ -40,6 +40,10 @@ export type UseAdkMessagesOptions = {
40
40
  };
41
41
  };
42
42
 
43
+ type UseAdkMessagesInternalOptions = UseAdkMessagesOptions & {
44
+ onMessages?: (messages: AdkMessage[], runConfig: unknown) => void;
45
+ };
46
+
43
47
  type AdkRuntimeCallbackName = "onError" | "onCustomEvent" | "onAgentTransfer";
44
48
 
45
49
  const invokeAdkRuntimeCallback = <TArgs extends readonly unknown[]>(
@@ -50,10 +54,11 @@ const invokeAdkRuntimeCallback = <TArgs extends readonly unknown[]>(
50
54
  void invokeUserCallback("react-google-adk", name, callback, ...args);
51
55
  };
52
56
 
53
- export const useAdkMessages = ({
57
+ const useAdkMessagesInternal = ({
54
58
  stream,
55
59
  eventHandlers,
56
- }: UseAdkMessagesOptions) => {
60
+ onMessages,
61
+ }: UseAdkMessagesInternalOptions) => {
57
62
  const [messages, _setMessages] = useState<AdkMessage[]>([]);
58
63
  const [stateDelta, setStateDelta] = useState<Record<string, unknown>>({});
59
64
  const [agentInfo, setAgentInfo] = useState<{
@@ -206,6 +211,17 @@ export const useAdkMessages = ({
206
211
  break;
207
212
  }
208
213
  const updatedMessages = accumulator.processEvent(event);
214
+ // Each event part can append at most one message, and a function call
215
+ // stays on the current assistant message until a later part finalizes
216
+ // it, so every message touched by this event is within this tail.
217
+ const affectedMessageCount = Math.max(
218
+ event.content?.parts?.length ?? 0,
219
+ 1,
220
+ );
221
+ const affectedMessages = updatedMessages.slice(-affectedMessageCount);
222
+ if (affectedMessages.length > 0) {
223
+ onMessages?.(affectedMessages, config.runConfig);
224
+ }
209
225
  setMessagesImmediate(updatedMessages);
210
226
  setStateDelta({
211
227
  ...stateDeltaRef.current,
@@ -308,6 +324,7 @@ export const useAdkMessages = ({
308
324
  onError,
309
325
  onCustomEvent,
310
326
  onAgentTransfer,
327
+ onMessages,
311
328
  ],
312
329
  );
313
330
 
@@ -337,6 +354,17 @@ export const useAdkMessages = ({
337
354
  };
338
355
  };
339
356
 
357
+ export const useAdkMessages = ({
358
+ stream,
359
+ eventHandlers,
360
+ }: UseAdkMessagesOptions) =>
361
+ useAdkMessagesInternal({
362
+ stream,
363
+ ...(eventHandlers !== undefined && { eventHandlers }),
364
+ });
365
+
366
+ export { useAdkMessagesInternal };
367
+
340
368
  /**
341
369
  * Transport sends every human and tool message of one `send` call as a single
342
370
  * ADK `Content`, and ADK parses that event's function responses before running
@@ -21,6 +21,7 @@ import {
21
21
  import {
22
22
  createAbortableThreadLoad,
23
23
  createCloudThreadListAdapterCreateFallback,
24
+ isRecord,
24
25
  } from "@assistant-ui/core/internal";
25
26
  import {
26
27
  useCloudThreadListAdapter,
@@ -41,7 +42,7 @@ import type {
41
42
  OnAdkCustomEventCallback,
42
43
  OnAdkAgentTransferCallback,
43
44
  } from "./types";
44
- import { useAdkMessages } from "./useAdkMessages";
45
+ import { useAdkMessagesInternal } from "./useAdkMessages";
45
46
  import {
46
47
  convertAdkMessage,
47
48
  createAdkMessageConverter,
@@ -123,6 +124,62 @@ const useAdkRuntimeImpl = (options: UseAdkRuntimeOptions) => {
123
124
  eventHandlers,
124
125
  } = options;
125
126
  const aui = useAui();
127
+ const runConfigByToolCallIdRef = useRef(new Map<string, unknown>());
128
+
129
+ const rememberMessageOwnership = useCallback(
130
+ (newMessages: AdkMessage[], runConfig: unknown) => {
131
+ const toolOwnership = runConfigByToolCallIdRef.current;
132
+ for (const message of newMessages) {
133
+ if (message.type !== "ai") continue;
134
+ for (const toolCall of message.tool_calls ?? []) {
135
+ if (!isRecord(toolCall)) continue;
136
+ if (!toolOwnership.has(toolCall.id)) {
137
+ toolOwnership.set(toolCall.id, runConfig);
138
+ }
139
+ }
140
+ }
141
+ },
142
+ [],
143
+ );
144
+
145
+ const seedMessageOwnership = useCallback((history: AdkMessage[]) => {
146
+ const currentOwnership = runConfigByToolCallIdRef.current;
147
+ const nextOwnership = new Map<string, unknown>();
148
+ for (const message of history) {
149
+ if (message.type !== "ai") continue;
150
+ for (const toolCall of message.tool_calls ?? []) {
151
+ if (!isRecord(toolCall)) continue;
152
+ // Loaded ids must remain present even without a local owner because
153
+ // streamed event windows use has() to avoid attributing them later.
154
+ nextOwnership.set(
155
+ toolCall.id,
156
+ currentOwnership.has(toolCall.id)
157
+ ? currentOwnership.get(toolCall.id)
158
+ : undefined,
159
+ );
160
+ }
161
+ }
162
+ runConfigByToolCallIdRef.current = nextOwnership;
163
+ }, []);
164
+
165
+ const pruneMessageOwnership = useCallback((history: AdkMessage[]) => {
166
+ const toolCallIds = new Set<string>();
167
+ for (const message of history) {
168
+ if (message.type !== "ai") continue;
169
+ for (const toolCall of message.tool_calls ?? []) {
170
+ if (!isRecord(toolCall)) continue;
171
+ toolCallIds.add(toolCall.id);
172
+ }
173
+ }
174
+ for (const id of runConfigByToolCallIdRef.current.keys()) {
175
+ if (!toolCallIds.has(id)) runConfigByToolCallIdRef.current.delete(id);
176
+ }
177
+ }, []);
178
+
179
+ const getToolRunConfig = useCallback((toolCallId: string) => {
180
+ return runConfigByToolCallIdRef.current.get(toolCallId);
181
+ }, []);
182
+
126
183
  const {
127
184
  messages,
128
185
  stateDelta,
@@ -136,11 +193,12 @@ const useAdkRuntimeImpl = (options: UseAdkRuntimeOptions) => {
136
193
  sendMessage,
137
194
  cancel,
138
195
  setMessages,
139
- replaceMessages,
140
- applySnapshot,
141
- } = useAdkMessages({
196
+ replaceMessages: replaceAdkMessages,
197
+ applySnapshot: applyAdkSnapshot,
198
+ } = useAdkMessagesInternal({
142
199
  stream,
143
200
  ...(eventHandlers && { eventHandlers }),
201
+ onMessages: rememberMessageOwnership,
144
202
  });
145
203
 
146
204
  const loadRef = useRef(load);
@@ -152,6 +210,20 @@ const useAdkRuntimeImpl = (options: UseAdkRuntimeOptions) => {
152
210
  useInsertionEffect(() => {
153
211
  messagesRef.current = messages;
154
212
  }, [messages]);
213
+ const applySnapshot = useCallback(
214
+ (snapshot: AdkThreadSnapshot) => {
215
+ seedMessageOwnership(snapshot.messages);
216
+ applyAdkSnapshot(snapshot);
217
+ },
218
+ [applyAdkSnapshot, seedMessageOwnership],
219
+ );
220
+ const replaceMessages = useCallback(
221
+ (nextMessages: AdkMessage[]) => {
222
+ pruneMessageOwnership(nextMessages);
223
+ replaceAdkMessages(nextMessages);
224
+ },
225
+ [pruneMessageOwnership, replaceAdkMessages],
226
+ );
155
227
  const [isLoadingThread, setIsLoadingThread] = useState(
156
228
  () =>
157
229
  load !== undefined && aui.threadListItem.getState().externalId != null,
@@ -175,10 +247,20 @@ const useAdkRuntimeImpl = (options: UseAdkRuntimeOptions) => {
175
247
  msgs: AdkMessage[],
176
248
  config: AdkSendMessageConfig,
177
249
  ) => {
250
+ const isToolContinuation =
251
+ msgs.length > 0 && msgs.every((msg) => msg.type === "tool");
252
+ const continuationConfig =
253
+ isToolContinuation && config.runConfig === undefined
254
+ ? {
255
+ ...config,
256
+ runConfig: getToolRunConfig(msgs[0]!.tool_call_id),
257
+ }
258
+ : config;
259
+
178
260
  const generation = ++runGenerationRef.current;
179
261
  try {
180
262
  setIsRunning(true);
181
- await sendMessage(msgs, config);
263
+ await sendMessage(msgs, continuationConfig);
182
264
  } finally {
183
265
  if (runGenerationRef.current === generation) setIsRunning(false);
184
266
  }
@@ -1,17 +1,33 @@
1
1
  import { act, renderHook } from "@testing-library/react";
2
2
  import { afterEach, describe, expect, it, vi } from "vitest";
3
3
  import type {
4
+ AddToolResultOptions,
5
+ AppendMessage,
4
6
  RespondToToolApprovalOptions,
5
7
  ThreadMessage,
6
8
  ToolCallMessagePart,
7
9
  } from "@assistant-ui/core";
8
- import type { AdkMessage } from "./types";
10
+ import type { AdkMessage, AdkSendMessageConfig } from "./types";
9
11
 
10
- const mocks = vi.hoisted(() => ({
11
- adapters: [] as unknown[],
12
- sendMessage: vi.fn().mockResolvedValue(undefined),
13
- messages: [] as AdkMessage[],
14
- }));
12
+ const mocks = vi.hoisted(() => {
13
+ const threadListItem = {
14
+ source: null as object | null,
15
+ externalId: undefined as string | undefined,
16
+ getState: () => ({
17
+ externalId: threadListItem.externalId,
18
+ }),
19
+ initialize: vi.fn(),
20
+ };
21
+ return {
22
+ adapters: [] as unknown[],
23
+ sendMessage: vi.fn().mockResolvedValue(undefined),
24
+ messages: [] as AdkMessage[],
25
+ messageRunConfig: undefined as unknown,
26
+ streamedMessages: undefined as AdkMessage[] | undefined,
27
+ applySnapshot: vi.fn(),
28
+ threadListItem,
29
+ };
30
+ });
15
31
 
16
32
  vi.mock("@assistant-ui/core/react", async (importOriginal) => ({
17
33
  ...(await importOriginal<typeof import("@assistant-ui/core/react")>()),
@@ -27,49 +43,88 @@ vi.mock("@assistant-ui/core/react", async (importOriginal) => ({
27
43
  vi.mock("@assistant-ui/store", async (importOriginal) => ({
28
44
  ...(await importOriginal<typeof import("@assistant-ui/store")>()),
29
45
  useAui: () => ({
30
- threadListItem: {
31
- source: null,
32
- getState: () => ({ externalId: undefined }),
33
- initialize: vi.fn(),
34
- },
46
+ threadListItem: mocks.threadListItem,
35
47
  }),
36
48
  }));
37
49
 
38
- vi.mock("./useAdkMessages", async (importOriginal) => ({
39
- ...(await importOriginal<typeof import("./useAdkMessages")>()),
40
- useAdkMessages: () => ({
41
- messages: mocks.messages,
42
- stateDelta: {},
43
- agentInfo: {},
44
- longRunningToolIds: [],
45
- artifactDelta: {},
46
- // Deliberately empty: the projection must come from the transcript, not
47
- // from derived confirmation state that a mid-run accumulator swap drops.
48
- toolConfirmations: [],
49
- authRequests: [],
50
- escalated: false,
51
- messageMetadata: new Map(),
52
- sendMessage: mocks.sendMessage,
53
- cancel: vi.fn(),
54
- setMessages: vi.fn(),
55
- replaceMessages: vi.fn(),
56
- applySnapshot: vi.fn(),
57
- }),
58
- }));
50
+ vi.mock("./useAdkMessages", async (importOriginal) => {
51
+ const useMockAdkMessages = (options: {
52
+ onMessages?: (messages: AdkMessage[], runConfig: unknown) => void;
53
+ }) => {
54
+ options.onMessages?.(mocks.messages, mocks.messageRunConfig);
55
+ const sendMessage = async (
56
+ messages: AdkMessage[],
57
+ config: AdkSendMessageConfig,
58
+ ) => {
59
+ await mocks.sendMessage(messages, config);
60
+ if (mocks.streamedMessages) {
61
+ options.onMessages?.(mocks.streamedMessages, config.runConfig);
62
+ }
63
+ };
64
+ return {
65
+ messages: mocks.messages,
66
+ stateDelta: {},
67
+ agentInfo: {},
68
+ longRunningToolIds: [],
69
+ artifactDelta: {},
70
+ // Deliberately empty: the projection must come from the transcript, not
71
+ // from derived confirmation state that a mid-run accumulator swap drops.
72
+ toolConfirmations: [],
73
+ authRequests: [],
74
+ escalated: false,
75
+ messageMetadata: new Map(),
76
+ sendMessage,
77
+ cancel: vi.fn(),
78
+ setMessages: vi.fn(),
79
+ replaceMessages: vi.fn(),
80
+ applySnapshot: mocks.applySnapshot,
81
+ };
82
+ };
83
+
84
+ return {
85
+ ...(await importOriginal<typeof import("./useAdkMessages")>()),
86
+ useAdkMessages: useMockAdkMessages,
87
+ useAdkMessagesInternal: useMockAdkMessages,
88
+ };
89
+ });
59
90
 
60
91
  import { AdkEventAccumulator } from "./AdkEventAccumulator";
61
92
  import { useAdkRuntime } from "./useAdkRuntime";
62
93
 
63
- type ApprovalAdapter = {
94
+ type RuntimeAdapter = {
64
95
  messages: readonly ThreadMessage[];
96
+ extras: {
97
+ send: (
98
+ messages: AdkMessage[],
99
+ config: AdkSendMessageConfig,
100
+ ) => Promise<void>;
101
+ };
102
+ onNew?: (message: AppendMessage) => Promise<void> | void;
103
+ onEdit?: (message: AppendMessage) => Promise<void> | void;
104
+ onAddToolResult?: (options: AddToolResultOptions) => Promise<void> | void;
65
105
  onRespondToToolApproval?: (
66
106
  options: RespondToToolApprovalOptions,
67
107
  ) => Promise<void> | void;
108
+ onRefetchThread?: () => Promise<void> | void;
68
109
  };
69
110
 
70
111
  const CONFIRMATION_CALL = "adk-confirmation-1";
71
112
 
72
- const latestAdapter = () => mocks.adapters.at(-1) as ApprovalAdapter;
113
+ const latestAdapter = () => mocks.adapters.at(-1) as RuntimeAdapter;
114
+
115
+ const makeUserMessage = (
116
+ text: string,
117
+ runConfig: AppendMessage["runConfig"],
118
+ ): AppendMessage =>
119
+ ({
120
+ role: "user",
121
+ content: [{ type: "text", text }],
122
+ attachments: [],
123
+ parentId: null,
124
+ sourceId: null,
125
+ runConfig,
126
+ metadata: { custom: {} },
127
+ }) as unknown as AppendMessage;
73
128
 
74
129
  const makeConfirmationRequest = (): AdkMessage => ({
75
130
  id: "ai-1",
@@ -96,20 +151,230 @@ const approvalPart = () =>
96
151
 
97
152
  afterEach(() => {
98
153
  mocks.adapters.length = 0;
154
+ mocks.sendMessage.mockClear();
99
155
  mocks.messages = [];
156
+ mocks.messageRunConfig = undefined;
157
+ mocks.streamedMessages = undefined;
158
+ mocks.applySnapshot.mockReset();
159
+ mocks.threadListItem.source = null;
160
+ mocks.threadListItem.externalId = undefined;
100
161
  });
101
162
 
102
163
  describe("useAdkRuntime tool approvals", () => {
164
+ it("resumes a delayed tool result with its originating run config", async () => {
165
+ const runConfigA = { custom: { model: "model-a" } };
166
+ const runConfigB = { custom: { model: "model-b" } };
167
+ const { rerender } = renderHook(() => useAdkRuntime({ stream: vi.fn() }));
168
+
169
+ await act(async () => {
170
+ await latestAdapter().onNew!(makeUserMessage("first", runConfigA));
171
+ });
172
+
173
+ mocks.messages = [
174
+ { id: "u-1", type: "human", content: "first" },
175
+ {
176
+ id: "ai-1",
177
+ type: "ai",
178
+ content: [],
179
+ tool_calls: [
180
+ {
181
+ id: "tool-a",
182
+ name: "lookup",
183
+ args: {},
184
+ },
185
+ ],
186
+ },
187
+ ];
188
+ mocks.messageRunConfig = runConfigA;
189
+ rerender();
190
+
191
+ await act(async () => {
192
+ await latestAdapter().onNew!(makeUserMessage("second", runConfigB));
193
+ await latestAdapter().onAddToolResult!({
194
+ messageId: "ai-1",
195
+ toolCallId: "tool-a",
196
+ toolName: "lookup",
197
+ result: { value: "done" },
198
+ isError: false,
199
+ });
200
+ });
201
+
202
+ expect(
203
+ mocks.sendMessage.mock.calls.map((call) => call[1].runConfig),
204
+ ).toEqual([runConfigA, runConfigB, runConfigA]);
205
+ });
206
+
207
+ it("preserves pending tool ownership across a thread refetch", async () => {
208
+ const runConfig = { custom: { model: "model-a" } };
209
+ const laterRunConfig = { custom: { model: "model-b" } };
210
+ const currentMessages: AdkMessage[] = [
211
+ {
212
+ id: "ai-1",
213
+ type: "ai",
214
+ content: [],
215
+ tool_calls: [{ id: "tool-a", name: "lookup", args: {} }],
216
+ },
217
+ ];
218
+ const loadedMessages: AdkMessage[] = [
219
+ ...currentMessages,
220
+ {
221
+ id: "ai-2",
222
+ type: "ai",
223
+ content: [],
224
+ tool_calls: [
225
+ null as never,
226
+ { id: "tool-loaded", name: "lookup", args: {} },
227
+ ],
228
+ },
229
+ ];
230
+ const load = vi.fn(async () => ({ messages: loadedMessages }));
231
+ mocks.threadListItem.source = {};
232
+
233
+ const { rerender } = renderHook(() =>
234
+ useAdkRuntime({ stream: vi.fn(), load }),
235
+ );
236
+
237
+ await act(async () => {
238
+ await latestAdapter().onNew!(makeUserMessage("first", runConfig));
239
+ });
240
+ mocks.messages = currentMessages;
241
+ mocks.messageRunConfig = runConfig;
242
+ rerender();
243
+
244
+ mocks.threadListItem.externalId = "thread-a";
245
+ rerender();
246
+ await act(async () => {
247
+ await latestAdapter().onRefetchThread!();
248
+ });
249
+
250
+ await act(async () => {
251
+ mocks.streamedMessages = [loadedMessages.at(-1)!];
252
+ await latestAdapter().extras.send(
253
+ [{ id: "u-later", type: "human", content: "later" }],
254
+ { runConfig: laterRunConfig },
255
+ );
256
+ mocks.streamedMessages = undefined;
257
+ await latestAdapter().onAddToolResult!({
258
+ messageId: "ai-1",
259
+ toolCallId: "tool-a",
260
+ toolName: "lookup",
261
+ result: { value: "done" },
262
+ isError: false,
263
+ });
264
+ await latestAdapter().onAddToolResult!({
265
+ messageId: "ai-2",
266
+ toolCallId: "tool-loaded",
267
+ toolName: "lookup",
268
+ result: { value: "loaded" },
269
+ isError: false,
270
+ });
271
+ });
272
+
273
+ expect(
274
+ mocks.sendMessage.mock.calls.slice(-2).map((call) => call[1]),
275
+ ).toEqual([{ runConfig }, { runConfig: undefined }]);
276
+ });
277
+
278
+ it("tracks and prunes ownership around malformed tool-call entries", async () => {
279
+ const runConfig = { custom: { model: "model-a" } };
280
+ mocks.messages = [
281
+ { id: "u-1", type: "human", content: "first" },
282
+ {
283
+ id: "ai-1",
284
+ type: "ai",
285
+ content: [],
286
+ tool_calls: [null as never, { id: "tool-a", name: "lookup", args: {} }],
287
+ },
288
+ ];
289
+ mocks.messageRunConfig = runConfig;
290
+ renderHook(() =>
291
+ useAdkRuntime({
292
+ stream: vi.fn(),
293
+ getCheckpointId: vi.fn(async () => null),
294
+ }),
295
+ );
296
+
297
+ await act(async () => {
298
+ await latestAdapter().onEdit!({
299
+ ...makeUserMessage("edited", runConfig),
300
+ parentId: "ai-1",
301
+ });
302
+ await latestAdapter().onAddToolResult!({
303
+ messageId: "ai-1",
304
+ toolCallId: "tool-a",
305
+ toolName: "lookup",
306
+ result: { value: "done" },
307
+ isError: false,
308
+ });
309
+ });
310
+
311
+ expect(mocks.sendMessage.mock.calls.at(-1)![1]).toEqual({ runConfig });
312
+ });
313
+
314
+ it("attributes new tool calls to an explicitly configured continuation", async () => {
315
+ const runConfigA = { custom: { model: "model-a" } };
316
+ const runConfigB = { custom: { model: "model-b" } };
317
+ renderHook(() => useAdkRuntime({ stream: vi.fn() }));
318
+
319
+ await act(async () => {
320
+ await latestAdapter().onNew!(makeUserMessage("first", runConfigA));
321
+ mocks.streamedMessages = [
322
+ {
323
+ id: "ai-b",
324
+ type: "ai",
325
+ content: [],
326
+ tool_calls: [{ id: "tool-b", name: "lookup", args: {} }],
327
+ },
328
+ ];
329
+ await latestAdapter().extras.send(
330
+ [
331
+ {
332
+ id: "tool-result-a",
333
+ type: "tool",
334
+ tool_call_id: "tool-a",
335
+ name: "lookup",
336
+ content: JSON.stringify({ value: "first" }),
337
+ },
338
+ ],
339
+ { runConfig: runConfigB },
340
+ );
341
+ });
342
+
343
+ await act(async () => {
344
+ await latestAdapter().onAddToolResult!({
345
+ messageId: "ai-b",
346
+ toolCallId: "tool-b",
347
+ toolName: "lookup",
348
+ result: { value: "second" },
349
+ isError: false,
350
+ });
351
+ });
352
+
353
+ expect(mocks.sendMessage.mock.calls.at(-1)![1]).toEqual({
354
+ runConfig: runConfigB,
355
+ });
356
+ });
357
+
103
358
  it("exposes, answers, and settles the default approval seam across a rerender", async () => {
359
+ const runConfig = { custom: { model: "model-a" } };
360
+ const nextRunConfig = { custom: { model: "model-b" } };
104
361
  // Retained across the rerender: core caches converted messages by input
105
362
  // object, so only a rebuilt converter can surface the settled decision.
106
363
  const confirmationRequest = makeConfirmationRequest();
364
+ const { rerender } = renderHook(() => useAdkRuntime({ stream: vi.fn() }));
365
+
366
+ await act(async () => {
367
+ await latestAdapter().onNew!(
368
+ makeUserMessage("delete the file", runConfig),
369
+ );
370
+ });
371
+
107
372
  mocks.messages = [
108
373
  { id: "u-1", type: "human", content: "delete the file" },
109
374
  confirmationRequest,
110
375
  ];
111
-
112
- const { rerender } = renderHook(() => useAdkRuntime({ stream: vi.fn() }));
376
+ mocks.messageRunConfig = runConfig;
377
+ rerender();
113
378
 
114
379
  expect(latestAdapter().messages.at(-1)!.status).toMatchObject({
115
380
  type: "requires-action",
@@ -121,6 +386,9 @@ describe("useAdkRuntime tool approvals", () => {
121
386
  });
122
387
 
123
388
  await act(async () => {
389
+ await latestAdapter().onNew!(
390
+ makeUserMessage("start another run", nextRunConfig),
391
+ );
124
392
  await latestAdapter().onRespondToToolApproval!({
125
393
  approvalId: CONFIRMATION_CALL,
126
394
  approved: false,
@@ -135,6 +403,7 @@ describe("useAdkRuntime tool approvals", () => {
135
403
  content: JSON.stringify({ confirmed: false }),
136
404
  }),
137
405
  ]);
406
+ expect(mocks.sendMessage.mock.calls.at(-1)![1]).toEqual({ runConfig });
138
407
 
139
408
  mocks.messages = [
140
409
  ...mocks.messages,