@assistant-ui/react-ag-ui 0.0.60 → 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 +1 -3
  2. package/dist/agUiExtras.d.ts.map +1 -1
  3. package/dist/hooks.d.ts +3 -5
  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 +152 -144
  8. package/dist/runtime/AgUiThreadRuntimeCore.d.ts.map +1 -1
  9. package/dist/runtime/AgUiThreadRuntimeCore.js +141 -23
  10. package/dist/runtime/AgUiThreadRuntimeCore.js.map +1 -1
  11. package/dist/runtime/adapter/conversions.d.ts +51 -53
  12. package/dist/runtime/adapter/conversions.d.ts.map +1 -1
  13. package/dist/runtime/adapter/conversions.js +65 -21
  14. package/dist/runtime/adapter/conversions.js.map +1 -1
  15. package/dist/runtime/adapter/run-aggregator.d.ts +89 -88
  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 +107 -108
  20. package/dist/runtime/adapter/subscriber.d.ts.map +1 -1
  21. package/dist/runtime/adapter/tool-approval.d.ts +3 -4
  22. package/dist/runtime/adapter/tool-approval.d.ts.map +1 -1
  23. package/dist/runtime/event-parser.d.ts +3 -5
  24. package/dist/runtime/event-parser.d.ts.map +1 -1
  25. package/dist/runtime/interrupt-internals.d.ts +1 -3
  26. package/dist/runtime/interrupt-internals.d.ts.map +1 -1
  27. package/dist/runtime/logger.d.ts +2 -4
  28. package/dist/runtime/logger.d.ts.map +1 -1
  29. package/dist/runtime/mcp-tool-result.d.ts +4 -6
  30. package/dist/runtime/mcp-tool-result.d.ts.map +1 -1
  31. package/dist/runtime/types.d.ts +237 -238
  32. package/dist/runtime/types.d.ts.map +1 -1
  33. package/dist/useAgUiRuntime.d.ts +13 -15
  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 +6 -6
  38. package/src/runtime/AgUiThreadRuntimeCore.ts +239 -32
  39. package/src/runtime/adapter/conversions.content.test.ts +56 -0
  40. package/src/runtime/adapter/conversions.ts +112 -44
  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.60",
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.20",
34
- "@assistant-ui/react-generative-ui": "^0.0.19",
35
- "@assistant-ui/store": "^0.3.14",
36
- "assistant-stream": "^0.3.44",
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,7 +46,7 @@
46
46
  }
47
47
  },
48
48
  "devDependencies": {
49
- "@assistant-ui/x-buildutils": "0.0.28",
49
+ "@assistant-ui/x-buildutils": "0.0.29",
50
50
  "@testing-library/dom": "^10.4.2",
51
51
  "@testing-library/react": "^16.3.3",
52
52
  "@types/react": "^19.3.0",
@@ -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();
@@ -893,6 +903,67 @@ export class AgUiThreadRuntimeCore {
893
903
  this.maybeResumeAfterToolResults(sessionMessageId);
894
904
  }
895
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
+
896
967
  sendA2uiAction(action: Record<string, unknown>): void {
897
968
  this.assertNoPendingInterrupts();
898
969
  this.maybeAutoCancelPendingToolCalls();
@@ -979,6 +1050,17 @@ export class AgUiThreadRuntimeCore {
979
1050
  }
980
1051
 
981
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
+ });
982
1064
  this.pendingA2uiResumeOwner = null;
983
1065
  this.pendingA2uiAction = undefined;
984
1066
  this.assistantHistoryParents.clear();
@@ -1059,6 +1141,31 @@ export class AgUiThreadRuntimeCore {
1059
1141
  this.notifyUpdate();
1060
1142
  }
1061
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
+
1062
1169
  private async startRun(
1063
1170
  parentId: string | null,
1064
1171
  runConfig?: RunConfig,
@@ -1177,6 +1284,8 @@ export class AgUiThreadRuntimeCore {
1177
1284
  }
1178
1285
  };
1179
1286
 
1287
+ const abortController = new AbortController();
1288
+ const abortSignal = abortController.signal;
1180
1289
  const aggregator = new RunAggregator({
1181
1290
  showThinking: this.showThinking,
1182
1291
  logger: this.logger,
@@ -1186,11 +1295,18 @@ export class AgUiThreadRuntimeCore {
1186
1295
  },
1187
1296
  onTextMessageStart: (serverId) => adoptServerMessageId(serverId, true),
1188
1297
  });
1189
- const dispatch = (event: AgUiEvent) =>
1190
- 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
+ };
1191
1309
 
1192
- const abortController = new AbortController();
1193
- const abortSignal = abortController.signal;
1194
1310
  this.abortController = abortController;
1195
1311
  const runAgentInstance = this.agent;
1196
1312
  this.activeRunAgent = runAgentInstance;
