@assistant-ui/react-ag-ui 0.1.4 → 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 +2 -0
- package/dist/runtime/AgUiThreadController.d.ts.map +1 -1
- package/dist/runtime/AgUiThreadController.js +13 -0
- 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.ts +11 -0
- 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",
|
|
@@ -702,6 +702,17 @@ export class AgUiThreadController {
|
|
|
702
702
|
await this.startRun(parentId, config.runConfig);
|
|
703
703
|
}
|
|
704
704
|
|
|
705
|
+
/** Deletes a message that has no replies under it, and its stored history entry. */
|
|
706
|
+
async deleteMessage(messageId: string): Promise<void> {
|
|
707
|
+
const item = this.session.tryGetMessage(messageId);
|
|
708
|
+
if (!item) return;
|
|
709
|
+
const parentId = item.parentId ?? null;
|
|
710
|
+
this.session.deleteMessage(messageId, parentId);
|
|
711
|
+
this.snapshotHistoryIds.delete(messageId);
|
|
712
|
+
this.publishUpdate();
|
|
713
|
+
await this.history?.delete?.([{ parentId, message: item.message }]);
|
|
714
|
+
}
|
|
715
|
+
|
|
705
716
|
async cancel(): Promise<void> {
|
|
706
717
|
this.dispatch({
|
|
707
718
|
type: "historyMutationAdvanced",
|
|
@@ -4,11 +4,13 @@ import { act, cleanup, render, waitFor } from "@testing-library/react";
|
|
|
4
4
|
import { HttpAgent, type RunAgentInput } from "@ag-ui/client";
|
|
5
5
|
import {
|
|
6
6
|
AssistantRuntimeProvider,
|
|
7
|
+
MessageByIndexProvider,
|
|
8
|
+
useActionBarResume,
|
|
7
9
|
useCloudThreadListAdapter,
|
|
8
10
|
useRemoteThreadListRuntime,
|
|
9
11
|
} from "@assistant-ui/core/react";
|
|
10
12
|
import type { AssistantRuntime } from "@assistant-ui/core";
|
|
11
|
-
import { useAui, type AssistantClient } from "@assistant-ui/store";
|
|
13
|
+
import { useAui, useAuiState, type AssistantClient } from "@assistant-ui/store";
|
|
12
14
|
import { AssistantCloud } from "assistant-cloud";
|
|
13
15
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
14
16
|
import { useAgUiRuntime } from "./useAgUiRuntime";
|
|
@@ -107,6 +109,13 @@ const createServer = (
|
|
|
107
109
|
if (row) row.content = body.content;
|
|
108
110
|
return json({});
|
|
109
111
|
}
|
|
112
|
+
if (method === "DELETE" && path === "/threads/remote-1/messages") {
|
|
113
|
+
for (const id of body.message_ids as string[]) {
|
|
114
|
+
const index = rows.findIndex((r) => r.id === id);
|
|
115
|
+
if (index !== -1) rows.splice(index, 1);
|
|
116
|
+
}
|
|
117
|
+
return json({});
|
|
118
|
+
}
|
|
110
119
|
if (/^\/threads\/remote-1\/runs\/[^/]+\/heartbeat$/.test(path))
|
|
111
120
|
return json({ heartbeat_interval_ms: heartbeatIntervalMs });
|
|
112
121
|
if (/^\/threads\/remote-1\/runs\/[^/]+\/end$/.test(path)) return json({});
|
|
@@ -192,6 +201,20 @@ const renderApp = async (staleAfter?: number) => {
|
|
|
192
201
|
aui = useAui();
|
|
193
202
|
return null;
|
|
194
203
|
};
|
|
204
|
+
const resume: { current: ReturnType<typeof useActionBarResume> | undefined } =
|
|
205
|
+
{ current: undefined };
|
|
206
|
+
const CaptureResume = () => {
|
|
207
|
+
resume.current = useActionBarResume();
|
|
208
|
+
return null;
|
|
209
|
+
};
|
|
210
|
+
const ReplyResume = () => {
|
|
211
|
+
const hasReply = useAuiState((s) => s.thread.messages.length >= 2);
|
|
212
|
+
return hasReply ? (
|
|
213
|
+
<MessageByIndexProvider index={1}>
|
|
214
|
+
<CaptureResume />
|
|
215
|
+
</MessageByIndexProvider>
|
|
216
|
+
) : null;
|
|
217
|
+
};
|
|
195
218
|
const App = () => {
|
|
196
219
|
const adapter = useCloudThreadListAdapter({
|
|
197
220
|
cloud,
|
|
@@ -206,6 +229,7 @@ const renderApp = async (staleAfter?: number) => {
|
|
|
206
229
|
return (
|
|
207
230
|
<AssistantRuntimeProvider runtime={runtime}>
|
|
208
231
|
<Capture />
|
|
232
|
+
<ReplyResume />
|
|
209
233
|
</AssistantRuntimeProvider>
|
|
210
234
|
);
|
|
211
235
|
};
|
|
@@ -213,19 +237,25 @@ const renderApp = async (staleAfter?: number) => {
|
|
|
213
237
|
await act(async () => {
|
|
214
238
|
await aui.threads.getLoadThreadsPromise();
|
|
215
239
|
});
|
|
216
|
-
return {
|
|
240
|
+
return {
|
|
241
|
+
agent,
|
|
242
|
+
getAui: () => aui,
|
|
243
|
+
getRuntime: () => runtime,
|
|
244
|
+
getResume: () => resume.current,
|
|
245
|
+
};
|
|
217
246
|
};
|
|
218
247
|
|
|
219
248
|
const assistantRow = (
|
|
220
249
|
last_heartbeat_ms_ago: number,
|
|
221
250
|
status: "running" | "ended" = "running",
|
|
251
|
+
content: readonly unknown[] = [{ type: "text", text: "half an answer" }],
|
|
222
252
|
): Row => ({
|
|
223
253
|
id: "a1",
|
|
224
254
|
parent_id: "u1",
|
|
225
255
|
format: "aui/v0",
|
|
226
256
|
content: {
|
|
227
257
|
role: "assistant",
|
|
228
|
-
content: [
|
|
258
|
+
content: [...content],
|
|
229
259
|
status: { type: "incomplete", reason: "cancelled" },
|
|
230
260
|
},
|
|
231
261
|
external_id: "a1",
|
|
@@ -304,7 +334,50 @@ describe("useAgUiRuntime with Assistant Cloud run leases", () => {
|
|
|
304
334
|
expect(reply.id).toBe("agent-msg-1");
|
|
305
335
|
});
|
|
306
336
|
|
|
307
|
-
it("shows a reloaded run quiet for longer than staleAfter as
|
|
337
|
+
it("shows a reloaded empty run quiet for longer than staleAfter as stale; Resume deletes it and runs the agent again from the user message with no sibling", async () => {
|
|
338
|
+
useServer([userRow, assistantRow(45_000, "running", [])]);
|
|
339
|
+
const { agent, getAui, getRuntime, getResume } = await renderApp();
|
|
340
|
+
await act(async () => {
|
|
341
|
+
await getRuntime().threads.switchToThread("remote-1");
|
|
342
|
+
});
|
|
343
|
+
await waitFor(() =>
|
|
344
|
+
expect(getAui().thread.message({ index: 1 }).getState().runState).toBe(
|
|
345
|
+
"stale",
|
|
346
|
+
),
|
|
347
|
+
);
|
|
348
|
+
await waitFor(() => expect(getResume()?.disabled).toBe(false));
|
|
349
|
+
|
|
350
|
+
await act(async () => {
|
|
351
|
+
getResume()!.resume();
|
|
352
|
+
});
|
|
353
|
+
await waitFor(() => expect(agent.runs).toHaveLength(1));
|
|
354
|
+
await waitFor(() =>
|
|
355
|
+
expect(createsWith(server.calls, true)).toHaveLength(1),
|
|
356
|
+
);
|
|
357
|
+
expect(createsWith(server.calls, true)[0]!.body).toMatchObject({
|
|
358
|
+
parent_id: "u1",
|
|
359
|
+
open_run: true,
|
|
360
|
+
});
|
|
361
|
+
await act(async () => {
|
|
362
|
+
agent.runs[0]!.finish();
|
|
363
|
+
});
|
|
364
|
+
await waitFor(() =>
|
|
365
|
+
expect(
|
|
366
|
+
server.calls.some((c) => c.path.endsWith("/runs/lease_1/end")),
|
|
367
|
+
).toBe(true),
|
|
368
|
+
);
|
|
369
|
+
const deletes = server.calls.filter((c) => c.method === "DELETE");
|
|
370
|
+
expect(deletes.map((c) => c.body)).toEqual([{ message_ids: ["a1"] }]);
|
|
371
|
+
expect(server.rows.map((r) => r.id)).toEqual(["u1", "msg_1"]);
|
|
372
|
+
const messages = getAui().thread.getState().messages;
|
|
373
|
+
expect(messages.map((m) => m.role)).toEqual(["user", "assistant"]);
|
|
374
|
+
expect(messages[1]!.id).not.toBe("a1");
|
|
375
|
+
expect(getAui().thread.message({ index: 1 }).getState().branchCount).toBe(
|
|
376
|
+
1,
|
|
377
|
+
);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it("shows a reloaded run quiet for longer than staleAfter as interrupted; Regenerate posts a sibling with its own lease and runs the agent again", async () => {
|
|
308
381
|
useServer([userRow, assistantRow(45_000)]);
|
|
309
382
|
const { agent, getAui, getRuntime } = await renderApp();
|
|
310
383
|
await act(async () => {
|
|
@@ -314,7 +387,12 @@ describe("useAgUiRuntime with Assistant Cloud run leases", () => {
|
|
|
314
387
|
expect(getAui().thread.getState().messages).toHaveLength(2),
|
|
315
388
|
);
|
|
316
389
|
const stale = getAui().thread.message({ index: 1 });
|
|
317
|
-
await waitFor(() =>
|
|
390
|
+
await waitFor(() =>
|
|
391
|
+
expect(stale.getState().status).toMatchObject({
|
|
392
|
+
error: { code: "run_stale" },
|
|
393
|
+
}),
|
|
394
|
+
);
|
|
395
|
+
expect(stale.getState().runState).toBeUndefined();
|
|
318
396
|
|
|
319
397
|
await act(async () => {
|
|
320
398
|
stale.reload();
|
|
@@ -363,8 +441,8 @@ describe("useAgUiRuntime with Assistant Cloud run leases", () => {
|
|
|
363
441
|
});
|
|
364
442
|
await waitFor(() =>
|
|
365
443
|
expect(
|
|
366
|
-
short.getAui().thread.message({ index: 1 }).getState().
|
|
367
|
-
).
|
|
444
|
+
short.getAui().thread.message({ index: 1 }).getState().status,
|
|
445
|
+
).toMatchObject({ error: { code: "run_stale" } }),
|
|
368
446
|
);
|
|
369
447
|
});
|
|
370
448
|
|
package/src/useAgUiRuntime.ts
CHANGED
|
@@ -345,6 +345,7 @@ export function useAgUiRuntime(
|
|
|
345
345
|
queueController?.clear();
|
|
346
346
|
return core.reload(parentId, config);
|
|
347
347
|
},
|
|
348
|
+
onDelete: (messageId: string) => core.deleteMessage(messageId),
|
|
348
349
|
onCancel: async () => {
|
|
349
350
|
queueController?.notifyCancelled();
|
|
350
351
|
core.cancel();
|