@assistant-ui/react-ag-ui 0.0.59 → 0.0.61

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/agUiExtras.d.ts +2 -5
  2. package/dist/agUiExtras.d.ts.map +1 -1
  3. package/dist/hooks.d.ts +9 -12
  4. package/dist/hooks.d.ts.map +1 -1
  5. package/dist/index.d.ts +7 -5
  6. package/dist/index.d.ts.map +1 -0
  7. package/dist/runtime/AgUiThreadRuntimeCore.d.ts +153 -146
  8. package/dist/runtime/AgUiThreadRuntimeCore.d.ts.map +1 -1
  9. package/dist/runtime/AgUiThreadRuntimeCore.js +146 -26
  10. package/dist/runtime/AgUiThreadRuntimeCore.js.map +1 -1
  11. package/dist/runtime/adapter/conversions.d.ts +54 -56
  12. package/dist/runtime/adapter/conversions.d.ts.map +1 -1
  13. package/dist/runtime/adapter/conversions.js +164 -23
  14. package/dist/runtime/adapter/conversions.js.map +1 -1
  15. package/dist/runtime/adapter/run-aggregator.d.ts +98 -98
  16. package/dist/runtime/adapter/run-aggregator.d.ts.map +1 -1
  17. package/dist/runtime/adapter/run-aggregator.js +35 -2
  18. package/dist/runtime/adapter/run-aggregator.js.map +1 -1
  19. package/dist/runtime/adapter/subscriber.d.ts +109 -111
  20. package/dist/runtime/adapter/subscriber.d.ts.map +1 -1
  21. package/dist/runtime/adapter/tool-approval.d.ts +7 -9
  22. package/dist/runtime/adapter/tool-approval.d.ts.map +1 -1
  23. package/dist/runtime/event-parser.d.ts +5 -8
  24. package/dist/runtime/event-parser.d.ts.map +1 -1
  25. package/dist/runtime/interrupt-internals.d.ts +4 -7
  26. package/dist/runtime/interrupt-internals.d.ts.map +1 -1
  27. package/dist/runtime/logger.d.ts +4 -7
  28. package/dist/runtime/logger.d.ts.map +1 -1
  29. package/dist/runtime/mcp-tool-result.d.ts +7 -10
  30. package/dist/runtime/mcp-tool-result.d.ts.map +1 -1
  31. package/dist/runtime/types.d.ts +248 -250
  32. package/dist/runtime/types.d.ts.map +1 -1
  33. package/dist/useAgUiRuntime.d.ts +15 -18
  34. package/dist/useAgUiRuntime.d.ts.map +1 -1
  35. package/dist/useAgUiRuntime.js +9 -2
  36. package/dist/useAgUiRuntime.js.map +1 -1
  37. package/package.json +12 -11
  38. package/src/runtime/AgUiThreadRuntimeCore.ts +246 -30
  39. package/src/runtime/adapter/conversions.content.test.ts +56 -0
  40. package/src/runtime/adapter/conversions.ts +278 -36
  41. package/src/runtime/adapter/run-aggregator.ts +53 -2
  42. package/src/useAgUiRuntime.history.test.tsx +93 -2
  43. package/src/useAgUiRuntime.thread-switch.test.tsx +84 -4
  44. package/src/useAgUiRuntime.ts +12 -3
  45. package/src/useAgUiRuntime.voice.test.tsx +402 -0