@@ -1591,29 +1707,57 @@ export class AgUiThreadRuntimeCore {
1591
1707
  previous: ThreadAssistantMessage["content"],
1592
1708
  next: ThreadAssistantMessage["content"],
1593
1709
  ): ThreadAssistantMessage["content"] {
1594
- const resolved = new Map<string, ToolCallMessagePart>();
1710
+ const preserved = new Map<string, ToolCallMessagePart>();
1595
1711
  for (const part of iterateToolCallParts(previous)) {
1596
1712
  if (isResolvedToolCall(part)) {
1597
- resolved.set(part.toolCallId, part);
1713
+ preserved.set(part.toolCallId, part);
1598
1714
  }
1599
1715
  }
1600
- if (resolved.size === 0) return next;
1716
+ if (preserved.size === 0) {
1717
+ return this.preserveToolInteractions(previous, next);
1718
+ }
1601
1719
 
1602
1720
  const { content: merged, changed } = mapToolCallPartsDeep(next, (part) => {
1603
- if (isResolvedToolCall(part)) return part;
1604
- const prior = resolved.get(part.toolCallId);
1721
+ const prior = preserved.get(part.toolCallId);
1605
1722
  if (!prior) return part;
1606
- return {
1607
- ...part,
1608
- result: prior.result,
1609
- ...(prior.artifact !== undefined ? { artifact: prior.artifact } : {}),
1610
- ...(prior.isError !== undefined ? { isError: prior.isError } : {}),
1611
- ...(prior.modelContent !== undefined
1612
- ? { modelContent: prior.modelContent }
1613
- : {}),
1614
- };
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;
1615
1735
  });
1616
- return changed ? merged : next;
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 };
1759
+ });
1760
+ return changed ? content : next;
1617
1761
  }
1618
1762
 
1619
1763
  private mergeAssistantMetadata(
@@ -1649,15 +1793,15 @@ export class AgUiThreadRuntimeCore {
1649
1793
  aggregator: RunAggregator,
1650
1794
  event: AgUiEvent,
1651
1795
  activeAssistantId: string | undefined,
1652
- ) {
1796
+ ): string | undefined {
1653
1797
  switch (event.type) {
1654
1798
  case "STATE_SNAPSHOT": {
1655
1799
  this.stateSnapshot = event.snapshot as ReadonlyJSONValue;
1656
- this.notifyUpdate();
1657
- return;
1800
+ this.updateActiveAssistantState(activeAssistantId);
1801
+ break;
1658
1802
  }
1659
1803
  case "STATE_DELTA": {
1660
- if (event.delta.length === 0) return;
1804
+ if (event.delta.length === 0) break;
1661
1805
  try {
1662
1806
  const state = this.stateSnapshot ?? {};
1663
1807
  const result = jsonpatch.applyPatch(
@@ -1667,26 +1811,44 @@ export class AgUiThreadRuntimeCore {
1667
1811
  /* mutateDocument */ false,
1668
1812
  );
1669
1813
  this.stateSnapshot = result.newDocument as ReadonlyJSONValue;
1670
- this.notifyUpdate();
1814
+ this.updateActiveAssistantState(activeAssistantId);
1671
1815
  } catch (error) {
1672
1816
  this.logger.error?.("[agui] failed to apply state delta", error);
1673
1817
  }
1674
- return;
1818
+ break;
1675
1819
  }
1676
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");
1677
1829
  this.importMessagesSnapshot(event.messages, activeAssistantId);
1678
- 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;
1679
1841
  }
1680
1842
  case "TOOL_CALL_RESULT": {
1681
1843
  if (!aggregator.hasToolCall(event.toolCallId)) {
1682
1844
  const messageId = this.findMessageIdForToolCall(event.toolCallId);
1683
1845
  if (messageId !== undefined) {
1684
1846
  this.applyCrossRunToolResult(messageId, event);
1685
- return;
1847
+ break;
1686
1848
  }
1687
1849
  }
1688
1850
  aggregator.handle(event);
1689
- return;
1851
+ break;
1690
1852
  }
1691
1853
  case "ACTIVITY_SNAPSHOT": {
1692
1854
  const toolCallId = event.content["toolCallId"];
@@ -1698,15 +1860,41 @@ export class AgUiThreadRuntimeCore {
1698
1860
  const messageId = this.findMessageIdForToolCall(toolCallId);
1699
1861
  if (messageId !== undefined) {
1700
1862
  this.applyCrossRunActivitySnapshot(messageId, toolCallId, event);
1701
- return;
1863
+ break;
1702
1864
  }
1703
1865
  }
1704
1866
  aggregator.handle(event);
1705
- return;
1867
+ break;
1706
1868
  }
1707
1869
  default:
1708
1870
  aggregator.handle(event);
1709
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();
1710
1898
  }
1711
1899
 
1712
1900
  private applyCrossRunToolResult(
@@ -1860,9 +2048,28 @@ export class AgUiThreadRuntimeCore {
1860
2048
  const converted: ThreadMessage[] = [];
1861
2049
  for (const message of normalized) {
1862
2050
  try {
1863
- converted.push(
1864
- fromThreadMessageLike(message, generateId(), FALLBACK_USER_STATUS),
2051
+ const convertedMessage = fromThreadMessageLike(
2052
+ message,
2053
+ generateId(),
2054
+ FALLBACK_USER_STATUS,
1865
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
+ }
1866
2073
  } catch (error) {
1867
2074
  this.logger.error?.(
1868
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
+ });