@assistant-ui/react-ag-ui 0.1.3 → 0.1.5
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/runtime/AgUiThreadController.d.ts +5 -0
- package/dist/runtime/AgUiThreadController.d.ts.map +1 -1
- package/dist/runtime/AgUiThreadController.js +106 -10
- package/dist/runtime/AgUiThreadController.js.map +1 -1
- package/dist/useAgUiRuntime.d.ts.map +1 -1
- package/dist/useAgUiRuntime.js +1 -0
- package/dist/useAgUiRuntime.js.map +1 -1
- package/package.json +2 -2
- package/src/runtime/AgUiThreadController.snapshot-head-history.test.ts +226 -0
- package/src/runtime/AgUiThreadController.snapshot-nested-settle.test.ts +210 -0
- package/src/runtime/AgUiThreadController.ts +148 -15
- package/src/useAgUiRuntime.cloud-run-leases.test.tsx +85 -7
- package/src/useAgUiRuntime.ts +1 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useAgUiRuntime.d.ts","sourceRoot":"","sources":["../src/useAgUiRuntime.ts"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EACV,gBAAgB,EAIjB,MAAM,oBAAoB,CAAC;AAG5B,OAAO,KAAK,EACV,aAAa,EACb,eAAe,EACf,qBAAqB,EACtB,2BAAwB;AASzB,MAAM,MAAM,oBAAoB,GAAG,gBAAgB,GAAG;IACpD;;;OAGG;IACH,6BAA6B,EAAE,MAAM,SAAS,aAAa,EAAE,CAAC;IAC9D;;;;OAIG;IACH,iCAAiC,EAAE,CACjC,SAAS,EAAE,SAAS,eAAe,EAAE,KAClC,OAAO,CAAC,IAAI,CAAC,CAAC;CACpB,CAAC;AAEF,wBAAgB,cAAc,CAC5B,OAAO,EAAE,qBAAqB,GAC7B,oBAAoB,
|
|
1
|
+
{"version":3,"file":"useAgUiRuntime.d.ts","sourceRoot":"","sources":["../src/useAgUiRuntime.ts"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EACV,gBAAgB,EAIjB,MAAM,oBAAoB,CAAC;AAG5B,OAAO,KAAK,EACV,aAAa,EACb,eAAe,EACf,qBAAqB,EACtB,2BAAwB;AASzB,MAAM,MAAM,oBAAoB,GAAG,gBAAgB,GAAG;IACpD;;;OAGG;IACH,6BAA6B,EAAE,MAAM,SAAS,aAAa,EAAE,CAAC;IAC9D;;;;OAIG;IACH,iCAAiC,EAAE,CACjC,SAAS,EAAE,SAAS,eAAe,EAAE,KAClC,OAAO,CAAC,IAAI,CAAC,CAAC;CACpB,CAAC;AAEF,wBAAgB,cAAc,CAC5B,OAAO,EAAE,qBAAqB,GAC7B,oBAAoB,CAyWtB"}
|
package/dist/useAgUiRuntime.js
CHANGED
|
@@ -199,6 +199,7 @@ function useAgUiRuntime(options) {
|
|
|
199
199
|
queueController?.clear();
|
|
200
200
|
return core.reload(parentId, config);
|
|
201
201
|
},
|
|
202
|
+
onDelete: (messageId) => core.deleteMessage(messageId),
|
|
202
203
|
onCancel: async () => {
|
|
203
204
|
queueController?.notifyCancelled();
|
|
204
205
|
core.cancel();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useAgUiRuntime.js","names":[],"sources":["../src/useAgUiRuntime.ts"],"sourcesContent":["\"use client\";\n\nimport {\n useEffect,\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 {\n useIsomorphicLayoutEffect,\n useReplaySafeEffect,\n} from \"@assistant-ui/store/internal\";\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 { AgUiThreadController } from \"./runtime/AgUiThreadController\";\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 coreRef = useRef<AgUiThreadController | null>(null);\n const threadSwitchGenerationRef = useRef(0);\n const switchingGenerationRef = useRef<number | null>(null);\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 AgUiThreadController({\n agent: options.agent,\n logger,\n showThinking: options.showThinking ?? true,\n joinStrategy: options.joinStrategy,\n getRunContext: options.getRunContext,\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 isThreadSwitching: () => switchingGenerationRef.current !== null,\n });\n }\n\n const core = coreRef.current;\n const snapshot = useSyncExternalStore(\n core.subscribe,\n core.getSnapshot,\n core.getSnapshot,\n );\n useEffect(() => {\n core.updateOptions({\n agent: options.agent,\n logger,\n showThinking: options.showThinking ?? true,\n joinStrategy: options.joinStrategy,\n getRunContext: options.getRunContext,\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 = snapshot.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 const lastQueueEdgeRef = useRef<{\n controller: MessageQueueController;\n busy: boolean;\n } | null>(null);\n if (options.unstable_enableMessageQueue && !queueRef.current) {\n queueRef.current = createMessageQueue({\n run: (message) => {\n const controller = queueRef.current;\n // A send dispatched mid-switch is dropped, and the queue released, or\n // it would wait for an idle edge that never comes.\n if (switchingGenerationRef.current !== null) {\n controller?.notifyIdle();\n return;\n }\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 useIsomorphicLayoutEffect(() => {\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 (options.isSendDisabled === true) {\n queueController?.hold();\n } else {\n queueController?.release();\n }\n }, [options.isSendDisabled, queueController]);\n\n useEffect(() => {\n if (!queueController) return;\n if (\n lastQueueEdgeRef.current?.controller === queueController &&\n lastQueueEdgeRef.current.busy === queueBusy\n )\n return;\n lastQueueEdgeRef.current = { controller: queueController, busy: queueBusy };\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 const abandonPreviousThread = () => {\n queueRef.current?.notifyCancelled();\n if (core.isRunning()) {\n void core.cancel().catch((error: unknown) => {\n logger.error?.(\n \"[agui] cancelling the run on thread switch failed\",\n error,\n );\n });\n }\n queueRef.current?.clear();\n };\n\n const releaseSwitch = (generation: number) => {\n if (switchingGenerationRef.current === generation) {\n switchingGenerationRef.current = null;\n }\n };\n\n return {\n ...rest,\n onSwitchToNewThread: onSwitchToNewThread\n ? async () => {\n const generation = ++threadSwitchGenerationRef.current;\n switchingGenerationRef.current = generation;\n try {\n abandonPreviousThread();\n if (generation !== threadSwitchGenerationRef.current) return;\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 abandonPreviousThread();\n if (generation !== threadSwitchGenerationRef.current) return;\n core.applyExternalMessages([]);\n core.resetThreadState();\n } finally {\n releaseSwitch(generation);\n }\n }\n : undefined,\n onSwitchToThread: onSwitchToThread\n ? async (threadId: string) => {\n const generation = ++threadSwitchGenerationRef.current;\n switchingGenerationRef.current = generation;\n try {\n abandonPreviousThread();\n if (generation !== threadSwitchGenerationRef.current) return;\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 abandonPreviousThread();\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 (generation !== threadSwitchGenerationRef.current) return;\n releaseSwitch(generation);\n if (result.unstable_resume) {\n void core.resumeInFlightRun(result.messages);\n }\n } finally {\n releaseSwitch(generation);\n }\n }\n : undefined,\n };\n }, [threadListAdapter, core, logger]);\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 return {\n ...shared,\n isLoading: snapshot.isLoading,\n messageRepository: snapshot.messageRepository,\n state: snapshot.state,\n isRunning: snapshot.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: snapshot.state,\n setState: (next) => core.setState(next),\n }),\n unstable_enableToolInvocations: true,\n unstable_persistsHistory: true,\n setToolStatuses,\n onNew: (message: AppendMessage) =>\n switchingGenerationRef.current !== null\n ? Promise.resolve()\n : 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 // toolInvocations intentionally excluded: abort/resume use refs internally and work with stale captures\n [\n adapterAdapters,\n core,\n isRunning,\n queueController,\n queueItems,\n steerQueueItems,\n shared,\n snapshot,\n ],\n );\n\n const baseRuntime = useExternalStoreRuntime(store);\n\n const createRuntime = (): AgUiAssistantRuntime => {\n return {\n threads: baseRuntime.threads,\n get thread() {\n return baseRuntime.thread;\n },\n registerModelContextProvider: (provider) =>\n baseRuntime.registerModelContextProvider(provider),\n unstable_getPendingInterrupts: () =>\n core.getPendingInterrupts()?.interrupts ?? [],\n unstable_submitInterruptResponses: (responses) =>\n core.submitInterruptResponses(responses),\n };\n };\n const [pinnedRuntime, setPinnedRuntime] = useState(() => ({\n baseRuntime,\n runtime: createRuntime(),\n }));\n let currentRuntime = pinnedRuntime;\n if (pinnedRuntime.baseRuntime !== baseRuntime) {\n currentRuntime = { baseRuntime, runtime: createRuntime() };\n setPinnedRuntime(currentRuntime);\n }\n const runtime = currentRuntime.runtime;\n\n useReplaySafeEffect(() => {\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 useEffect,\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 {\n useIsomorphicLayoutEffect,\n useReplaySafeEffect,\n} from \"@assistant-ui/store/internal\";\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 { AgUiThreadController } from \"./runtime/AgUiThreadController\";\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 coreRef = useRef<AgUiThreadController | null>(null);\n const threadSwitchGenerationRef = useRef(0);\n const switchingGenerationRef = useRef<number | null>(null);\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 AgUiThreadController({\n agent: options.agent,\n logger,\n showThinking: options.showThinking ?? true,\n joinStrategy: options.joinStrategy,\n getRunContext: options.getRunContext,\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 isThreadSwitching: () => switchingGenerationRef.current !== null,\n });\n }\n\n const core = coreRef.current;\n const snapshot = useSyncExternalStore(\n core.subscribe,\n core.getSnapshot,\n core.getSnapshot,\n );\n useEffect(() => {\n core.updateOptions({\n agent: options.agent,\n logger,\n showThinking: options.showThinking ?? true,\n joinStrategy: options.joinStrategy,\n getRunContext: options.getRunContext,\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 = snapshot.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 const lastQueueEdgeRef = useRef<{\n controller: MessageQueueController;\n busy: boolean;\n } | null>(null);\n if (options.unstable_enableMessageQueue && !queueRef.current) {\n queueRef.current = createMessageQueue({\n run: (message) => {\n const controller = queueRef.current;\n // A send dispatched mid-switch is dropped, and the queue released, or\n // it would wait for an idle edge that never comes.\n if (switchingGenerationRef.current !== null) {\n controller?.notifyIdle();\n return;\n }\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 useIsomorphicLayoutEffect(() => {\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 (options.isSendDisabled === true) {\n queueController?.hold();\n } else {\n queueController?.release();\n }\n }, [options.isSendDisabled, queueController]);\n\n useEffect(() => {\n if (!queueController) return;\n if (\n lastQueueEdgeRef.current?.controller === queueController &&\n lastQueueEdgeRef.current.busy === queueBusy\n )\n return;\n lastQueueEdgeRef.current = { controller: queueController, busy: queueBusy };\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 const abandonPreviousThread = () => {\n queueRef.current?.notifyCancelled();\n if (core.isRunning()) {\n void core.cancel().catch((error: unknown) => {\n logger.error?.(\n \"[agui] cancelling the run on thread switch failed\",\n error,\n );\n });\n }\n queueRef.current?.clear();\n };\n\n const releaseSwitch = (generation: number) => {\n if (switchingGenerationRef.current === generation) {\n switchingGenerationRef.current = null;\n }\n };\n\n return {\n ...rest,\n onSwitchToNewThread: onSwitchToNewThread\n ? async () => {\n const generation = ++threadSwitchGenerationRef.current;\n switchingGenerationRef.current = generation;\n try {\n abandonPreviousThread();\n if (generation !== threadSwitchGenerationRef.current) return;\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 abandonPreviousThread();\n if (generation !== threadSwitchGenerationRef.current) return;\n core.applyExternalMessages([]);\n core.resetThreadState();\n } finally {\n releaseSwitch(generation);\n }\n }\n : undefined,\n onSwitchToThread: onSwitchToThread\n ? async (threadId: string) => {\n const generation = ++threadSwitchGenerationRef.current;\n switchingGenerationRef.current = generation;\n try {\n abandonPreviousThread();\n if (generation !== threadSwitchGenerationRef.current) return;\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 abandonPreviousThread();\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 (generation !== threadSwitchGenerationRef.current) return;\n releaseSwitch(generation);\n if (result.unstable_resume) {\n void core.resumeInFlightRun(result.messages);\n }\n } finally {\n releaseSwitch(generation);\n }\n }\n : undefined,\n };\n }, [threadListAdapter, core, logger]);\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 return {\n ...shared,\n isLoading: snapshot.isLoading,\n messageRepository: snapshot.messageRepository,\n state: snapshot.state,\n isRunning: snapshot.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: snapshot.state,\n setState: (next) => core.setState(next),\n }),\n unstable_enableToolInvocations: true,\n unstable_persistsHistory: true,\n setToolStatuses,\n onNew: (message: AppendMessage) =>\n switchingGenerationRef.current !== null\n ? Promise.resolve()\n : 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 onDelete: (messageId: string) => core.deleteMessage(messageId),\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 // toolInvocations intentionally excluded: abort/resume use refs internally and work with stale captures\n [\n adapterAdapters,\n core,\n isRunning,\n queueController,\n queueItems,\n steerQueueItems,\n shared,\n snapshot,\n ],\n );\n\n const baseRuntime = useExternalStoreRuntime(store);\n\n const createRuntime = (): AgUiAssistantRuntime => {\n return {\n threads: baseRuntime.threads,\n get thread() {\n return baseRuntime.thread;\n },\n registerModelContextProvider: (provider) =>\n baseRuntime.registerModelContextProvider(provider),\n unstable_getPendingInterrupts: () =>\n core.getPendingInterrupts()?.interrupts ?? [],\n unstable_submitInterruptResponses: (responses) =>\n core.submitInterruptResponses(responses),\n };\n };\n const [pinnedRuntime, setPinnedRuntime] = useState(() => ({\n baseRuntime,\n runtime: createRuntime(),\n }));\n let currentRuntime = pinnedRuntime;\n if (pinnedRuntime.baseRuntime !== baseRuntime) {\n currentRuntime = { baseRuntime, runtime: createRuntime() };\n setPinnedRuntime(currentRuntime);\n }\n const runtime = currentRuntime.runtime;\n\n useReplaySafeEffect(() => {\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.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "AG-UI protocol adapter for assistant-ui",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ag-ui",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"sideEffects": false,
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@ag-ui/client": "^1.0.3",
|
|
33
|
-
"@assistant-ui/core": "^0.3.
|
|
33
|
+
"@assistant-ui/core": "^0.3.35",
|
|
34
34
|
"@assistant-ui/generative-ui": "^0.0.10",
|
|
35
35
|
"@assistant-ui/store": "^0.3.23",
|
|
36
36
|
"assistant-stream": "^0.3.57",
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { AppendMessage } from "@assistant-ui/core";
|
|
3
|
+
import { HttpAgent } from "@ag-ui/client";
|
|
4
|
+
import { AgUiThreadController } from "./AgUiThreadController";
|
|
5
|
+
import { makeLogger } from "./logger";
|
|
6
|
+
|
|
7
|
+
const tick = () => new Promise((r) => setTimeout(r, 0));
|
|
8
|
+
const settle = async () => {
|
|
9
|
+
for (let i = 0; i < 8; i++) await tick();
|
|
10
|
+
};
|
|
11
|
+
const userMessage = {
|
|
12
|
+
role: "user",
|
|
13
|
+
content: [{ type: "text", text: "hello" }],
|
|
14
|
+
attachments: [],
|
|
15
|
+
metadata: { custom: {} },
|
|
16
|
+
createdAt: new Date(0),
|
|
17
|
+
parentId: null,
|
|
18
|
+
sourceId: null,
|
|
19
|
+
runConfig: {},
|
|
20
|
+
startRun: true,
|
|
21
|
+
} as unknown as AppendMessage;
|
|
22
|
+
|
|
23
|
+
type Row = { id: string; status: string | undefined };
|
|
24
|
+
|
|
25
|
+
// Follows the ThreadHistoryAdapter contract: both writes upsert by message
|
|
26
|
+
// id, and only append moves the stored head.
|
|
27
|
+
function createHistory(writes: string[]) {
|
|
28
|
+
const items = new Map<string, any>();
|
|
29
|
+
let headId: string | null = null;
|
|
30
|
+
const store = (item: any, moveHead: boolean) => {
|
|
31
|
+
if (moveHead) headId = item.message.id;
|
|
32
|
+
items.set(item.message.id, structuredClone(item));
|
|
33
|
+
};
|
|
34
|
+
return {
|
|
35
|
+
rows: () =>
|
|
36
|
+
new Map<string, Row>(
|
|
37
|
+
[...items.values()].map((item) => [
|
|
38
|
+
item.message.id,
|
|
39
|
+
{ id: item.message.id, status: item.message.status?.type },
|
|
40
|
+
]),
|
|
41
|
+
),
|
|
42
|
+
load: async () => ({ headId, messages: [...items.values()] }),
|
|
43
|
+
append: async (item: any) => {
|
|
44
|
+
writes.push(`append ${item.message.id}`);
|
|
45
|
+
store(item, true);
|
|
46
|
+
},
|
|
47
|
+
update: async (item: any) => {
|
|
48
|
+
writes.push(`update ${item.message.id}`);
|
|
49
|
+
store(item, false);
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function reload(history: ReturnType<typeof createHistory>) {
|
|
55
|
+
const core = new AgUiThreadController({
|
|
56
|
+
agent: new HttpAgent({ url: "https://agent.test" }),
|
|
57
|
+
logger: makeLogger(),
|
|
58
|
+
showThinking: false,
|
|
59
|
+
history: history as any,
|
|
60
|
+
});
|
|
61
|
+
await core.__internal_load();
|
|
62
|
+
return core.getMessages().map((m: any) => `${m.id}:${m.status?.type ?? "-"}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function run(events: (requestMessages: any[]) => object[][]) {
|
|
66
|
+
const writes: string[] = [];
|
|
67
|
+
const history = createHistory(writes);
|
|
68
|
+
let write!: (e: object) => void;
|
|
69
|
+
let close!: () => void;
|
|
70
|
+
let requestMessages: any[] = [];
|
|
71
|
+
const agent = new HttpAgent({
|
|
72
|
+
url: "https://agent.test",
|
|
73
|
+
fetch: async (_url: unknown, init: RequestInit) => {
|
|
74
|
+
const encoder = new TextEncoder();
|
|
75
|
+
let controller!: ReadableStreamDefaultController<Uint8Array>;
|
|
76
|
+
const body = new ReadableStream<Uint8Array>({
|
|
77
|
+
start: (c) => void (controller = c),
|
|
78
|
+
});
|
|
79
|
+
requestMessages = JSON.parse(init.body as string).messages ?? [];
|
|
80
|
+
write = (e) =>
|
|
81
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(e)}\n\n`));
|
|
82
|
+
close = () => controller.close();
|
|
83
|
+
return new Response(body, {
|
|
84
|
+
headers: { "Content-Type": "text/event-stream" },
|
|
85
|
+
});
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
const core = new AgUiThreadController({
|
|
89
|
+
agent,
|
|
90
|
+
logger: makeLogger(),
|
|
91
|
+
showThinking: false,
|
|
92
|
+
history: history as any,
|
|
93
|
+
});
|
|
94
|
+
const sending = core.append(userMessage).catch(() => {});
|
|
95
|
+
for (let i = 0; i < 50 && !write; i++) await tick();
|
|
96
|
+
for (const batch of events(requestMessages)) {
|
|
97
|
+
for (const e of batch) write(e);
|
|
98
|
+
await settle();
|
|
99
|
+
}
|
|
100
|
+
close();
|
|
101
|
+
await sending;
|
|
102
|
+
await settle();
|
|
103
|
+
const shown = core
|
|
104
|
+
.getMessages()
|
|
105
|
+
.map((m: any) => `${m.id}:${m.status?.type ?? "-"}`);
|
|
106
|
+
return {
|
|
107
|
+
rows: history.rows(),
|
|
108
|
+
writes,
|
|
109
|
+
shown,
|
|
110
|
+
reloaded: await reload(history),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const start = [
|
|
115
|
+
{ type: "RUN_STARTED", threadId: "t", runId: "r" },
|
|
116
|
+
{ type: "TEXT_MESSAGE_START", messageId: "answer", role: "assistant" },
|
|
117
|
+
{ type: "TEXT_MESSAGE_CONTENT", messageId: "answer", delta: "a1" },
|
|
118
|
+
];
|
|
119
|
+
const end = { type: "TEXT_MESSAGE_END", messageId: "answer" };
|
|
120
|
+
const moveSnapshot = (req: any[]) => ({
|
|
121
|
+
type: "MESSAGES_SNAPSHOT",
|
|
122
|
+
messages: [
|
|
123
|
+
...req,
|
|
124
|
+
{ id: "answer", role: "assistant", content: "a1" },
|
|
125
|
+
{ id: "other", role: "assistant", content: "o" },
|
|
126
|
+
],
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe("ag-ui: a snapshot's new head and the history adapter", () => {
|
|
130
|
+
it("stores the new head after one head-move snapshot and RUN_FINISHED", async () => {
|
|
131
|
+
const { rows, writes, shown, reloaded } = await run((req) => [
|
|
132
|
+
start,
|
|
133
|
+
[moveSnapshot(req)],
|
|
134
|
+
[end, { type: "RUN_FINISHED", threadId: "t", runId: "r" }],
|
|
135
|
+
]);
|
|
136
|
+
expect(shown).toContain("other:complete");
|
|
137
|
+
expect(rows.get("other")?.status, writes.join(", ")).toBe("complete");
|
|
138
|
+
expect(reloaded.slice(1), writes.join(", ")).toEqual(shown.slice(1));
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("stores the new head after one head-move snapshot and RUN_ERROR", async () => {
|
|
142
|
+
const { rows, writes, shown, reloaded } = await run((req) => [
|
|
143
|
+
start,
|
|
144
|
+
[moveSnapshot(req)],
|
|
145
|
+
[{ type: "RUN_ERROR", message: "boom" }],
|
|
146
|
+
]);
|
|
147
|
+
// Which message takes the error depends on whether the run moved onto
|
|
148
|
+
// "other"; either way history holds what the thread shows.
|
|
149
|
+
const other = shown.find((entry) => entry.startsWith("other:"));
|
|
150
|
+
expect(["other:incomplete", "other:complete"]).toContain(other);
|
|
151
|
+
expect(`other:${rows.get("other")?.status}`, writes.join(", ")).toBe(other);
|
|
152
|
+
expect(reloaded.slice(1), writes.join(", ")).toEqual(shown.slice(1));
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("stores a snapshot-only answer that a second snapshot repeats", async () => {
|
|
156
|
+
const snapshot = (req: any[]) => ({
|
|
157
|
+
type: "MESSAGES_SNAPSHOT",
|
|
158
|
+
messages: [...req, { id: "only", role: "assistant", content: "o" }],
|
|
159
|
+
});
|
|
160
|
+
const { rows, writes, shown, reloaded } = await run((req) => [
|
|
161
|
+
[{ type: "RUN_STARTED", threadId: "t", runId: "r" }, snapshot(req)],
|
|
162
|
+
[snapshot(req)],
|
|
163
|
+
[{ type: "RUN_FINISHED", threadId: "t", runId: "r" }],
|
|
164
|
+
]);
|
|
165
|
+
expect(rows.get("only")?.status, writes.join(", ")).toBe("complete");
|
|
166
|
+
expect(reloaded.slice(1), writes.join(", ")).toEqual(shown.slice(1));
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("stores an answer that arrives only in a snapshot", async () => {
|
|
170
|
+
const { rows, writes, shown, reloaded } = await run((req) => [
|
|
171
|
+
[
|
|
172
|
+
{ type: "RUN_STARTED", threadId: "t", runId: "r" },
|
|
173
|
+
{
|
|
174
|
+
type: "MESSAGES_SNAPSHOT",
|
|
175
|
+
messages: [...req, { id: "only", role: "assistant", content: "o" }],
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
[{ type: "RUN_FINISHED", threadId: "t", runId: "r" }],
|
|
179
|
+
]);
|
|
180
|
+
expect(shown).toContain("only:complete");
|
|
181
|
+
expect(rows.get("only")?.status, writes.join(", ")).toBe("complete");
|
|
182
|
+
expect(writes.filter((w) => w.endsWith(" only"))).toHaveLength(1);
|
|
183
|
+
expect(reloaded.slice(1), writes.join(", ")).toEqual(shown.slice(1));
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("keeps the streamed answer as the head after a snapshot adds an earlier step", async () => {
|
|
187
|
+
const { writes, shown, reloaded } = await run((req) => [
|
|
188
|
+
start,
|
|
189
|
+
[
|
|
190
|
+
{
|
|
191
|
+
type: "MESSAGES_SNAPSHOT",
|
|
192
|
+
messages: [
|
|
193
|
+
...req,
|
|
194
|
+
{ id: "step", role: "assistant", content: "s" },
|
|
195
|
+
{ id: "answer", role: "assistant", content: "a1" },
|
|
196
|
+
],
|
|
197
|
+
},
|
|
198
|
+
],
|
|
199
|
+
[end, { type: "RUN_FINISHED", threadId: "t", runId: "r" }],
|
|
200
|
+
]);
|
|
201
|
+
expect(shown.slice(1)).toEqual(["step:complete", "answer:complete"]);
|
|
202
|
+
expect(reloaded.slice(1), writes.join(", ")).toEqual(shown.slice(1));
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("control: a second snapshot after the head move stores the new head", async () => {
|
|
206
|
+
const { rows, writes, shown, reloaded } = await run((req) => [
|
|
207
|
+
start,
|
|
208
|
+
[moveSnapshot(req)],
|
|
209
|
+
[moveSnapshot(req)],
|
|
210
|
+
[end, { type: "RUN_FINISHED", threadId: "t", runId: "r" }],
|
|
211
|
+
]);
|
|
212
|
+
expect(rows.get("other")?.status, JSON.stringify({ writes, shown })).toBe(
|
|
213
|
+
"complete",
|
|
214
|
+
);
|
|
215
|
+
expect(reloaded.slice(1), writes.join(", ")).toEqual(shown.slice(1));
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("control: the streamed answer without a snapshot is stored", async () => {
|
|
219
|
+
const { rows, shown, reloaded } = await run(() => [
|
|
220
|
+
start,
|
|
221
|
+
[end, { type: "RUN_FINISHED", threadId: "t", runId: "r" }],
|
|
222
|
+
]);
|
|
223
|
+
expect(rows.get("answer")?.status).toBe("complete");
|
|
224
|
+
expect(reloaded).toEqual(shown);
|
|
225
|
+
});
|
|
226
|
+
});
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { AppendMessage } from "@assistant-ui/core";
|
|
3
|
+
import { HttpAgent } from "@ag-ui/client";
|
|
4
|
+
import { AgUiThreadController } from "./AgUiThreadController";
|
|
5
|
+
import { makeLogger } from "./logger";
|
|
6
|
+
|
|
7
|
+
const tick = () => new Promise((r) => setTimeout(r, 0));
|
|
8
|
+
const settle = async () => {
|
|
9
|
+
for (let i = 0; i < 8; i++) await tick();
|
|
10
|
+
};
|
|
11
|
+
const userMessage = {
|
|
12
|
+
role: "user",
|
|
13
|
+
content: [{ type: "text", text: "hello" }],
|
|
14
|
+
attachments: [],
|
|
15
|
+
metadata: { custom: {} },
|
|
16
|
+
createdAt: new Date(0),
|
|
17
|
+
parentId: null,
|
|
18
|
+
sourceId: null,
|
|
19
|
+
runConfig: {},
|
|
20
|
+
startRun: true,
|
|
21
|
+
} as unknown as AppendMessage;
|
|
22
|
+
|
|
23
|
+
type End = "close" | "error" | "stop" | "finish";
|
|
24
|
+
|
|
25
|
+
async function run(end: End, withSnapshot = true, history?: unknown) {
|
|
26
|
+
let write!: (e: object) => void;
|
|
27
|
+
let close!: () => void;
|
|
28
|
+
let requestMessages: any[] = [];
|
|
29
|
+
const agent = new HttpAgent({
|
|
30
|
+
url: "https://agent.test",
|
|
31
|
+
fetch: async (_url: unknown, init: RequestInit) => {
|
|
32
|
+
const encoder = new TextEncoder();
|
|
33
|
+
let controller!: ReadableStreamDefaultController<Uint8Array>;
|
|
34
|
+
let closed = false;
|
|
35
|
+
const body = new ReadableStream<Uint8Array>({
|
|
36
|
+
start: (c) => void (controller = c),
|
|
37
|
+
});
|
|
38
|
+
requestMessages = JSON.parse(init.body as string).messages ?? [];
|
|
39
|
+
write = (e) => {
|
|
40
|
+
if (!closed)
|
|
41
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(e)}\n\n`));
|
|
42
|
+
};
|
|
43
|
+
close = () => {
|
|
44
|
+
if (closed) return;
|
|
45
|
+
closed = true;
|
|
46
|
+
controller.close();
|
|
47
|
+
};
|
|
48
|
+
init.signal?.addEventListener("abort", () => {
|
|
49
|
+
if (closed) return;
|
|
50
|
+
closed = true;
|
|
51
|
+
try {
|
|
52
|
+
controller.error(new DOMException("aborted", "AbortError"));
|
|
53
|
+
} catch {}
|
|
54
|
+
});
|
|
55
|
+
return new Response(body, {
|
|
56
|
+
headers: { "Content-Type": "text/event-stream" },
|
|
57
|
+
});
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
const core = new AgUiThreadController({
|
|
61
|
+
agent,
|
|
62
|
+
logger: makeLogger(),
|
|
63
|
+
showThinking: false,
|
|
64
|
+
...(history ? { history: history as any } : {}),
|
|
65
|
+
});
|
|
66
|
+
const sending = core.append(userMessage).catch(() => {});
|
|
67
|
+
for (let i = 0; i < 50 && !write; i++) await tick();
|
|
68
|
+
const events: object[] = [
|
|
69
|
+
{ type: "RUN_STARTED", threadId: "t", runId: "r" },
|
|
70
|
+
{ type: "TEXT_MESSAGE_START", messageId: "answer", role: "assistant" },
|
|
71
|
+
{ type: "TEXT_MESSAGE_CONTENT", messageId: "answer", delta: "a1" },
|
|
72
|
+
{ type: "TEXT_MESSAGE_END", messageId: "answer" },
|
|
73
|
+
{ type: "TOOL_CALL_START", toolCallId: "task", toolCallName: "task" },
|
|
74
|
+
{ type: "TOOL_CALL_END", toolCallId: "task" },
|
|
75
|
+
{
|
|
76
|
+
type: "SUBAGENT_STARTED",
|
|
77
|
+
subagentRunId: "sub",
|
|
78
|
+
name: "worker",
|
|
79
|
+
parentToolCallId: "task",
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
type: "TOOL_CALL_START",
|
|
83
|
+
toolCallId: "ncall",
|
|
84
|
+
toolCallName: "search",
|
|
85
|
+
subagentRunId: "sub",
|
|
86
|
+
},
|
|
87
|
+
{ type: "TOOL_CALL_END", toolCallId: "ncall", subagentRunId: "sub" },
|
|
88
|
+
{ type: "SUBAGENT_FINISHED", subagentRunId: "sub" },
|
|
89
|
+
];
|
|
90
|
+
for (const e of events) write(e);
|
|
91
|
+
await settle();
|
|
92
|
+
if (withSnapshot) {
|
|
93
|
+
write({
|
|
94
|
+
type: "MESSAGES_SNAPSHOT",
|
|
95
|
+
messages: [
|
|
96
|
+
...requestMessages,
|
|
97
|
+
{
|
|
98
|
+
id: "answer",
|
|
99
|
+
role: "assistant",
|
|
100
|
+
content: "a1",
|
|
101
|
+
toolCalls: [
|
|
102
|
+
{
|
|
103
|
+
id: "task",
|
|
104
|
+
type: "function",
|
|
105
|
+
function: { name: "task", arguments: "{}" },
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
id: "s-call",
|
|
111
|
+
role: "assistant",
|
|
112
|
+
content: "",
|
|
113
|
+
subagentRunId: "sub",
|
|
114
|
+
toolCalls: [
|
|
115
|
+
{
|
|
116
|
+
id: "ncall",
|
|
117
|
+
type: "function",
|
|
118
|
+
function: { name: "search", arguments: "{}" },
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
});
|
|
124
|
+
await settle();
|
|
125
|
+
}
|
|
126
|
+
const before = nested(core);
|
|
127
|
+
if (end === "close") close();
|
|
128
|
+
else if (end === "error")
|
|
129
|
+
write({ type: "RUN_ERROR", message: "server failed" });
|
|
130
|
+
else if (end === "finish")
|
|
131
|
+
write({ type: "RUN_FINISHED", threadId: "t", runId: "r" });
|
|
132
|
+
else await core.cancel();
|
|
133
|
+
await settle();
|
|
134
|
+
close();
|
|
135
|
+
await sending;
|
|
136
|
+
await settle();
|
|
137
|
+
return { before, after: nested(core) };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function nested(core: AgUiThreadController) {
|
|
141
|
+
const answer = core.getMessages().find((m: any) => m.id === "answer") as any;
|
|
142
|
+
const call = answer?.content.find(
|
|
143
|
+
(p: any) => p.type === "tool-call" && p.toolCallId === "task",
|
|
144
|
+
);
|
|
145
|
+
const sub = call?.messages?.find((m: any) => m.id === "sub");
|
|
146
|
+
return {
|
|
147
|
+
answer: answer?.status?.type,
|
|
148
|
+
sub: sub?.status?.type,
|
|
149
|
+
subReason: sub?.status?.reason,
|
|
150
|
+
ncallOpen: sub?.content.some(
|
|
151
|
+
(p: any) => p.toolCallId === "ncall" && p.result === undefined,
|
|
152
|
+
),
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
describe("ag-ui: a nested message waiting on its own call after the run ends", () => {
|
|
157
|
+
for (const end of ["close", "error", "stop"] as const) {
|
|
158
|
+
it(`settles the nested message when the run ends by ${end}`, async () => {
|
|
159
|
+
const { before, after } = await run(end);
|
|
160
|
+
// the snapshot imported the sub-agent's message waiting on ncall
|
|
161
|
+
expect(before.sub, JSON.stringify(before)).toBe("requires-action");
|
|
162
|
+
expect(after.answer, JSON.stringify(after)).toBe("incomplete");
|
|
163
|
+
expect(after.sub, JSON.stringify(after)).not.toBe("requires-action");
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
it("control: after RUN_FINISHED the nested call stays open and is not settled as an error", async () => {
|
|
168
|
+
const { after } = await run("finish");
|
|
169
|
+
expect(after.answer, JSON.stringify(after)).toBe("requires-action");
|
|
170
|
+
expect(after.ncallOpen, JSON.stringify(after)).toBe(true);
|
|
171
|
+
expect(after.subReason, JSON.stringify(after)).not.toBe("error");
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("control: without the snapshot the nested message is the stream's (complete) and settles", async () => {
|
|
175
|
+
const { before, after } = await run("close", false);
|
|
176
|
+
expect(before.sub, JSON.stringify(before)).toBe("complete");
|
|
177
|
+
expect(after.sub, JSON.stringify(after)).not.toBe("requires-action");
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("appends the answer again when its first append fails under a queued update", async () => {
|
|
181
|
+
const writes: string[] = [];
|
|
182
|
+
let headId: string | null = null;
|
|
183
|
+
const rows = new Set<string>();
|
|
184
|
+
let failed = false;
|
|
185
|
+
const history = {
|
|
186
|
+
load: async () => ({ headId: null, messages: [] }),
|
|
187
|
+
append: async (item: any) => {
|
|
188
|
+
const id = item.message.id;
|
|
189
|
+
writes.push(`append ${id}`);
|
|
190
|
+
if (id === "answer" && !failed) {
|
|
191
|
+
failed = true;
|
|
192
|
+
await tick();
|
|
193
|
+
throw new Error("write failed");
|
|
194
|
+
}
|
|
195
|
+
rows.add(id);
|
|
196
|
+
headId = id;
|
|
197
|
+
},
|
|
198
|
+
// Follows the ThreadHistoryAdapter contract: update does not move the head.
|
|
199
|
+
update: async (item: any) => {
|
|
200
|
+
writes.push(`update ${item.message.id}`);
|
|
201
|
+
rows.add(item.message.id);
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
const { after } = await run("error", true, history);
|
|
205
|
+
expect(after.sub, JSON.stringify(after)).not.toBe("requires-action");
|
|
206
|
+
expect(writes, writes.join(", ")).not.toContain("update answer");
|
|
207
|
+
expect(rows.has("answer"), writes.join(", ")).toBe(true);
|
|
208
|
+
expect(headId, writes.join(", ")).toBe("answer");
|
|
209
|
+
});
|
|
210
|
+
});
|