@@ -1 +1 @@
1
- {"version":3,"file":"useAgUiRuntime.js","names":[],"sources":["../src/useAgUiRuntime.ts"],"sourcesContent":["\"use client\";\n\nimport {\n useCallback,\n useEffect,\n useLayoutEffect,\n useMemo,\n useRef,\n useState,\n useSyncExternalStore,\n} from \"react\";\nimport {\n useExternalStoreRuntime,\n useExternalStoreSharedOptions,\n useRuntimeAdapters,\n} from \"@assistant-ui/core/react\";\nimport { createMessageQueue } from \"@assistant-ui/core\";\nimport type {\n MessageQueueController,\n ToolExecutionStatus,\n} from \"@assistant-ui/core\";\nimport type {\n AssistantRuntime,\n AppendMessage,\n ExternalStoreAdapter,\n ThreadMessage,\n} from \"@assistant-ui/core\";\nimport type { ReadonlyJSONValue } from \"assistant-stream/utils\";\nimport { makeLogger } from \"./runtime/logger\";\nimport type {\n AgUiInterrupt,\n AgUiResumeEntry,\n UseAgUiRuntimeOptions,\n} from \"./runtime/types\";\nimport { AgUiThreadRuntimeCore } from \"./runtime/AgUiThreadRuntimeCore\";\nimport { agUiExtras } from \"./agUiExtras\";\nimport type { QueueItemState } from \"@assistant-ui/core/store\";\n\nconst EMPTY_INTERRUPTS: readonly AgUiInterrupt[] = [];\nconst EMPTY_QUEUE_ITEMS: readonly QueueItemState[] = Object.freeze([]);\nconst subscribeNoop = () => () => {};\n\nexport type AgUiAssistantRuntime = AssistantRuntime & {\n /**\n * @deprecated Use the `useAgUiInterrupts()` hook instead. This method is kept\n * for backward compatibility and will be removed in a future major release.\n */\n unstable_getPendingInterrupts: () => readonly AgUiInterrupt[];\n /**\n * @deprecated Use the `useAgUiSubmitInterruptResponses()` hook instead. This\n * method is kept for backward compatibility and will be removed in a future\n * major release.\n */\n unstable_submitInterruptResponses: (\n responses: readonly AgUiResumeEntry[],\n ) => Promise<void>;\n};\n\nexport function useAgUiRuntime(\n options: UseAgUiRuntimeOptions,\n): AgUiAssistantRuntime {\n const logger = useMemo(() => makeLogger(options.logger), [options.logger]);\n const [_version, setVersion] = useState(0);\n const notifyUpdate = useCallback(() => setVersion((v) => v + 1), []);\n const coreRef = useRef<AgUiThreadRuntimeCore | null>(null);\n const threadSwitchGenerationRef = useRef(0);\n const runtimeAdapters = useRuntimeAdapters();\n\n const historyAdapter = options.adapters?.history ?? runtimeAdapters?.history;\n const threadListAdapter = options.adapters?.threadList;\n\n if (!coreRef.current) {\n coreRef.current = new AgUiThreadRuntimeCore({\n agent: options.agent,\n logger,\n showThinking: options.showThinking ?? true,\n resumeTranscript: options.resumeTranscript,\n autoCancelPendingToolCalls: options.autoCancelPendingToolCalls,\n ...(options.onError && { onError: options.onError }),\n ...(options.onCancel && { onCancel: options.onCancel }),\n ...(historyAdapter && { history: historyAdapter }),\n notifyUpdate,\n });\n }\n\n const core = coreRef.current;\n useEffect(() => {\n core.updateOptions({\n agent: options.agent,\n logger,\n showThinking: options.showThinking ?? true,\n resumeTranscript: options.resumeTranscript,\n autoCancelPendingToolCalls: options.autoCancelPendingToolCalls,\n ...(options.onError && { onError: options.onError }),\n ...(options.onCancel && { onCancel: options.onCancel }),\n ...(historyAdapter && { history: historyAdapter }),\n });\n });\n\n const [toolStatuses, setToolStatuses] = useState<\n Record<string, ToolExecutionStatus>\n >({});\n\n const hasExecutingTools = Object.values(toolStatuses).some(\n (s) => s?.type === \"executing\",\n );\n const isRunning = core.isRunning() || hasExecutingTools;\n\n // The driver sends through the agent core rather than the runtime, because\n // the runtime's append routes every tail append back into this queue.\n const queueRef = useRef<MessageQueueController | null>(null);\n // Counts rendered busy edges, whether a run start or an interrupt arriving.\n // A dispatch whose count never moves was never observed as busy, so no\n // falling edge is coming to release the queue.\n const busyEdgesRef = useRef(0);\n if (options.unstable_enableMessageQueue && !queueRef.current) {\n queueRef.current = createMessageQueue({\n run: (message) => {\n const controller = queueRef.current;\n const edgesAtDispatch = busyEdgesRef.current;\n // The queue drops the item before dispatching and stays busy until an\n // idle edge. An append observed as busy is released by that falling\n // edge, and releasing again here would advance the queue twice; one\n // that never runs (a pre-run rejection, or startRun false) has to be\n // released here or every later send buffers forever. An interrupt is\n // rechecked because a run that resolves without yielding to React\n // reaches here before the effect below has seen either edge.\n const releaseIfNoRun = () => {\n if (\n queueRef.current !== controller ||\n busyEdgesRef.current !== edgesAtDispatch ||\n core.getPendingInterrupts() !== null\n )\n return;\n controller?.notifyIdle();\n };\n void core.append(message).then(releaseIfNoRun, (e: unknown) => {\n releaseIfNoRun();\n logger.error?.(\"[agui] queued message failed to send\", e);\n });\n },\n });\n }\n const queueController = options.unstable_enableMessageQueue\n ? queueRef.current\n : null;\n useLayoutEffect(() => {\n if (options.unstable_enableMessageQueue || !queueRef.current) return;\n const controller = queueRef.current;\n queueRef.current = null;\n controller.clear();\n }, [options.unstable_enableMessageQueue]);\n\n // Feeds the store memo below: the runtime core skips an adapter whose\n // identity is unchanged, so queue items have to move the store reference or\n // subscribers never see composer.queue change.\n const queueItems = useSyncExternalStore(\n queueController?.subscribe ?? subscribeNoop,\n () => queueController?.adapter.items ?? EMPTY_QUEUE_ITEMS,\n () => EMPTY_QUEUE_ITEMS,\n );\n const steerQueueItems = useSyncExternalStore(\n queueController?.subscribe ?? subscribeNoop,\n () => queueController?.adapter.steerItems ?? EMPTY_QUEUE_ITEMS,\n () => EMPTY_QUEUE_ITEMS,\n );\n\n // Holds the queue while client-side tools are still executing, and while an\n // interrupt is unanswered: `append` refuses a new run until it is resolved,\n // and the queue drops an item before dispatching it, so draining into that\n // refusal would lose the message instead of keeping it visible.\n const queueBusy = isRunning || core.getPendingInterrupts() !== null;\n useEffect(() => {\n if (queueBusy) {\n busyEdgesRef.current++;\n queueController?.notifyBusy();\n } else {\n queueController?.notifyIdle();\n }\n }, [queueBusy, queueController]);\n\n const threadList = useMemo(() => {\n if (!threadListAdapter) return undefined;\n\n const { onSwitchToNewThread, onSwitchToThread, ...rest } =\n threadListAdapter;\n\n return {\n ...rest,\n onSwitchToNewThread: onSwitchToNewThread\n ? async () => {\n const generation = ++threadSwitchGenerationRef.current;\n await onSwitchToNewThread();\n if (generation !== threadSwitchGenerationRef.current) return;\n core.applyExternalMessages([]);\n core.resetState();\n }\n : undefined,\n onSwitchToThread: onSwitchToThread\n ? async (threadId: string) => {\n const generation = ++threadSwitchGenerationRef.current;\n // Clear before the thread id flips, or the old messages leak\n // into the new thread as a sibling branch.\n core.applyExternalMessages([]);\n const result = await onSwitchToThread(threadId);\n if (generation !== threadSwitchGenerationRef.current) return;\n core.applyExternalMessages(result.messages);\n if (result.state !== undefined) {\n core.loadExternalState(result.state);\n } else {\n core.resetState();\n }\n if (result.unstable_resume) {\n void core.resumeInFlightRun(result.messages);\n }\n }\n : undefined,\n };\n }, [threadListAdapter, core]);\n\n const adapters = options.adapters;\n const adapterAdapters = useMemo(\n () => ({\n attachments: adapters?.attachments ?? runtimeAdapters?.attachments,\n speech: adapters?.speech,\n dictation: adapters?.dictation,\n voice: adapters?.voice,\n feedback: adapters?.feedback,\n threadList,\n }),\n [adapters, runtimeAdapters, threadList],\n );\n\n const shared = useExternalStoreSharedOptions(options);\n const store = useMemo(\n () => {\n void _version; // rerender on version change\n\n return {\n ...shared,\n isLoading: core.isLoading,\n messageRepository: core.getMessageRepository(),\n state: core.getState(),\n isRunning: core.isRunning(),\n extras: agUiExtras.provide({\n interrupts:\n core.getPendingInterrupts()?.interrupts ?? EMPTY_INTERRUPTS,\n sendA2uiAction: (action) => core.sendA2uiAction(action),\n submitInterruptResponses: (responses) =>\n core.submitInterruptResponses(responses),\n steerAway: (message, responses) => core.steerAway(message, responses),\n state: core.getState(),\n setState: (next) => core.setState(next),\n }),\n unstable_enableToolInvocations: true,\n setToolStatuses,\n onNew: (message: AppendMessage) => core.append(message),\n onEdit: (message: AppendMessage) => {\n queueController?.clear();\n return core.edit(message);\n },\n onReload: (parentId: string | null, config: { runConfig?: any }) => {\n queueController?.clear();\n return core.reload(parentId, config);\n },\n onCancel: async () => {\n queueController?.notifyCancelled();\n core.cancel();\n },\n onAddToolResult: (options) => core.addToolResult(options),\n onRespondToToolApproval: (options) =>\n core.respondToToolApproval(options).catch((error: unknown) => {\n core.reportError(error);\n throw error;\n }),\n onResume: (config) => core.resume(config),\n setMessages: (messages: readonly ThreadMessage[]) =>\n core.applyExternalMessages(messages),\n onImport: (messages: readonly ThreadMessage[]) =>\n core.applyExternalMessages(messages),\n onLoadExternalState: (state: ReadonlyJSONValue) =>\n core.loadExternalState(state),\n adapters: adapterAdapters,\n ...(queueController && { queue: queueController.adapter }),\n } satisfies ExternalStoreAdapter<ThreadMessage>;\n },\n // _version is intentionally included to trigger re-computation when core state changes via notifyUpdate\n // toolInvocations intentionally excluded: abort/resume use refs internally and work with stale captures\n [\n adapterAdapters,\n core,\n _version,\n isRunning,\n queueController,\n queueItems,\n steerQueueItems,\n shared,\n ],\n );\n\n const baseRuntime = useExternalStoreRuntime(store);\n\n const runtime = useMemo<AgUiAssistantRuntime>(() => {\n const wrapper = Object.create(baseRuntime) as AgUiAssistantRuntime;\n wrapper.unstable_getPendingInterrupts = () =>\n core.getPendingInterrupts()?.interrupts ?? [];\n wrapper.unstable_submitInterruptResponses = (responses) =>\n core.submitInterruptResponses(responses);\n return wrapper;\n }, [baseRuntime, core]);\n\n useEffect(() => {\n core.attachRuntime(runtime);\n return () => {\n core.detachRuntime();\n };\n }, [core, runtime]);\n\n useEffect(() => {\n core.__internal_load();\n }, [core]);\n\n return runtime;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"useAgUiRuntime.js","names":[],"sources":["../src/useAgUiRuntime.ts"],"sourcesContent":["\"use client\";\n\nimport {\n useCallback,\n useEffect,\n useLayoutEffect,\n useMemo,\n useRef,\n useState,\n useSyncExternalStore,\n} from \"react\";\nimport {\n useExternalStoreRuntime,\n useExternalStoreSharedOptions,\n useRuntimeAdapters,\n} from \"@assistant-ui/core/react\";\nimport { createMessageQueue } from \"@assistant-ui/core\";\nimport type {\n MessageQueueController,\n ToolExecutionStatus,\n} from \"@assistant-ui/core\";\nimport type {\n AssistantRuntime,\n AppendMessage,\n ExternalStoreAdapter,\n ThreadMessage,\n} from \"@assistant-ui/core\";\nimport type { ReadonlyJSONValue } from \"assistant-stream/utils\";\nimport { makeLogger } from \"./runtime/logger\";\nimport type {\n AgUiInterrupt,\n AgUiResumeEntry,\n UseAgUiRuntimeOptions,\n} from \"./runtime/types\";\nimport { AgUiThreadRuntimeCore } from \"./runtime/AgUiThreadRuntimeCore\";\nimport { agUiExtras } from \"./agUiExtras\";\nimport type { QueueItemState } from \"@assistant-ui/core/store\";\n\nconst EMPTY_INTERRUPTS: readonly AgUiInterrupt[] = [];\nconst EMPTY_QUEUE_ITEMS: readonly QueueItemState[] = Object.freeze([]);\nconst subscribeNoop = () => () => {};\n\nexport type AgUiAssistantRuntime = AssistantRuntime & {\n /**\n * @deprecated Use the `useAgUiInterrupts()` hook instead. This method is kept\n * for backward compatibility and will be removed in a future major release.\n */\n unstable_getPendingInterrupts: () => readonly AgUiInterrupt[];\n /**\n * @deprecated Use the `useAgUiSubmitInterruptResponses()` hook instead. This\n * method is kept for backward compatibility and will be removed in a future\n * major release.\n */\n unstable_submitInterruptResponses: (\n responses: readonly AgUiResumeEntry[],\n ) => Promise<void>;\n};\n\nexport function useAgUiRuntime(\n options: UseAgUiRuntimeOptions,\n): AgUiAssistantRuntime {\n const logger = useMemo(() => makeLogger(options.logger), [options.logger]);\n const [_version, setVersion] = useState(0);\n const notifyUpdate = useCallback(() => setVersion((v) => v + 1), []);\n const coreRef = useRef<AgUiThreadRuntimeCore | null>(null);\n const threadSwitchGenerationRef = useRef(0);\n const runtimeAdapters = useRuntimeAdapters();\n\n const historyAdapter = options.adapters?.history ?? runtimeAdapters?.history;\n const threadListAdapter = options.adapters?.threadList;\n\n if (!coreRef.current) {\n coreRef.current = new AgUiThreadRuntimeCore({\n agent: options.agent,\n logger,\n showThinking: options.showThinking ?? true,\n resumeTranscript: options.resumeTranscript,\n autoCancelPendingToolCalls: options.autoCancelPendingToolCalls,\n ...(options.onError && { onError: options.onError }),\n ...(options.onCancel && { onCancel: options.onCancel }),\n ...(historyAdapter && { history: historyAdapter }),\n notifyUpdate,\n });\n }\n\n const core = coreRef.current;\n useEffect(() => {\n core.updateOptions({\n agent: options.agent,\n logger,\n showThinking: options.showThinking ?? true,\n resumeTranscript: options.resumeTranscript,\n autoCancelPendingToolCalls: options.autoCancelPendingToolCalls,\n ...(options.onError && { onError: options.onError }),\n ...(options.onCancel && { onCancel: options.onCancel }),\n ...(historyAdapter && { history: historyAdapter }),\n });\n });\n\n const [toolStatuses, setToolStatuses] = useState<\n Record<string, ToolExecutionStatus>\n >({});\n\n const hasExecutingTools = Object.values(toolStatuses).some(\n (s) => s?.type === \"executing\",\n );\n const isRunning = core.isRunning() || hasExecutingTools;\n\n // The driver sends through the agent core rather than the runtime, because\n // the runtime's append routes every tail append back into this queue.\n const queueRef = useRef<MessageQueueController | null>(null);\n // Counts rendered busy edges, whether a run start or an interrupt arriving.\n // A dispatch whose count never moves was never observed as busy, so no\n // falling edge is coming to release the queue.\n const busyEdgesRef = useRef(0);\n if (options.unstable_enableMessageQueue && !queueRef.current) {\n queueRef.current = createMessageQueue({\n run: (message) => {\n const controller = queueRef.current;\n const edgesAtDispatch = busyEdgesRef.current;\n // The queue drops the item before dispatching and stays busy until an\n // idle edge. An append observed as busy is released by that falling\n // edge, and releasing again here would advance the queue twice; one\n // that never runs (a pre-run rejection, or startRun false) has to be\n // released here or every later send buffers forever. An interrupt is\n // rechecked because a run that resolves without yielding to React\n // reaches here before the effect below has seen either edge.\n const releaseIfNoRun = () => {\n if (\n queueRef.current !== controller ||\n busyEdgesRef.current !== edgesAtDispatch ||\n core.getPendingInterrupts() !== null\n )\n return;\n controller?.notifyIdle();\n };\n void core.append(message).then(releaseIfNoRun, (e: unknown) => {\n releaseIfNoRun();\n logger.error?.(\"[agui] queued message failed to send\", e);\n });\n },\n });\n }\n const queueController = options.unstable_enableMessageQueue\n ? queueRef.current\n : null;\n useLayoutEffect(() => {\n if (options.unstable_enableMessageQueue || !queueRef.current) return;\n const controller = queueRef.current;\n queueRef.current = null;\n controller.clear();\n }, [options.unstable_enableMessageQueue]);\n\n // Feeds the store memo below: the runtime core skips an adapter whose\n // identity is unchanged, so queue items have to move the store reference or\n // subscribers never see composer.queue change.\n const queueItems = useSyncExternalStore(\n queueController?.subscribe ?? subscribeNoop,\n () => queueController?.adapter.items ?? EMPTY_QUEUE_ITEMS,\n () => EMPTY_QUEUE_ITEMS,\n );\n const steerQueueItems = useSyncExternalStore(\n queueController?.subscribe ?? subscribeNoop,\n () => queueController?.adapter.steerItems ?? EMPTY_QUEUE_ITEMS,\n () => EMPTY_QUEUE_ITEMS,\n );\n\n // Holds the queue while client-side tools are still executing, and while an\n // interrupt is unanswered: `append` refuses a new run until it is resolved,\n // and the queue drops an item before dispatching it, so draining into that\n // refusal would lose the message instead of keeping it visible.\n const queueBusy = isRunning || core.getPendingInterrupts() !== null;\n useEffect(() => {\n if (queueBusy) {\n busyEdgesRef.current++;\n queueController?.notifyBusy();\n } else {\n queueController?.notifyIdle();\n }\n }, [queueBusy, queueController]);\n\n const threadList = useMemo(() => {\n if (!threadListAdapter) return undefined;\n\n const { onSwitchToNewThread, onSwitchToThread, ...rest } =\n threadListAdapter;\n\n return {\n ...rest,\n onSwitchToNewThread: onSwitchToNewThread\n ? async () => {\n const generation = ++threadSwitchGenerationRef.current;\n // Clear before the thread id flips, or the old messages leak\n // into the new thread as a sibling branch.\n core.applyExternalMessages([]);\n core.resetThreadState();\n await onSwitchToNewThread();\n if (generation !== threadSwitchGenerationRef.current) return;\n core.applyExternalMessages([]);\n core.resetThreadState();\n }\n : undefined,\n onSwitchToThread: onSwitchToThread\n ? async (threadId: string) => {\n const generation = ++threadSwitchGenerationRef.current;\n // Clear before the thread id flips, or the old messages leak\n // into the new thread as a sibling branch.\n core.applyExternalMessages([]);\n core.resetThreadState();\n const result = await onSwitchToThread(threadId);\n if (generation !== threadSwitchGenerationRef.current) return;\n core.applyExternalMessages([]);\n core.resetThreadState();\n core.applyExternalMessages(result.messages);\n if (result.state !== undefined) {\n core.loadExternalState(result.state);\n }\n if (result.unstable_resume) {\n void core.resumeInFlightRun(result.messages);\n }\n }\n : undefined,\n };\n }, [threadListAdapter, core]);\n\n const adapters = options.adapters;\n const adapterAdapters = useMemo(\n () => ({\n attachments: adapters?.attachments ?? runtimeAdapters?.attachments,\n speech: adapters?.speech,\n dictation: adapters?.dictation,\n voice: adapters?.voice,\n feedback: adapters?.feedback,\n threadList,\n }),\n [adapters, runtimeAdapters, threadList],\n );\n\n const shared = useExternalStoreSharedOptions(options);\n const store = useMemo(\n () => {\n void _version; // rerender on version change\n\n return {\n ...shared,\n isLoading: core.isLoading,\n messageRepository: core.getMessageRepository(),\n state: core.getState(),\n isRunning: core.isRunning(),\n extras: agUiExtras.provide({\n interrupts:\n core.getPendingInterrupts()?.interrupts ?? EMPTY_INTERRUPTS,\n sendA2uiAction: (action) => core.sendA2uiAction(action),\n submitInterruptResponses: (responses) =>\n core.submitInterruptResponses(responses),\n steerAway: (message, responses) => core.steerAway(message, responses),\n state: core.getState(),\n setState: (next) => core.setState(next),\n }),\n unstable_enableToolInvocations: true,\n unstable_persistsHistory: true,\n setToolStatuses,\n onNew: (message: AppendMessage) => core.append(message),\n onVoiceTranscript: (message) => core.appendVoiceTranscript(message),\n onEdit: (message: AppendMessage) => {\n queueController?.clear();\n return core.edit(message);\n },\n onReload: (parentId: string | null, config: { runConfig?: any }) => {\n queueController?.clear();\n return core.reload(parentId, config);\n },\n onCancel: async () => {\n queueController?.notifyCancelled();\n core.cancel();\n },\n onAddToolResult: (options) => core.addToolResult(options),\n unstable_onRecordToolInteraction: (options) =>\n core.recordToolInteraction(options),\n onRespondToToolApproval: (options) =>\n core.respondToToolApproval(options).catch((error: unknown) => {\n core.reportError(error);\n throw error;\n }),\n onResume: (config) => core.resume(config),\n setMessages: (messages: readonly ThreadMessage[]) =>\n core.applyExternalMessages(messages),\n onImport: (messages: readonly ThreadMessage[]) =>\n core.applyExternalMessages(messages),\n onLoadExternalState: (state: ReadonlyJSONValue) =>\n core.loadExternalState(state),\n adapters: adapterAdapters,\n ...(queueController && { queue: queueController.adapter }),\n } satisfies ExternalStoreAdapter<ThreadMessage>;\n },\n // _version is intentionally included to trigger re-computation when core state changes via notifyUpdate\n // toolInvocations intentionally excluded: abort/resume use refs internally and work with stale captures\n [\n adapterAdapters,\n core,\n _version,\n isRunning,\n queueController,\n queueItems,\n steerQueueItems,\n shared,\n ],\n );\n\n const baseRuntime = useExternalStoreRuntime(store);\n\n const runtime = useMemo<AgUiAssistantRuntime>(() => {\n const wrapper = Object.create(baseRuntime) as AgUiAssistantRuntime;\n wrapper.unstable_getPendingInterrupts = () =>\n core.getPendingInterrupts()?.interrupts ?? [];\n wrapper.unstable_submitInterruptResponses = (responses) =>\n core.submitInterruptResponses(responses);\n return wrapper;\n }, [baseRuntime, core]);\n\n useEffect(() => {\n core.attachRuntime(runtime);\n return () => {\n core.detachRuntime();\n };\n }, [core, runtime]);\n\n useEffect(() => {\n core.__internal_load();\n }, [core]);\n\n return runtime;\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@assistant-ui/react-ag-ui",
3
- "version": "0.0.59",
3
+ "version": "0.0.61",
4
4
  "description": "AG-UI protocol adapter for assistant-ui",
5
5
  "keywords": [
6
6
  "ag-ui",
@@ -30,10 +30,10 @@
30
30
  "sideEffects": false,
31
31
  "dependencies": {
32
32
  "@ag-ui/client": "^0.0.59",
33
- "@assistant-ui/core": "^0.3.18",
34
- "@assistant-ui/react-generative-ui": "^0.0.18",
35
- "@assistant-ui/store": "^0.3.13",
36
- "assistant-stream": "^0.3.42",
33
+ "@assistant-ui/core": "^0.3.21",
34
+ "@assistant-ui/react-generative-ui": "^0.0.20",
35
+ "@assistant-ui/store": "^0.3.15",
36
+ "assistant-stream": "^0.3.45",
37
37
  "fast-json-patch": "^3.1.1"
38
38
  },
39
39
  "peerDependencies": {
@@ -46,14 +46,14 @@
46
46
  }
47
47
  },
48
48
  "devDependencies": {
49
- "@assistant-ui/x-buildutils": "0.0.26",
50
- "@testing-library/dom": "^10.4.1",
49
+ "@assistant-ui/x-buildutils": "0.0.29",
50
+ "@testing-library/dom": "^10.4.2",
51
51
  "@testing-library/react": "^16.3.3",
52
- "@types/react": "^19.2.18",
52
+ "@types/react": "^19.3.0",
53
53
  "jsdom": "^30.0.1",
54
- "react": "^19.2.8",
55
- "vitest": "^4.1.11",
56
- "zod": "^4.5.4"
54
+ "react": "^19.3.0",
55
+ "vitest": "^5.0.1",
56
+ "zod": "^4.6.5"
57
57
  },
58
58
  "publishConfig": {
59
59
  "access": "public",
@@ -71,6 +71,7 @@
71
71
  "scripts": {
72
72
  "build": "aui-build",
73
73
  "test": "vitest run",
74
+ "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json",
74
75
  "test:watch": "vitest"
75
76
  }
76
77
  }
@@ -19,8 +19,10 @@ import type {
19
19
  ThreadHistoryAdapter,
20
20
  ThreadMessage,
21
21
  ToolCallMessagePart,
22
+ Unstable_ToolInteraction,
22
23
  } from "@assistant-ui/core";
23
24
  import {
25
+ appendToolInteraction,
24
26
  createMessageRepositorySession,
25
27
  invokeUserCallback,
26
28
  iterateToolCallParts,
@@ -329,6 +331,14 @@ export class AgUiThreadRuntimeCore {
329
331
  await this.startRun(threadMessageId, message.runConfig);
330
332
  }
331
333
 
334
+ appendVoiceTranscript(message: ThreadMessage): void {
335
+ const parentId = this.session.headId;
336
+ this.session.addOrUpdateMessage(parentId, message);
337
+ this.session.switchToBranch(message.id);
338
+ this.notifyUpdate();
339
+ this.recordHistoryEntry(parentId, message);
340
+ }
341
+
332
342
  private maybeAutoCancelPendingToolCalls(): void {
333
343
  if (this.autoCancelPendingToolCalls === false) return;
334
344
  const pending = this.getPendingToolCalls();
@@ -871,11 +881,17 @@ export class AgUiThreadRuntimeCore {
871
881
  const { content } = mapToolCallPartsDeep(assistant.content, (part) => {
872
882
  if (part.toolCallId !== options.toolCallId) return part;
873
883
  matchedToolCall = true;
884
+ // artifact and modelContent are optional; only override when supplied
885
+ // so a later result that omits them keeps a stored value, and
886
+ // emitToolResult can forward the model-facing content to the agent.
874
887
  return {
875
888
  ...part,
876
889
  result: options.result,
877
- artifact: options.artifact,
878
890
  isError: options.isError,
891
+ ...(options.artifact !== undefined && { artifact: options.artifact }),
892
+ ...(options.modelContent !== undefined && {
893
+ modelContent: options.modelContent,
894
+ }),
879
895
  };
880
896
  });
881
897
  if (!matchedToolCall) return message;
@@ -887,6 +903,67 @@ export class AgUiThreadRuntimeCore {
887
903
  this.maybeResumeAfterToolResults(sessionMessageId);
888
904
  }
889
905
 
906
+ async recordToolInteraction(options: {
907
+ messageId: string;
908
+ toolCallId: string;
909
+ interaction: Unstable_ToolInteraction;
910
+ }): Promise<void> {
911
+ const sessionMessageId = this.session.tryGetMessage(options.messageId)
912
+ ? options.messageId
913
+ : this.findMessageIdForToolCall(options.toolCallId);
914
+ if (sessionMessageId === undefined) {
915
+ throw new Error(
916
+ `[agui] recordToolInteraction: message "${options.messageId}" was not found`,
917
+ );
918
+ }
919
+
920
+ const item = this.session.tryGetMessage(sessionMessageId);
921
+ if (!item) {
922
+ throw new Error(
923
+ `[agui] recordToolInteraction: message "${sessionMessageId}" was not found`,
924
+ );
925
+ }
926
+
927
+ let recorded = false;
928
+ const updated = this.session.updateMessage(sessionMessageId, (message) => {
929
+ if (message.role !== "assistant") return message;
930
+ const assistant = message as ThreadAssistantMessage;
931
+ const { content } = mapToolCallPartsDeep(assistant.content, (part) => {
932
+ if (part.toolCallId !== options.toolCallId) return part;
933
+ recorded = true;
934
+ return {
935
+ ...part,
936
+ unstable_interactions: appendToolInteraction(
937
+ part.unstable_interactions,
938
+ options.interaction,
939
+ ),
940
+ };
941
+ });
942
+ return recorded ? { ...assistant, content } : assistant;
943
+ });
944
+ if (!recorded || !updated) {
945
+ throw new Error(
946
+ `[agui] recordToolInteraction: tool call "${options.toolCallId}" was not found on message "${options.messageId}"`,
947
+ );
948
+ }
949
+
950
+ this.notifyUpdate();
951
+
952
+ const message = this.session.tryGetMessage(sessionMessageId)?.message;
953
+ const history = this.history;
954
+ if (
955
+ !message ||
956
+ !history?.update ||
957
+ !this.isPersistableStatus(message.status)
958
+ ) {
959
+ return;
960
+ }
961
+
962
+ await this.chainHistoryWrite(sessionMessageId, () =>
963
+ history.update!({ parentId: item.parentId, message }),
964
+ );
965
+ }
966
+
890
967
  sendA2uiAction(action: Record<string, unknown>): void {
891
968
  this.assertNoPendingInterrupts();
892
969
  this.maybeAutoCancelPendingToolCalls();
@@ -973,6 +1050,17 @@ export class AgUiThreadRuntimeCore {
973
1050
  }
974
1051
 
975
1052
  applyExternalMessages(messages: readonly ThreadMessage[]): void {
1053
+ messages = messages.map((message) => {
1054
+ if (message.role === "system" || message.metadata.modality !== undefined)
1055
+ return message;
1056
+ const modality = this.session.tryGetMessage(message.id)?.message.metadata
1057
+ .modality;
1058
+ if (modality === undefined) return message;
1059
+ return {
1060
+ ...message,
1061
+ metadata: { ...message.metadata, modality },
1062
+ } as ThreadMessage;
1063
+ });
976
1064
  this.pendingA2uiResumeOwner = null;
977
1065
  this.pendingA2uiAction = undefined;
978
1066
  this.assistantHistoryParents.clear();
@@ -1053,6 +1141,31 @@ export class AgUiThreadRuntimeCore {
1053
1141
  this.notifyUpdate();
1054
1142
  }
1055
1143
 
1144
+ resetThreadState(): void {
1145
+ const controller = this.abortController;
1146
+ const activeRunAgent = this.activeRunAgent;
1147
+
1148
+ this.stateSnapshot = undefined;
1149
+ this.pendingResume = null;
1150
+ this.pendingA2uiResumeOwner = null;
1151
+ this.pendingA2uiAction = undefined;
1152
+
1153
+ if (controller) {
1154
+ this.abortController = null;
1155
+ this.activeRunAgent = null;
1156
+ this.setRunning(false);
1157
+ try {
1158
+ (activeRunAgent ?? this.agent).abortRun();
1159
+ } catch (error) {
1160
+ this.logger.error?.("[agui] agent abortRun failed", error);
1161
+ } finally {
1162
+ controller.abort();
1163
+ }
1164
+ } else {
1165
+ this.notifyUpdate();
1166
+ }
1167
+ }
1168
+
1056
1169
  private async startRun(
1057
1170
  parentId: string | null,
1058
1171
  runConfig?: RunConfig,
@@ -1171,6 +1284,8 @@ export class AgUiThreadRuntimeCore {
1171
1284
  }
1172
1285
  };
1173
1286
 
1287
+ const abortController = new AbortController();
1288
+ const abortSignal = abortController.signal;
1174
1289
  const aggregator = new RunAggregator({
1175
1290
  showThinking: this.showThinking,
1176
1291
  logger: this.logger,
@@ -1180,11 +1295,18 @@ export class AgUiThreadRuntimeCore {
1180
1295
  },
1181
1296
  onTextMessageStart: (serverId) => adoptServerMessageId(serverId, true),
1182
1297
  });
1183
- const dispatch = (event: AgUiEvent) =>
1184
- this.handleEvent(aggregator, event, assistantMessageId);
1298
+ const dispatch = (event: AgUiEvent) => {
1299
+ if (this.abortController !== abortController) return;
1300
+ const nextAssistantMessageId = this.handleEvent(
1301
+ aggregator,
1302
+ event,
1303
+ assistantMessageId,
1304
+ );
1305
+ if (event.type === "MESSAGES_SNAPSHOT") {
1306
+ assistantMessageId = nextAssistantMessageId;
1307
+ }
1308
+ };
1185
1309
 
1186
- const abortController = new AbortController();
1187
- const abortSignal = abortController.signal;
1188
1310
  this.abortController = abortController;
1189
1311
  const runAgentInstance = this.agent;
1190
1312
  this.activeRunAgent = runAgentInstance;
@@ -1585,26 +1707,57 @@ export class AgUiThreadRuntimeCore {
1585
1707
  previous: ThreadAssistantMessage["content"],
1586
1708
  next: ThreadAssistantMessage["content"],
1587
1709
  ): ThreadAssistantMessage["content"] {
1588
- const resolved = new Map<string, ToolCallMessagePart>();
1710
+ const preserved = new Map<string, ToolCallMessagePart>();
1589
1711
  for (const part of iterateToolCallParts(previous)) {
1590
1712
  if (isResolvedToolCall(part)) {
1591
- resolved.set(part.toolCallId, part);
1713
+ preserved.set(part.toolCallId, part);
1592
1714
  }
1593
1715
  }
1594
- if (resolved.size === 0) return next;
1716
+ if (preserved.size === 0) {
1717
+ return this.preserveToolInteractions(previous, next);
1718
+ }
1595
1719
 
1596
1720
  const { content: merged, changed } = mapToolCallPartsDeep(next, (part) => {
1597
- if (isResolvedToolCall(part)) return part;
1598
- const prior = resolved.get(part.toolCallId);
1721
+ const prior = preserved.get(part.toolCallId);
1599
1722
  if (!prior) return part;
1600
- return {
1601
- ...part,
1602
- result: prior.result,
1603
- ...(prior.artifact !== undefined ? { artifact: prior.artifact } : {}),
1604
- ...(prior.isError !== undefined ? { isError: prior.isError } : {}),
1605
- };
1723
+ if (!isResolvedToolCall(part) && isResolvedToolCall(prior)) {
1724
+ return {
1725
+ ...part,
1726
+ result: prior.result,
1727
+ ...(prior.artifact !== undefined ? { artifact: prior.artifact } : {}),
1728
+ ...(prior.isError !== undefined ? { isError: prior.isError } : {}),
1729
+ ...(prior.modelContent !== undefined
1730
+ ? { modelContent: prior.modelContent }
1731
+ : {}),
1732
+ };
1733
+ }
1734
+ return part;
1735
+ });
1736
+ return this.preserveToolInteractions(previous, changed ? merged : next);
1737
+ }
1738
+
1739
+ private preserveToolInteractions(
1740
+ previous: ThreadAssistantMessage["content"],
1741
+ next: ThreadAssistantMessage["content"],
1742
+ ): ThreadAssistantMessage["content"] {
1743
+ const interactions = new Map<
1744
+ string,
1745
+ NonNullable<ToolCallMessagePart["unstable_interactions"]>
1746
+ >();
1747
+ for (const part of iterateToolCallParts(previous)) {
1748
+ if (part.unstable_interactions !== undefined) {
1749
+ interactions.set(part.toolCallId, part.unstable_interactions);
1750
+ }
1751
+ }
1752
+ if (interactions.size === 0) return next;
1753
+
1754
+ const { content, changed } = mapToolCallPartsDeep(next, (part) => {
1755
+ const interaction = interactions.get(part.toolCallId);
1756
+ if (interaction === undefined || part.unstable_interactions !== undefined)
1757
+ return part;
1758
+ return { ...part, unstable_interactions: interaction };
1606
1759
  });
1607
- return changed ? merged : next;
1760
+ return changed ? content : next;
1608
1761
  }
1609
1762
 
1610
1763
  private mergeAssistantMetadata(
@@ -1640,15 +1793,15 @@ export class AgUiThreadRuntimeCore {
1640
1793
  aggregator: RunAggregator,
1641
1794
  event: AgUiEvent,
1642
1795
  activeAssistantId: string | undefined,
1643
- ) {
1796
+ ): string | undefined {
1644
1797
  switch (event.type) {
1645
1798
  case "STATE_SNAPSHOT": {
1646
1799
  this.stateSnapshot = event.snapshot as ReadonlyJSONValue;
1647
- this.notifyUpdate();
1648
- return;
1800
+ this.updateActiveAssistantState(activeAssistantId);
1801
+ break;
1649
1802
  }
1650
1803
  case "STATE_DELTA": {
1651
- if (event.delta.length === 0) return;
1804
+ if (event.delta.length === 0) break;
1652
1805
  try {
1653
1806
  const state = this.stateSnapshot ?? {};
1654
1807
  const result = jsonpatch.applyPatch(
@@ -1658,26 +1811,44 @@ export class AgUiThreadRuntimeCore {
1658
1811
  /* mutateDocument */ false,
1659
1812
  );
1660
1813
  this.stateSnapshot = result.newDocument as ReadonlyJSONValue;
1661
- this.notifyUpdate();
1814
+ this.updateActiveAssistantState(activeAssistantId);
1662
1815
  } catch (error) {
1663
1816
  this.logger.error?.("[agui] failed to apply state delta", error);
1664
1817
  }
1665
- return;
1818
+ break;
1666
1819
  }
1667
1820
  case "MESSAGES_SNAPSHOT": {
1821
+ const previousHeadId = this.session.headId;
1822
+ const activeAssistant =
1823
+ activeAssistantId === undefined
1824
+ ? undefined
1825
+ : this.session.tryGetMessage(activeAssistantId)?.message;
1826
+ const hasActiveText =
1827
+ activeAssistant?.role === "assistant" &&
1828
+ activeAssistant.content.some((part) => part.type === "text");
1668
1829
  this.importMessagesSnapshot(event.messages, activeAssistantId);
1669
- return;
1830
+ const headId = this.session.headId;
1831
+ if (hasActiveText && headId !== previousHeadId) {
1832
+ const head =
1833
+ headId === null
1834
+ ? undefined
1835
+ : this.session.tryGetMessage(headId)?.message;
1836
+ activeAssistantId =
1837
+ head?.role === "assistant" ? (headId ?? undefined) : undefined;
1838
+ }
1839
+ this.updateActiveAssistantState(activeAssistantId);
1840
+ break;
1670
1841
  }
1671
1842
  case "TOOL_CALL_RESULT": {
1672
1843
  if (!aggregator.hasToolCall(event.toolCallId)) {
1673
1844
  const messageId = this.findMessageIdForToolCall(event.toolCallId);
1674
1845
  if (messageId !== undefined) {
1675
1846
  this.applyCrossRunToolResult(messageId, event);
1676
- return;
1847
+ break;
1677
1848
  }
1678
1849
  }
1679
1850
  aggregator.handle(event);
1680
- return;
1851
+ break;
1681
1852
  }
1682
1853
  case "ACTIVITY_SNAPSHOT": {
1683
1854
  const toolCallId = event.content["toolCallId"];
@@ -1689,15 +1860,41 @@ export class AgUiThreadRuntimeCore {
1689
1860
  const messageId = this.findMessageIdForToolCall(toolCallId);
1690
1861
  if (messageId !== undefined) {
1691
1862
  this.applyCrossRunActivitySnapshot(messageId, toolCallId, event);
1692
- return;
1863
+ break;
1693
1864
  }
1694
1865
  }
1695
1866
  aggregator.handle(event);
1696
- return;
1867
+ break;
1697
1868
  }
1698
1869
  default:
1699
1870
  aggregator.handle(event);
1700
1871
  }
1872
+ return activeAssistantId;
1873
+ }
1874
+
1875
+ private updateActiveAssistantState(messageId: string | undefined): void {
1876
+ const current =
1877
+ messageId === undefined
1878
+ ? undefined
1879
+ : this.session.tryGetMessage(messageId)?.message;
1880
+ const activeAssistantId =
1881
+ current?.role === "assistant" && this.session.headId === messageId
1882
+ ? messageId
1883
+ : this.session.headId;
1884
+ if (activeAssistantId !== null && activeAssistantId !== undefined) {
1885
+ this.session.updateMessage(activeAssistantId, (message) => {
1886
+ if (message.role !== "assistant") return message;
1887
+ const assistant = message as ThreadAssistantMessage;
1888
+ return {
1889
+ ...assistant,
1890
+ metadata: {
1891
+ ...assistant.metadata,
1892
+ unstable_state: this.stateSnapshot ?? null,
1893
+ },
1894
+ };
1895
+ });
1896
+ }
1897
+ this.notifyUpdate();
1701
1898
  }
1702
1899
 
1703
1900
  private applyCrossRunToolResult(
@@ -1851,9 +2048,28 @@ export class AgUiThreadRuntimeCore {
1851
2048
  const converted: ThreadMessage[] = [];
1852
2049
  for (const message of normalized) {
1853
2050
  try {
1854
- converted.push(
1855
- fromThreadMessageLike(message, generateId(), FALLBACK_USER_STATUS),
2051
+ const convertedMessage = fromThreadMessageLike(
2052
+ message,
2053
+ generateId(),
2054
+ FALLBACK_USER_STATUS,
1856
2055
  );
2056
+ const existing = this.session.tryGetMessage(
2057
+ convertedMessage.id,
2058
+ )?.message;
2059
+ if (
2060
+ convertedMessage.role === "assistant" &&
2061
+ existing?.role === "assistant"
2062
+ ) {
2063
+ converted.push({
2064
+ ...convertedMessage,
2065
+ content: this.preserveToolInteractions(
2066
+ existing.content,
2067
+ convertedMessage.content,
2068
+ ),
2069
+ });
2070
+ } else {
2071
+ converted.push(convertedMessage);
2072
+ }
1857
2073
  } catch (error) {
1858
2074
  this.logger.error?.(
1859
2075
  "[agui] failed to import message from snapshot",
@@ -276,3 +276,59 @@ describe("toAgUiMessages content metadata", () => {
276
276
  expect(toAgUiMessages(rebuilt as never)).toEqual(sent);
277
277
  });
278
278
  });
279
+
280
+ describe("toAgUiMessages percent-encoded data URLs", () => {
281
+ it("sends a data URL that is not base64 as a url source", () => {
282
+ expect(
283
+ contentOf(
284
+ userMessage([
285
+ { type: "image", image: "data:image/svg+xml,%3Csvg%2F%3E" },
286
+ {
287
+ type: "file",
288
+ data: "data:text/plain,hello",
289
+ mimeType: "text/plain",
290
+ filename: "f.txt",
291
+ },
292
+ ]),
293
+ ),
294
+ ).toEqual([
295
+ {
296
+ type: "image",
297
+ source: { type: "url", value: "data:image/svg+xml,%3Csvg%2F%3E" },
298
+ },
299
+ {
300
+ type: "document",
301
+ source: {
302
+ type: "url",
303
+ value: "data:text/plain,hello",
304
+ mimeType: "text/plain",
305
+ },
306
+ metadata: { filename: "f.txt" },
307
+ },
308
+ ]);
309
+ });
310
+
311
+ it("keeps a data URL that is not base64 through a snapshot round trip", () => {
312
+ const sent = toAgUiMessages([
313
+ userMessage([
314
+ {
315
+ type: "file",
316
+ data: "data:text/plain,hello",
317
+ mimeType: "text/plain",
318
+ filename: "f.txt",
319
+ },
320
+ ]),
321
+ ]);
322
+
323
+ const rebuilt = fromAgUiMessages(sent as never);
324
+ const attachment = (rebuilt[0] as unknown as { attachments: unknown[] })
325
+ .attachments[0] as { content: unknown[] };
326
+
327
+ expect(attachment.content[0]).toMatchObject({
328
+ type: "file",
329
+ data: "data:text/plain,hello",
330
+ mimeType: "text/plain",
331
+ });
332
+ expect(toAgUiMessages(rebuilt as never)).toEqual(sent);
333
+ });
334
+ });