@assistant-ui/react-google-adk 0.0.28 → 0.0.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/AdkEventAccumulator.d.ts.map +1 -1
- package/dist/AdkEventAccumulator.js +2 -2
- package/dist/AdkEventAccumulator.js.map +1 -1
- package/dist/convertToAdkMessages.js +1 -1
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +6 -8
- package/dist/hooks.js.map +1 -1
- package/dist/parseAdkEvent.d.ts.map +1 -1
- package/dist/parseAdkEvent.js +15 -2
- package/dist/parseAdkEvent.js.map +1 -1
- package/dist/useAdkMessages.d.ts.map +1 -1
- package/dist/useAdkMessages.js +8 -5
- package/dist/useAdkMessages.js.map +1 -1
- package/dist/useAdkRuntime.d.ts.map +1 -1
- package/dist/useAdkRuntime.js +4 -2
- package/dist/useAdkRuntime.js.map +1 -1
- package/package.json +5 -5
- package/src/AdkClient.test.ts +53 -0
- package/src/AdkEventAccumulator.test.ts +21 -0
- package/src/AdkEventAccumulator.ts +3 -2
- package/src/hooks.render.test.tsx +74 -0
- package/src/hooks.test.tsx +33 -0
- package/src/hooks.ts +17 -22
- package/src/parseAdkEvent.ts +36 -7
- package/src/useAdkMessages.test.ts +99 -0
- package/src/useAdkMessages.ts +29 -9
- package/src/useAdkRuntime.replacement.test.tsx +138 -0
- package/src/useAdkRuntime.ts +3 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useAdkRuntime.js","names":[],"sources":["../src/useAdkRuntime.ts"],"sourcesContent":["import {\n useCallback,\n useEffect,\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 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\";\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 = useMemo(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\n const handleSendMessage = async (\n msgs: AdkMessage[],\n config: AdkSendMessageConfig,\n ) => {\n try {\n setIsRunning(true);\n await sendMessage(msgs, config);\n } finally {\n 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 useEffect(() => {\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 }, [loadController, runLoad]);\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 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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"}
|
|
1
|
+
{"version":3,"file":"useAdkRuntime.js","names":[],"sources":["../src/useAdkRuntime.ts"],"sourcesContent":["import {\n useCallback,\n useEffect,\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 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\";\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 = useMemo(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 useEffect(() => {\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 }, [loadController, runLoad]);\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 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":";;;;;;;;;;;AAiHA,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,iBAAiB,QAAQ,2BAA2B,CAAC,CAAC;CAC5D,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,gBAAgB;EACd,QAAQ;EACR,aAAa;GAGX,eAAe,MAAM;GACrB,mBAAmB,KAAK;EAC1B;CACF,GAAG,CAAC,gBAAgB,OAAO,CAAC;CA+J5B,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;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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@assistant-ui/react-google-adk",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.29",
|
|
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.
|
|
38
|
-
"@assistant-ui/store": "^0.3.
|
|
37
|
+
"@assistant-ui/core": "^0.3.18",
|
|
38
|
+
"@assistant-ui/store": "^0.3.13",
|
|
39
39
|
"assistant-cloud": "*",
|
|
40
|
-
"assistant-stream": "^0.3.
|
|
40
|
+
"assistant-stream": "^0.3.42"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
43
|
"@google/adk": ">=0.5.0",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
}
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
|
-
"@assistant-ui/x-buildutils": "0.0.
|
|
56
|
+
"@assistant-ui/x-buildutils": "0.0.26",
|
|
57
57
|
"@testing-library/dom": "^10.4.1",
|
|
58
58
|
"@testing-library/react": "^16.3.3",
|
|
59
59
|
"@types/react": "^19.2.18",
|
package/src/AdkClient.test.ts
CHANGED
|
@@ -472,6 +472,59 @@ describe("createAdkStream - SSE parsing", () => {
|
|
|
472
472
|
},
|
|
473
473
|
);
|
|
474
474
|
|
|
475
|
+
it.each([
|
|
476
|
+
[{ content: [] }, "content", "an object"],
|
|
477
|
+
[{ content: { parts: 42 } }, "content.parts", "an array of objects"],
|
|
478
|
+
[{ content: { parts: [null] } }, "content.parts", "an array of objects"],
|
|
479
|
+
])(
|
|
480
|
+
"rejects malformed nested stream event content: %#",
|
|
481
|
+
async (event, field, expectation) => {
|
|
482
|
+
mockFetch.mockResolvedValueOnce(
|
|
483
|
+
sseResponse(sseBody(`data: ${JSON.stringify(event)}\n\n`)),
|
|
484
|
+
);
|
|
485
|
+
|
|
486
|
+
const stream = createAdkStream({ api: "/api/adk" });
|
|
487
|
+
const consume = async () => {
|
|
488
|
+
const gen = await stream(
|
|
489
|
+
[{ id: "m1", type: "human", content: "Hi" }],
|
|
490
|
+
makeConfig(),
|
|
491
|
+
);
|
|
492
|
+
for await (const _event of gen) {
|
|
493
|
+
void _event;
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
await expect(consume()).rejects.toThrow(
|
|
498
|
+
`Invalid ADK stream event: expected "${field}" to be ${expectation} when present.`,
|
|
499
|
+
);
|
|
500
|
+
},
|
|
501
|
+
);
|
|
502
|
+
|
|
503
|
+
it.each([
|
|
504
|
+
[{ id: "e1", content: null }, undefined],
|
|
505
|
+
[{ id: "e1", content: { role: "model", parts: null } }, { role: "model" }],
|
|
506
|
+
])(
|
|
507
|
+
"accepts null optional nested stream event content: %#",
|
|
508
|
+
async (event, expectedContent) => {
|
|
509
|
+
mockFetch.mockResolvedValueOnce(
|
|
510
|
+
sseResponse(sseBody(`data: ${JSON.stringify(event)}\n\n`)),
|
|
511
|
+
);
|
|
512
|
+
|
|
513
|
+
const stream = createAdkStream({ api: "/api/adk" });
|
|
514
|
+
const gen = await stream(
|
|
515
|
+
[{ id: "m1", type: "human", content: "Hi" }],
|
|
516
|
+
makeConfig(),
|
|
517
|
+
);
|
|
518
|
+
const collected: AdkEvent[] = [];
|
|
519
|
+
for await (const parsedEvent of gen) {
|
|
520
|
+
collected.push(parsedEvent);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
expect(collected).toHaveLength(1);
|
|
524
|
+
expect(collected[0]!.content).toEqual(expectedContent);
|
|
525
|
+
},
|
|
526
|
+
);
|
|
527
|
+
|
|
475
528
|
it("reports invalid JSON as an ADK stream event error", async () => {
|
|
476
529
|
mockFetch.mockResolvedValueOnce(sseResponse(sseBody("data: not-json\n\n")));
|
|
477
530
|
|
|
@@ -838,6 +838,27 @@ describe("AdkEventAccumulator - actions tracking", () => {
|
|
|
838
838
|
expect(acc.getArtifactDelta()).toEqual({ "file.txt": 1 });
|
|
839
839
|
});
|
|
840
840
|
|
|
841
|
+
it("preserves prototype-named state and artifact keys", () => {
|
|
842
|
+
const acc = new AdkEventAccumulator();
|
|
843
|
+
acc.processEvent(
|
|
844
|
+
makeEvent({
|
|
845
|
+
actions: {
|
|
846
|
+
stateDelta: JSON.parse('{"__proto__":"session"}'),
|
|
847
|
+
artifactDelta: JSON.parse('{"__proto__":1}'),
|
|
848
|
+
},
|
|
849
|
+
author: "agent",
|
|
850
|
+
content: { parts: [{ text: "x" }] },
|
|
851
|
+
}),
|
|
852
|
+
);
|
|
853
|
+
|
|
854
|
+
const stateDelta = acc.getStateDelta();
|
|
855
|
+
const artifactDelta = acc.getArtifactDelta();
|
|
856
|
+
expect(Object.hasOwn(stateDelta, "__proto__")).toBe(true);
|
|
857
|
+
expect(stateDelta["__proto__"]).toBe("session");
|
|
858
|
+
expect(Object.hasOwn(artifactDelta, "__proto__")).toBe(true);
|
|
859
|
+
expect(artifactDelta["__proto__"]).toBe(1);
|
|
860
|
+
});
|
|
861
|
+
|
|
841
862
|
it("tracks escalation flag", () => {
|
|
842
863
|
const acc = new AdkEventAccumulator();
|
|
843
864
|
expect(acc.isEscalated()).toBe(false);
|
|
@@ -194,8 +194,9 @@ export class AdkEventAccumulator {
|
|
|
194
194
|
private finalTextReplacedThisEvent = false;
|
|
195
195
|
private finalReasoningReplacedThisEvent = false;
|
|
196
196
|
private partialReasoningBuffer = "";
|
|
197
|
-
private accumulatedStateDelta: Record<string, unknown> =
|
|
198
|
-
private accumulatedArtifactDelta: Record<string, number> =
|
|
197
|
+
private accumulatedStateDelta: Record<string, unknown> = Object.create(null);
|
|
198
|
+
private accumulatedArtifactDelta: Record<string, number> =
|
|
199
|
+
Object.create(null);
|
|
199
200
|
private lastAgentInfo: {
|
|
200
201
|
name?: string | undefined;
|
|
201
202
|
branch?: string | undefined;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { act, render, waitFor } from "@testing-library/react";
|
|
4
|
+
import type { AssistantRuntime } from "@assistant-ui/core";
|
|
5
|
+
import { AssistantRuntimeProvider } from "@assistant-ui/core/react";
|
|
6
|
+
import { describe, expect, it, vi } from "vitest";
|
|
7
|
+
import { useAdkAppState } from "./hooks";
|
|
8
|
+
import type { AdkEvent } from "./types";
|
|
9
|
+
import { useAdkRuntime } from "./useAdkRuntime";
|
|
10
|
+
|
|
11
|
+
describe("ADK state hook rendering", () => {
|
|
12
|
+
it("keeps app state stable across unrelated store updates", async () => {
|
|
13
|
+
const deltas = [
|
|
14
|
+
{
|
|
15
|
+
"app:visible": 1,
|
|
16
|
+
"app:__proto__": { source: "provider" },
|
|
17
|
+
},
|
|
18
|
+
{ unrelated: true },
|
|
19
|
+
{ "app:visible": 2 },
|
|
20
|
+
];
|
|
21
|
+
const stream = vi.fn(async function* () {
|
|
22
|
+
const call = stream.mock.calls.length - 1;
|
|
23
|
+
yield {
|
|
24
|
+
id: `event-${call}`,
|
|
25
|
+
author: "agent",
|
|
26
|
+
actions: { stateDelta: deltas[call] },
|
|
27
|
+
turnComplete: true,
|
|
28
|
+
} satisfies AdkEvent;
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
let runtime: AssistantRuntime | undefined;
|
|
32
|
+
let appState: Record<string, unknown> | undefined;
|
|
33
|
+
|
|
34
|
+
const Probe = () => {
|
|
35
|
+
appState = useAdkAppState();
|
|
36
|
+
return null;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const App = () => {
|
|
40
|
+
runtime = useAdkRuntime({
|
|
41
|
+
stream,
|
|
42
|
+
create: async () => ({ externalId: "thread-1" }),
|
|
43
|
+
});
|
|
44
|
+
return (
|
|
45
|
+
<AssistantRuntimeProvider runtime={runtime}>
|
|
46
|
+
<Probe />
|
|
47
|
+
</AssistantRuntimeProvider>
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
render(<App />);
|
|
52
|
+
const send = (text: string) =>
|
|
53
|
+
act(async () => {
|
|
54
|
+
await runtime!.thread.append({
|
|
55
|
+
role: "user",
|
|
56
|
+
content: [{ type: "text", text }],
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
await send("first");
|
|
61
|
+
await waitFor(() => expect(appState?.visible).toBe(1));
|
|
62
|
+
|
|
63
|
+
const initial = appState;
|
|
64
|
+
expect(Object.hasOwn(initial!, "__proto__")).toBe(true);
|
|
65
|
+
expect(initial?.["__proto__"]).toEqual({ source: "provider" });
|
|
66
|
+
|
|
67
|
+
await send("second");
|
|
68
|
+
expect(appState).toBe(initial);
|
|
69
|
+
|
|
70
|
+
await send("third");
|
|
71
|
+
await waitFor(() => expect(appState?.visible).toBe(2));
|
|
72
|
+
expect(appState).not.toBe(initial);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { renderHook } from "@testing-library/react";
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
|
|
4
|
+
const mocks = vi.hoisted(() => ({
|
|
5
|
+
stateDelta: {} as Record<string, unknown>,
|
|
6
|
+
}));
|
|
7
|
+
|
|
8
|
+
vi.mock("./adkExtras", async (importOriginal) => ({
|
|
9
|
+
...(await importOriginal<typeof import("./adkExtras")>()),
|
|
10
|
+
adkExtras: {
|
|
11
|
+
use: (
|
|
12
|
+
selector: (extras: { stateDelta: Record<string, unknown> }) => unknown,
|
|
13
|
+
) => selector({ stateDelta: mocks.stateDelta }),
|
|
14
|
+
},
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
import { useAdkAppState } from "./hooks";
|
|
18
|
+
|
|
19
|
+
describe("ADK state hooks", () => {
|
|
20
|
+
it("preserves a prototype-named state key", () => {
|
|
21
|
+
mocks.stateDelta = Object.fromEntries([
|
|
22
|
+
["app:__proto__", { source: "provider" }],
|
|
23
|
+
["app:visible", true],
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
const { result } = renderHook(() => useAdkAppState());
|
|
27
|
+
|
|
28
|
+
expect(Object.getPrototypeOf(result.current)).toBe(Object.prototype);
|
|
29
|
+
expect(Object.hasOwn(result.current, "__proto__")).toBe(true);
|
|
30
|
+
expect(result.current["__proto__"]).toEqual({ source: "provider" });
|
|
31
|
+
expect(result.current.visible).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
});
|
package/src/hooks.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { generateId } from "@assistant-ui/core";
|
|
2
2
|
import { useAui } from "@assistant-ui/store";
|
|
3
|
+
import { useShallowSelector } from "@assistant-ui/store/internal";
|
|
3
4
|
import type { ReadonlyJSONValue } from "assistant-stream/utils";
|
|
4
5
|
import { adkExtras } from "./adkExtras";
|
|
5
6
|
import { toAdkConfirmationReply } from "./adkToolApproval";
|
|
@@ -10,6 +11,7 @@ import type {
|
|
|
10
11
|
AdkAuthCredential,
|
|
11
12
|
AdkAuthRequest,
|
|
12
13
|
AdkMessageMetadata,
|
|
14
|
+
AdkRuntimeExtras,
|
|
13
15
|
} from "./types";
|
|
14
16
|
|
|
15
17
|
const EMPTY_STATE_DELTA: Record<string, unknown> = {};
|
|
@@ -119,33 +121,26 @@ const TEMP_PREFIX = "temp:";
|
|
|
119
121
|
const filterByPrefix = (
|
|
120
122
|
state: Record<string, unknown>,
|
|
121
123
|
prefix: string,
|
|
122
|
-
): Record<string, unknown> =>
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
return result;
|
|
130
|
-
};
|
|
124
|
+
): Record<string, unknown> =>
|
|
125
|
+
Object.fromEntries(
|
|
126
|
+
Object.entries(state)
|
|
127
|
+
.filter(([key]) => key.startsWith(prefix))
|
|
128
|
+
.map(([key, value]) => [key.slice(prefix.length), value]),
|
|
129
|
+
);
|
|
131
130
|
|
|
132
|
-
|
|
133
|
-
export const useAdkAppState = () =>
|
|
131
|
+
const useAdkStateByPrefix = (prefix: string) =>
|
|
134
132
|
adkExtras.use(
|
|
135
|
-
(e) =>
|
|
133
|
+
useShallowSelector((e: AdkRuntimeExtras) =>
|
|
134
|
+
filterByPrefix(e.stateDelta, prefix),
|
|
135
|
+
),
|
|
136
136
|
EMPTY_STATE_DELTA,
|
|
137
137
|
);
|
|
138
138
|
|
|
139
|
+
/** Returns app-level state (keys prefixed with `app:`, prefix stripped). */
|
|
140
|
+
export const useAdkAppState = () => useAdkStateByPrefix(APP_PREFIX);
|
|
141
|
+
|
|
139
142
|
/** Returns user-level state (keys prefixed with `user:`, prefix stripped). */
|
|
140
|
-
export const useAdkUserState = () =>
|
|
141
|
-
adkExtras.use(
|
|
142
|
-
(e) => filterByPrefix(e.stateDelta, USER_PREFIX),
|
|
143
|
-
EMPTY_STATE_DELTA,
|
|
144
|
-
);
|
|
143
|
+
export const useAdkUserState = () => useAdkStateByPrefix(USER_PREFIX);
|
|
145
144
|
|
|
146
145
|
/** Returns temp state (keys prefixed with `temp:`, prefix stripped). Not persisted. */
|
|
147
|
-
export const useAdkTempState = () =>
|
|
148
|
-
adkExtras.use(
|
|
149
|
-
(e) => filterByPrefix(e.stateDelta, TEMP_PREFIX),
|
|
150
|
-
EMPTY_STATE_DELTA,
|
|
151
|
-
);
|
|
146
|
+
export const useAdkTempState = () => useAdkStateByPrefix(TEMP_PREFIX);
|
package/src/parseAdkEvent.ts
CHANGED
|
@@ -1,19 +1,26 @@
|
|
|
1
|
+
import { isRecord } from "@assistant-ui/core/internal";
|
|
1
2
|
import type { AdkEvent } from "./types";
|
|
2
3
|
|
|
4
|
+
const invalidField = (
|
|
5
|
+
errorPrefix: string,
|
|
6
|
+
field: string,
|
|
7
|
+
expectation: string,
|
|
8
|
+
): Error =>
|
|
9
|
+
new Error(`${errorPrefix}: expected "${field}" to be ${expectation}.`);
|
|
10
|
+
|
|
3
11
|
export function parseAdkEventValue(
|
|
4
12
|
value: unknown,
|
|
5
13
|
errorPrefix: string,
|
|
6
14
|
): AdkEvent {
|
|
7
|
-
if (
|
|
8
|
-
typeof value !== "object" ||
|
|
9
|
-
value === null ||
|
|
10
|
-
Array.isArray(value) ||
|
|
11
|
-
Object.keys(value).length === 0
|
|
12
|
-
) {
|
|
15
|
+
if (!isRecord(value) || Object.keys(value).length === 0) {
|
|
13
16
|
throw new Error(`${errorPrefix}: expected a non-empty object.`);
|
|
14
17
|
}
|
|
15
18
|
|
|
16
|
-
const {
|
|
19
|
+
const {
|
|
20
|
+
id: rawId,
|
|
21
|
+
content: rawContent,
|
|
22
|
+
...event
|
|
23
|
+
} = value as Record<string, unknown>;
|
|
17
24
|
if (
|
|
18
25
|
rawId != null &&
|
|
19
26
|
typeof rawId !== "string" &&
|
|
@@ -24,12 +31,34 @@ export function parseAdkEventValue(
|
|
|
24
31
|
);
|
|
25
32
|
}
|
|
26
33
|
|
|
34
|
+
let content: Record<string, unknown> | undefined;
|
|
35
|
+
if (rawContent != null) {
|
|
36
|
+
if (!isRecord(rawContent)) {
|
|
37
|
+
throw invalidField(errorPrefix, "content", "an object when present");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const { parts, ...contentFields } = rawContent;
|
|
41
|
+
if (parts != null && (!Array.isArray(parts) || !parts.every(isRecord))) {
|
|
42
|
+
throw invalidField(
|
|
43
|
+
errorPrefix,
|
|
44
|
+
"content.parts",
|
|
45
|
+
"an array of objects when present",
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
content = {
|
|
50
|
+
...contentFields,
|
|
51
|
+
...(parts != null && { parts }),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
27
55
|
const errorMessage =
|
|
28
56
|
"error" in event && typeof event.error === "string"
|
|
29
57
|
? event.error
|
|
30
58
|
: undefined;
|
|
31
59
|
return {
|
|
32
60
|
...event,
|
|
61
|
+
...(content !== undefined && { content }),
|
|
33
62
|
...(rawId != null && { id: String(rawId) }),
|
|
34
63
|
...(errorMessage !== undefined &&
|
|
35
64
|
!("errorMessage" in event) &&
|
|
@@ -131,6 +131,105 @@ describe("ADK runtime callbacks", () => {
|
|
|
131
131
|
});
|
|
132
132
|
|
|
133
133
|
describe("ADK stream lifecycle", () => {
|
|
134
|
+
it("settles a superseded send while its stream is still opening", async () => {
|
|
135
|
+
const signals: AbortSignal[] = [];
|
|
136
|
+
const parked = new Promise<AsyncGenerator<AdkEvent>>(() => {});
|
|
137
|
+
let calls = 0;
|
|
138
|
+
const stream = vi.fn(function (_messages, { abortSignal }) {
|
|
139
|
+
signals.push(abortSignal);
|
|
140
|
+
if (calls++ === 0) return parked;
|
|
141
|
+
return (async function* () {
|
|
142
|
+
yield {
|
|
143
|
+
id: "event-1",
|
|
144
|
+
invocationId: "run-1",
|
|
145
|
+
author: "agent",
|
|
146
|
+
content: { role: "model", parts: [{ text: "done-1" }] },
|
|
147
|
+
};
|
|
148
|
+
})();
|
|
149
|
+
}) satisfies AdkStreamCallback;
|
|
150
|
+
const { result } = renderHook(() => useAdkMessages({ stream }));
|
|
151
|
+
|
|
152
|
+
let firstSend!: Promise<void>;
|
|
153
|
+
act(() => {
|
|
154
|
+
firstSend = result.current.sendMessage(
|
|
155
|
+
[{ id: "user-1", type: "human", content: "first" }],
|
|
156
|
+
{},
|
|
157
|
+
);
|
|
158
|
+
});
|
|
159
|
+
await vi.waitFor(() => expect(stream).toHaveBeenCalledOnce());
|
|
160
|
+
|
|
161
|
+
let secondSend!: Promise<void>;
|
|
162
|
+
act(() => {
|
|
163
|
+
secondSend = result.current.sendMessage(
|
|
164
|
+
[{ id: "user-2", type: "human", content: "second" }],
|
|
165
|
+
{},
|
|
166
|
+
);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
await act(async () => {
|
|
170
|
+
await Promise.all([firstSend, secondSend]);
|
|
171
|
+
});
|
|
172
|
+
expect(signals[0]?.aborted).toBe(true);
|
|
173
|
+
expect(signals[1]?.aborted).toBe(false);
|
|
174
|
+
expect(result.current.messages.at(-1)).toMatchObject({
|
|
175
|
+
type: "ai",
|
|
176
|
+
content: [{ type: "text", text: "done-1" }],
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("aborts and settles a superseded stream that stops yielding", async () => {
|
|
181
|
+
const signals: AbortSignal[] = [];
|
|
182
|
+
const parked = new Promise<void>(() => {});
|
|
183
|
+
let calls = 0;
|
|
184
|
+
const stream = vi.fn(function (
|
|
185
|
+
_messages,
|
|
186
|
+
{ abortSignal },
|
|
187
|
+
): AsyncGenerator<AdkEvent> {
|
|
188
|
+
signals.push(abortSignal);
|
|
189
|
+
if (calls++ === 0) {
|
|
190
|
+
return (async function* () {
|
|
191
|
+
await parked;
|
|
192
|
+
})();
|
|
193
|
+
}
|
|
194
|
+
return (async function* () {
|
|
195
|
+
yield {
|
|
196
|
+
id: "event-1",
|
|
197
|
+
invocationId: "run-1",
|
|
198
|
+
author: "agent",
|
|
199
|
+
content: { role: "model", parts: [{ text: "done-1" }] },
|
|
200
|
+
};
|
|
201
|
+
})();
|
|
202
|
+
}) satisfies AdkStreamCallback;
|
|
203
|
+
const { result } = renderHook(() => useAdkMessages({ stream }));
|
|
204
|
+
|
|
205
|
+
let firstSend!: Promise<void>;
|
|
206
|
+
act(() => {
|
|
207
|
+
firstSend = result.current.sendMessage(
|
|
208
|
+
[{ id: "user-1", type: "human", content: "first" }],
|
|
209
|
+
{},
|
|
210
|
+
);
|
|
211
|
+
});
|
|
212
|
+
await vi.waitFor(() => expect(stream).toHaveBeenCalledOnce());
|
|
213
|
+
|
|
214
|
+
let secondSend!: Promise<void>;
|
|
215
|
+
act(() => {
|
|
216
|
+
secondSend = result.current.sendMessage(
|
|
217
|
+
[{ id: "user-2", type: "human", content: "second" }],
|
|
218
|
+
{},
|
|
219
|
+
);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
await act(async () => {
|
|
223
|
+
await Promise.all([firstSend, secondSend]);
|
|
224
|
+
});
|
|
225
|
+
expect(signals[0]?.aborted).toBe(true);
|
|
226
|
+
expect(signals[1]?.aborted).toBe(false);
|
|
227
|
+
expect(result.current.messages.at(-1)).toMatchObject({
|
|
228
|
+
type: "ai",
|
|
229
|
+
content: [{ type: "text", text: "done-1" }],
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
134
233
|
it("aborts the active stream when the hook unmounts", async () => {
|
|
135
234
|
let runSignal: AbortSignal | undefined;
|
|
136
235
|
let resolveStarted!: () => void;
|
package/src/useAdkMessages.ts
CHANGED
|
@@ -8,7 +8,11 @@ import {
|
|
|
8
8
|
} from "react";
|
|
9
9
|
import { generateId } from "@assistant-ui/core";
|
|
10
10
|
import { useAui } from "@assistant-ui/store";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
abortableIterable,
|
|
13
|
+
invokeUserCallback,
|
|
14
|
+
openAbortableIterable,
|
|
15
|
+
} from "@assistant-ui/core/internal";
|
|
12
16
|
import { AdkEventAccumulator } from "./AdkEventAccumulator";
|
|
13
17
|
import { contentToParts } from "./contentToParts";
|
|
14
18
|
import type {
|
|
@@ -152,19 +156,34 @@ export const useAdkMessages = ({
|
|
|
152
156
|
}
|
|
153
157
|
setMessagesImmediate(accumulator.getMessages());
|
|
154
158
|
|
|
159
|
+
// Google ADK replaces active runs, while React LangGraph queues sends.
|
|
160
|
+
abortControllerRef.current?.abort();
|
|
155
161
|
const abortController = new AbortController();
|
|
156
162
|
abortControllerRef.current = abortController;
|
|
157
163
|
|
|
158
164
|
try {
|
|
159
|
-
const response = await
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
165
|
+
const response = await openAbortableIterable(
|
|
166
|
+
stream(newMessagesWithId, {
|
|
167
|
+
...config,
|
|
168
|
+
abortSignal: abortController.signal,
|
|
169
|
+
initialize: async () => {
|
|
170
|
+
return await aui.threadListItem.initialize();
|
|
171
|
+
},
|
|
172
|
+
}),
|
|
173
|
+
abortController.signal,
|
|
174
|
+
);
|
|
175
|
+
if (!response) return;
|
|
166
176
|
|
|
167
|
-
for await (const event of
|
|
177
|
+
for await (const event of abortableIterable(
|
|
178
|
+
response,
|
|
179
|
+
abortController.signal,
|
|
180
|
+
)) {
|
|
181
|
+
if (
|
|
182
|
+
abortController.signal.aborted ||
|
|
183
|
+
abortControllerRef.current !== abortController
|
|
184
|
+
) {
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
168
187
|
const updatedMessages = accumulator.processEvent(event);
|
|
169
188
|
setMessagesImmediate(updatedMessages);
|
|
170
189
|
setStateDelta({
|
|
@@ -222,6 +241,7 @@ export const useAdkMessages = ({
|
|
|
222
241
|
} catch (error) {
|
|
223
242
|
if (
|
|
224
243
|
!abortController.signal.aborted &&
|
|
244
|
+
abortControllerRef.current === abortController &&
|
|
225
245
|
!(error instanceof Error && error.name === "AbortError")
|
|
226
246
|
) {
|
|
227
247
|
throw error;
|