@assistant-ui/react-ag-ui 0.0.60 → 0.0.62
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/agUiExtras.d.ts +1 -3
- package/dist/agUiExtras.d.ts.map +1 -1
- package/dist/hooks.d.ts +3 -5
- package/dist/hooks.d.ts.map +1 -1
- package/dist/index.d.ts +7 -5
- package/dist/index.d.ts.map +1 -0
- package/dist/runtime/AgUiThreadRuntimeCore.d.ts +152 -144
- package/dist/runtime/AgUiThreadRuntimeCore.d.ts.map +1 -1
- package/dist/runtime/AgUiThreadRuntimeCore.js +141 -23
- package/dist/runtime/AgUiThreadRuntimeCore.js.map +1 -1
- package/dist/runtime/adapter/conversions.d.ts +51 -53
- package/dist/runtime/adapter/conversions.d.ts.map +1 -1
- package/dist/runtime/adapter/conversions.js +65 -21
- package/dist/runtime/adapter/conversions.js.map +1 -1
- package/dist/runtime/adapter/run-aggregator.d.ts +89 -88
- package/dist/runtime/adapter/run-aggregator.d.ts.map +1 -1
- package/dist/runtime/adapter/run-aggregator.js +35 -2
- package/dist/runtime/adapter/run-aggregator.js.map +1 -1
- package/dist/runtime/adapter/subscriber.d.ts +107 -108
- package/dist/runtime/adapter/subscriber.d.ts.map +1 -1
- package/dist/runtime/adapter/tool-approval.d.ts +3 -4
- package/dist/runtime/adapter/tool-approval.d.ts.map +1 -1
- package/dist/runtime/event-parser.d.ts +3 -5
- package/dist/runtime/event-parser.d.ts.map +1 -1
- package/dist/runtime/interrupt-internals.d.ts +1 -3
- package/dist/runtime/interrupt-internals.d.ts.map +1 -1
- package/dist/runtime/logger.d.ts +2 -4
- package/dist/runtime/logger.d.ts.map +1 -1
- package/dist/runtime/mcp-tool-result.d.ts +4 -6
- package/dist/runtime/mcp-tool-result.d.ts.map +1 -1
- package/dist/runtime/types.d.ts +237 -238
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/useAgUiRuntime.d.ts +13 -15
- package/dist/useAgUiRuntime.d.ts.map +1 -1
- package/dist/useAgUiRuntime.js +9 -2
- package/dist/useAgUiRuntime.js.map +1 -1
- package/package.json +6 -6
- package/src/runtime/AgUiThreadRuntimeCore.ts +239 -32
- package/src/runtime/adapter/conversions.content.test.ts +56 -0
- package/src/runtime/adapter/conversions.ts +112 -44
- package/src/runtime/adapter/run-aggregator.ts +53 -2
- package/src/useAgUiRuntime.history.test.tsx +93 -2
- package/src/useAgUiRuntime.thread-switch.test.tsx +84 -4
- package/src/useAgUiRuntime.ts +12 -3
- 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.
|
|
3
|
+
"version": "0.0.62",
|
|
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.
|
|
34
|
-
"@assistant-ui/react-generative-ui": "^0.0.
|
|
35
|
-
"@assistant-ui/store": "^0.3.
|
|
36
|
-
"assistant-stream": "^0.3.
|
|
33
|
+
"@assistant-ui/core": "^0.3.21",
|
|
34
|
+
"@assistant-ui/react-generative-ui": "^0.0.21",
|
|
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.
|
|
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.
|
|
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
|
|
1710
|
+
const preserved = new Map<string, ToolCallMessagePart>();
|
|
1595
1711
|
for (const part of iterateToolCallParts(previous)) {
|
|
1596
1712
|
if (isResolvedToolCall(part)) {
|
|
1597
|
-
|
|
1713
|
+
preserved.set(part.toolCallId, part);
|
|
1598
1714
|
}
|
|
1599
1715
|
}
|
|
1600
|
-
if (
|
|
1716
|
+
if (preserved.size === 0) {
|
|
1717
|
+
return this.preserveToolInteractions(previous, next);
|
|
1718
|
+
}
|
|
1601
1719
|
|
|
1602
1720
|
const { content: merged, changed } = mapToolCallPartsDeep(next, (part) => {
|
|
1603
|
-
|
|
1604
|
-
const prior = resolved.get(part.toolCallId);
|
|
1721
|
+
const prior = preserved.get(part.toolCallId);
|
|
1605
1722
|
if (!prior) return part;
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
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.
|
|
1657
|
-
|
|
1800
|
+
this.updateActiveAssistantState(activeAssistantId);
|
|
1801
|
+
break;
|
|
1658
1802
|
}
|
|
1659
1803
|
case "STATE_DELTA": {
|
|
1660
|
-
if (event.delta.length === 0)
|
|
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.
|
|
1814
|
+
this.updateActiveAssistantState(activeAssistantId);
|
|
1671
1815
|
} catch (error) {
|
|
1672
1816
|
this.logger.error?.("[agui] failed to apply state delta", error);
|
|
1673
1817
|
}
|
|
1674
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1847
|
+
break;
|
|
1686
1848
|
}
|
|
1687
1849
|
}
|
|
1688
1850
|
aggregator.handle(event);
|
|
1689
|
-
|
|
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
|
-
|
|
1863
|
+
break;
|
|
1702
1864
|
}
|
|
1703
1865
|
}
|
|
1704
1866
|
aggregator.handle(event);
|
|
1705
|
-
|
|
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
|
-
|
|
1864
|
-
|
|
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
|
+
});
|