@assistant-ui/react-ag-ui 0.0.57 → 0.0.58
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/README.md +16 -0
- package/dist/runtime/AgUiThreadRuntimeCore.d.ts +3 -4
- package/dist/runtime/AgUiThreadRuntimeCore.d.ts.map +1 -1
- package/dist/runtime/AgUiThreadRuntimeCore.js +29 -43
- package/dist/runtime/AgUiThreadRuntimeCore.js.map +1 -1
- package/dist/runtime/adapter/conversions.d.ts.map +1 -1
- package/dist/runtime/adapter/conversions.js +34 -11
- package/dist/runtime/adapter/conversions.js.map +1 -1
- package/dist/runtime/adapter/run-aggregator.d.ts +13 -3
- package/dist/runtime/adapter/run-aggregator.d.ts.map +1 -1
- package/dist/runtime/adapter/run-aggregator.js +326 -119
- package/dist/runtime/adapter/run-aggregator.js.map +1 -1
- package/dist/runtime/adapter/subscriber.d.ts +9 -0
- package/dist/runtime/adapter/subscriber.d.ts.map +1 -1
- package/dist/runtime/adapter/subscriber.js +4 -1
- package/dist/runtime/adapter/subscriber.js.map +1 -1
- package/dist/runtime/adapter/tool-approval.d.ts +3 -1
- package/dist/runtime/adapter/tool-approval.d.ts.map +1 -1
- package/dist/runtime/adapter/tool-approval.js +11 -12
- package/dist/runtime/adapter/tool-approval.js.map +1 -1
- package/dist/runtime/event-parser.d.ts.map +1 -1
- package/dist/runtime/event-parser.js +176 -45
- package/dist/runtime/event-parser.js.map +1 -1
- package/dist/runtime/types.d.ts +42 -1
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/useAgUiRuntime.js +1 -0
- package/dist/useAgUiRuntime.js.map +1 -1
- package/package.json +9 -9
- package/src/runtime/AgUiThreadRuntimeCore.ts +40 -60
- package/src/runtime/adapter/conversions.ts +87 -17
- package/src/runtime/adapter/run-aggregator.approval.test.ts +128 -0
- package/src/runtime/adapter/run-aggregator.ts +510 -139
- package/src/runtime/adapter/subscriber.ts +10 -1
- package/src/runtime/adapter/tool-approval.test.ts +75 -0
- package/src/runtime/adapter/tool-approval.ts +17 -12
- package/src/runtime/event-parser.ts +216 -36
- package/src/runtime/types.ts +70 -12
- package/src/useAgUiRuntime.approval.test.tsx +34 -24
- package/src/useAgUiRuntime.toolDeferral.test.tsx +186 -0
- package/src/useAgUiRuntime.ts +1 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useAgUiRuntime.js","names":[],"sources":["../src/useAgUiRuntime.ts"],"sourcesContent":["\"use client\";\n\nimport {\n useCallback,\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 { 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 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 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 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 } else if (!options.unstable_enableMessageQueue && queueRef.current) {\n queueRef.current.clear();\n queueRef.current = null;\n }\n const queueController = options.unstable_enableMessageQueue\n ? queueRef.current\n : null;\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 await onSwitchToNewThread();\n core.applyExternalMessages([]);\n core.resetState();\n }\n : undefined,\n onSwitchToThread: onSwitchToThread\n ? async (threadId: string) => {\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 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 }),\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 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 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 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 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 } else if (!options.unstable_enableMessageQueue && queueRef.current) {\n queueRef.current.clear();\n queueRef.current = null;\n }\n const queueController = options.unstable_enableMessageQueue\n ? queueRef.current\n : null;\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 await onSwitchToNewThread();\n core.applyExternalMessages([]);\n core.resetState();\n }\n : undefined,\n onSwitchToThread: onSwitchToThread\n ? async (threadId: string) => {\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 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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"}
|
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.58",
|
|
4
4
|
"description": "AG-UI protocol adapter for assistant-ui",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ag-ui",
|
|
@@ -29,11 +29,11 @@
|
|
|
29
29
|
],
|
|
30
30
|
"sideEffects": false,
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@ag-ui/client": "^0.0.
|
|
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.
|
|
32
|
+
"@ag-ui/client": "^0.0.59",
|
|
33
|
+
"@assistant-ui/core": "^0.3.17",
|
|
34
|
+
"@assistant-ui/react-generative-ui": "^0.0.17",
|
|
35
|
+
"@assistant-ui/store": "^0.3.12",
|
|
36
|
+
"assistant-stream": "^0.3.41",
|
|
37
37
|
"fast-json-patch": "^3.1.1"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
@@ -46,14 +46,14 @@
|
|
|
46
46
|
}
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
|
-
"@assistant-ui/x-buildutils": "0.0.
|
|
49
|
+
"@assistant-ui/x-buildutils": "0.0.25",
|
|
50
50
|
"@testing-library/dom": "^10.4.1",
|
|
51
|
-
"@testing-library/react": "^16.3.
|
|
51
|
+
"@testing-library/react": "^16.3.3",
|
|
52
52
|
"@types/react": "^19.2.18",
|
|
53
53
|
"jsdom": "^30.0.1",
|
|
54
54
|
"react": "^19.2.8",
|
|
55
55
|
"vitest": "^4.1.11",
|
|
56
|
-
"zod": "^4.4
|
|
56
|
+
"zod": "^4.5.4"
|
|
57
57
|
},
|
|
58
58
|
"publishConfig": {
|
|
59
59
|
"access": "public",
|
|
@@ -23,6 +23,8 @@ import type {
|
|
|
23
23
|
import {
|
|
24
24
|
createMessageRepositorySession,
|
|
25
25
|
invokeUserCallback,
|
|
26
|
+
iterateToolCallParts,
|
|
27
|
+
mapToolCallPartsDeep,
|
|
26
28
|
} from "@assistant-ui/core/internal";
|
|
27
29
|
import type {
|
|
28
30
|
AbstractAgent,
|
|
@@ -74,8 +76,6 @@ const isResolvedToolCall = (
|
|
|
74
76
|
): boolean =>
|
|
75
77
|
part.type === "tool-call" && "result" in part && part.result !== undefined;
|
|
76
78
|
|
|
77
|
-
const symbolResumeShim = Symbol("agui-resume-shim");
|
|
78
|
-
|
|
79
79
|
type RunConfig = NonNullable<AppendMessage["runConfig"]>;
|
|
80
80
|
type ResumeStream = (
|
|
81
81
|
options: ChatModelRunOptions,
|
|
@@ -187,7 +187,6 @@ export class AgUiThreadRuntimeCore {
|
|
|
187
187
|
this.onCancel = options.onCancel;
|
|
188
188
|
this.history = options.history;
|
|
189
189
|
this.notifyUpdate = options.notifyUpdate;
|
|
190
|
-
this.installResumeShim();
|
|
191
190
|
}
|
|
192
191
|
|
|
193
192
|
updateOptions(options: Omit<CoreOptions, "notifyUpdate">) {
|
|
@@ -199,8 +198,6 @@ export class AgUiThreadRuntimeCore {
|
|
|
199
198
|
this.onCancel = options.onCancel;
|
|
200
199
|
const previousHistory = this.history;
|
|
201
200
|
this.history = options.history;
|
|
202
|
-
this.installResumeShim();
|
|
203
|
-
|
|
204
201
|
if (
|
|
205
202
|
this._loadRequested &&
|
|
206
203
|
!this._loadPromise &&
|
|
@@ -439,8 +436,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
439
436
|
const assistant = this.findRequiresActionAssistant("tool-calls");
|
|
440
437
|
if (!assistant) return null;
|
|
441
438
|
const toolCallIds: string[] = [];
|
|
442
|
-
for (const part of assistant.content) {
|
|
443
|
-
if (part.type !== "tool-call") continue;
|
|
439
|
+
for (const part of iterateToolCallParts(assistant.content)) {
|
|
444
440
|
if (isResolvedToolCall(part)) continue;
|
|
445
441
|
toolCallIds.push(part.toolCallId);
|
|
446
442
|
}
|
|
@@ -498,9 +494,9 @@ export class AgUiThreadRuntimeCore {
|
|
|
498
494
|
}
|
|
499
495
|
|
|
500
496
|
/**
|
|
501
|
-
*
|
|
502
|
-
*
|
|
503
|
-
*
|
|
497
|
+
* A rejected decision reaches the caller through the approval seam, and
|
|
498
|
+
* `onError` in addition, because a consumer watching only the runtime would
|
|
499
|
+
* otherwise miss it. A failure raised by the resumed run itself was already
|
|
504
500
|
* reported by `startRun` before it rethrew, so reporting it here again would
|
|
505
501
|
* give the consumer two notifications for one failure.
|
|
506
502
|
*/
|
|
@@ -557,9 +553,9 @@ export class AgUiThreadRuntimeCore {
|
|
|
557
553
|
const gated = projectAgUiToolApprovals(
|
|
558
554
|
pending.interrupts,
|
|
559
555
|
new Set(
|
|
560
|
-
(gatedMessage?.content ?? [])
|
|
561
|
-
|
|
562
|
-
|
|
556
|
+
Array.from(iterateToolCallParts(gatedMessage?.content ?? [])).map(
|
|
557
|
+
(part) => part.toolCallId,
|
|
558
|
+
),
|
|
563
559
|
),
|
|
564
560
|
);
|
|
565
561
|
const isGated = [...gated.values()].some(
|
|
@@ -774,9 +770,8 @@ export class AgUiThreadRuntimeCore {
|
|
|
774
770
|
for (let index = messages.length - 1; index >= 0; index--) {
|
|
775
771
|
const message = messages[index];
|
|
776
772
|
if (!message || message.role !== "assistant") continue;
|
|
777
|
-
for (const part of message.content) {
|
|
778
|
-
if (part.
|
|
779
|
-
continue;
|
|
773
|
+
for (const part of iterateToolCallParts(message.content)) {
|
|
774
|
+
if (part.toolCallId !== toolCallId) continue;
|
|
780
775
|
if (!isResolvedToolCall(part)) {
|
|
781
776
|
return message.id;
|
|
782
777
|
}
|
|
@@ -790,8 +785,8 @@ export class AgUiThreadRuntimeCore {
|
|
|
790
785
|
const updated = this.session.updateMessage(messageId, (message) => {
|
|
791
786
|
if (message.role !== "assistant") return message;
|
|
792
787
|
const assistant = message as ThreadAssistantMessage;
|
|
793
|
-
const content = assistant.content
|
|
794
|
-
if (
|
|
788
|
+
const { content } = mapToolCallPartsDeep(assistant.content, (part) => {
|
|
789
|
+
if (isResolvedToolCall(part)) return part;
|
|
795
790
|
return {
|
|
796
791
|
...part,
|
|
797
792
|
result: { error: "Tool call cancelled by user" },
|
|
@@ -804,13 +799,19 @@ export class AgUiThreadRuntimeCore {
|
|
|
804
799
|
}
|
|
805
800
|
|
|
806
801
|
addToolResult(options: AddToolResultOptions): void {
|
|
807
|
-
|
|
802
|
+
// Core's ToolInvocationTracker resolves a nested call to the nested
|
|
803
|
+
// subagent message's id, which is not a session message; re-anchor on the
|
|
804
|
+
// top-level message that owns the tree so the update can land.
|
|
805
|
+
const sessionMessageId = this.session.tryGetMessage(options.messageId)
|
|
806
|
+
? options.messageId
|
|
807
|
+
: this.findMessageIdForToolCall(options.toolCallId);
|
|
808
|
+
if (sessionMessageId === undefined) return;
|
|
809
|
+
const updated = this.session.updateMessage(sessionMessageId, (message) => {
|
|
808
810
|
if (message.role !== "assistant") return message;
|
|
809
811
|
const assistant = message as ThreadAssistantMessage;
|
|
810
812
|
let matchedToolCall = false;
|
|
811
|
-
const content = assistant.content
|
|
812
|
-
if (part.
|
|
813
|
-
return part;
|
|
813
|
+
const { content } = mapToolCallPartsDeep(assistant.content, (part) => {
|
|
814
|
+
if (part.toolCallId !== options.toolCallId) return part;
|
|
814
815
|
matchedToolCall = true;
|
|
815
816
|
return {
|
|
816
817
|
...part,
|
|
@@ -825,7 +826,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
825
826
|
|
|
826
827
|
if (!updated) return;
|
|
827
828
|
this.notifyUpdate();
|
|
828
|
-
this.maybeResumeAfterToolResults(
|
|
829
|
+
this.maybeResumeAfterToolResults(sessionMessageId);
|
|
829
830
|
}
|
|
830
831
|
|
|
831
832
|
sendA2uiAction(action: Record<string, unknown>): void {
|
|
@@ -878,9 +879,13 @@ export class AgUiThreadRuntimeCore {
|
|
|
878
879
|
) {
|
|
879
880
|
return false;
|
|
880
881
|
}
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
882
|
+
let allResolved = true;
|
|
883
|
+
for (const part of iterateToolCallParts(assistant.content)) {
|
|
884
|
+
if (!isResolvedToolCall(part)) {
|
|
885
|
+
allResolved = false;
|
|
886
|
+
break;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
884
889
|
if (!allResolved) return false;
|
|
885
890
|
|
|
886
891
|
const updated = this.session.updateMessage(messageId, (current) =>
|
|
@@ -1319,27 +1324,6 @@ export class AgUiThreadRuntimeCore {
|
|
|
1319
1324
|
return input;
|
|
1320
1325
|
}
|
|
1321
1326
|
|
|
1322
|
-
private installResumeShim(): void {
|
|
1323
|
-
const agent = this.agent as any;
|
|
1324
|
-
if (agent[symbolResumeShim]) return;
|
|
1325
|
-
agent[symbolResumeShim] = true;
|
|
1326
|
-
const onInstance = Object.hasOwn(agent, "prepareRunAgentInput");
|
|
1327
|
-
const original = onInstance
|
|
1328
|
-
? agent.prepareRunAgentInput
|
|
1329
|
-
: Object.getPrototypeOf(agent)?.prepareRunAgentInput;
|
|
1330
|
-
if (typeof original !== "function") return;
|
|
1331
|
-
agent.prepareRunAgentInput = function (
|
|
1332
|
-
this: unknown,
|
|
1333
|
-
params: { resume?: unknown } | undefined,
|
|
1334
|
-
) {
|
|
1335
|
-
const input = original.call(this, params);
|
|
1336
|
-
if (params?.resume !== undefined && input && typeof input === "object") {
|
|
1337
|
-
return { ...(input as object), resume: params.resume };
|
|
1338
|
-
}
|
|
1339
|
-
return input;
|
|
1340
|
-
};
|
|
1341
|
-
}
|
|
1342
|
-
|
|
1343
1327
|
private setRunning(running: boolean) {
|
|
1344
1328
|
this.isRunningFlag = running;
|
|
1345
1329
|
this.notifyUpdate();
|
|
@@ -1498,19 +1482,17 @@ export class AgUiThreadRuntimeCore {
|
|
|
1498
1482
|
next: ThreadAssistantMessage["content"],
|
|
1499
1483
|
): ThreadAssistantMessage["content"] {
|
|
1500
1484
|
const resolved = new Map<string, ToolCallMessagePart>();
|
|
1501
|
-
for (const part of previous) {
|
|
1502
|
-
if (
|
|
1485
|
+
for (const part of iterateToolCallParts(previous)) {
|
|
1486
|
+
if (isResolvedToolCall(part)) {
|
|
1503
1487
|
resolved.set(part.toolCallId, part);
|
|
1504
1488
|
}
|
|
1505
1489
|
}
|
|
1506
1490
|
if (resolved.size === 0) return next;
|
|
1507
1491
|
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
if (part.type !== "tool-call" || isResolvedToolCall(part)) return part;
|
|
1492
|
+
const { content: merged, changed } = mapToolCallPartsDeep(next, (part) => {
|
|
1493
|
+
if (isResolvedToolCall(part)) return part;
|
|
1511
1494
|
const prior = resolved.get(part.toolCallId);
|
|
1512
1495
|
if (!prior) return part;
|
|
1513
|
-
changed = true;
|
|
1514
1496
|
return {
|
|
1515
1497
|
...part,
|
|
1516
1498
|
result: prior.result,
|
|
@@ -1518,7 +1500,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
1518
1500
|
...(prior.isError !== undefined ? { isError: prior.isError } : {}),
|
|
1519
1501
|
};
|
|
1520
1502
|
});
|
|
1521
|
-
return changed ?
|
|
1503
|
+
return changed ? merged : next;
|
|
1522
1504
|
}
|
|
1523
1505
|
|
|
1524
1506
|
private mergeAssistantMetadata(
|
|
@@ -1623,9 +1605,8 @@ export class AgUiThreadRuntimeCore {
|
|
|
1623
1605
|
const assistant = message as ThreadAssistantMessage;
|
|
1624
1606
|
const mcpAppUri = readMcpAppResourceUri(event.mcpResult?._meta);
|
|
1625
1607
|
let matchedToolCall = false;
|
|
1626
|
-
const content = assistant.content
|
|
1627
|
-
if (part.
|
|
1628
|
-
return part;
|
|
1608
|
+
const { content } = mapToolCallPartsDeep(assistant.content, (part) => {
|
|
1609
|
+
if (part.toolCallId !== event.toolCallId) return part;
|
|
1629
1610
|
matchedToolCall = true;
|
|
1630
1611
|
// An applied activity snapshot owns part.result; a later result only
|
|
1631
1612
|
// fills what is missing, mirroring the aggregator's finishToolCall.
|
|
@@ -1709,9 +1690,8 @@ export class AgUiThreadRuntimeCore {
|
|
|
1709
1690
|
if (message.role !== "assistant") return message;
|
|
1710
1691
|
const assistant = message as ThreadAssistantMessage;
|
|
1711
1692
|
let matchedToolCall = false;
|
|
1712
|
-
const content = assistant.content
|
|
1713
|
-
if (part.
|
|
1714
|
-
return part;
|
|
1693
|
+
const { content } = mapToolCallPartsDeep(assistant.content, (part) => {
|
|
1694
|
+
if (part.toolCallId !== toolCallId) return part;
|
|
1715
1695
|
matchedToolCall = true;
|
|
1716
1696
|
return {
|
|
1717
1697
|
...part,
|
|
@@ -5,6 +5,7 @@ import { generateId } from "@assistant-ui/core";
|
|
|
5
5
|
import type {
|
|
6
6
|
ThreadMessageLike as CoreThreadMessageLike,
|
|
7
7
|
PartProviderMetadata,
|
|
8
|
+
ThreadMessage,
|
|
8
9
|
ToolCallMessagePartMcpMetadata,
|
|
9
10
|
ToolModelContentPart,
|
|
10
11
|
} from "@assistant-ui/core";
|
|
@@ -106,8 +107,17 @@ type ToolCallPart = {
|
|
|
106
107
|
modelContent?: readonly ToolModelContentPart[];
|
|
107
108
|
unstable_toolMessageId?: string;
|
|
108
109
|
mcp?: ToolCallMessagePartMcpMetadata;
|
|
110
|
+
messages?: readonly ThreadMessage[];
|
|
111
|
+
approval?: CoreToolCallPartApproval;
|
|
109
112
|
};
|
|
110
113
|
|
|
114
|
+
type CoreToolCallPartApproval = NonNullable<
|
|
115
|
+
Extract<
|
|
116
|
+
Exclude<CoreThreadMessageLike["content"], string>[number],
|
|
117
|
+
{ type: "tool-call" }
|
|
118
|
+
>["approval"]
|
|
119
|
+
>;
|
|
120
|
+
|
|
111
121
|
const isObject = (value: unknown): value is Record<string, unknown> =>
|
|
112
122
|
typeof value === "object" && value !== null;
|
|
113
123
|
|
|
@@ -1051,36 +1061,96 @@ function convertAssistantMessage(
|
|
|
1051
1061
|
return;
|
|
1052
1062
|
}
|
|
1053
1063
|
|
|
1064
|
+
// A subagent's tool calls live on nested assistant messages. Before
|
|
1065
|
+
// subagent attribution they flattened to root and went out with this
|
|
1066
|
+
// assistant record as their antecedent, so the resume payload restores
|
|
1067
|
+
// exactly that shape: the calls join this record's toolCalls and their
|
|
1068
|
+
// results follow as tool records — never a tool record without its call.
|
|
1069
|
+
// The nested assistant content itself is backend-owned state and is not
|
|
1070
|
+
// re-sent.
|
|
1071
|
+
const nestedToolCalls: {
|
|
1072
|
+
id: string;
|
|
1073
|
+
call: AgUiToolCall;
|
|
1074
|
+
part: ToolCallPart;
|
|
1075
|
+
}[] = [];
|
|
1076
|
+
for (const { part } of toolCalls) {
|
|
1077
|
+
collectNestedToolCalls(part, nestedToolCalls);
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1054
1080
|
converted.push({
|
|
1055
1081
|
id: message.id,
|
|
1056
1082
|
role: "assistant",
|
|
1057
1083
|
content,
|
|
1058
1084
|
...(message.name ? { name: message.name } : {}),
|
|
1059
|
-
...(toolCalls.length > 0
|
|
1060
|
-
? {
|
|
1085
|
+
...(toolCalls.length + nestedToolCalls.length > 0
|
|
1086
|
+
? {
|
|
1087
|
+
toolCalls: [...toolCalls, ...nestedToolCalls].map(
|
|
1088
|
+
(entry) => entry.call,
|
|
1089
|
+
),
|
|
1090
|
+
}
|
|
1061
1091
|
: {}),
|
|
1062
1092
|
});
|
|
1063
1093
|
|
|
1064
1094
|
for (const { id: toolCallId, part } of toolCalls) {
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1095
|
+
emitToolResult(toolCallId, part, converted);
|
|
1096
|
+
}
|
|
1097
|
+
for (const { id: toolCallId, part } of nestedToolCalls) {
|
|
1098
|
+
// A result recorded while the call's approval gate is still open must not
|
|
1099
|
+
// reach the backend as if the gate had been decided.
|
|
1100
|
+
const gateOpen =
|
|
1101
|
+
part.approval != null &&
|
|
1102
|
+
part.approval.approved === undefined &&
|
|
1103
|
+
part.approval.resolution === undefined;
|
|
1104
|
+
if (gateOpen) continue;
|
|
1105
|
+
emitToolResult(toolCallId, part, converted);
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1073
1108
|
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1109
|
+
function collectNestedToolCalls(
|
|
1110
|
+
part: ToolCallPart,
|
|
1111
|
+
out: { id: string; call: AgUiToolCall; part: ToolCallPart }[],
|
|
1112
|
+
): void {
|
|
1113
|
+
for (const nested of part.messages ?? []) {
|
|
1114
|
+
if (!isObject(nested) || nested.role !== "assistant") continue;
|
|
1115
|
+
const nestedContent = Array.isArray(nested.content) ? nested.content : [];
|
|
1116
|
+
for (const nestedPart of nestedContent) {
|
|
1117
|
+
if (!isObject(nestedPart) || nestedPart.type !== "tool-call") continue;
|
|
1118
|
+
const nestedToolCall = nestedPart as ToolCallPart;
|
|
1119
|
+
if (
|
|
1120
|
+
typeof nestedToolCall.toolCallId !== "string" ||
|
|
1121
|
+
nestedToolCall.toolCallId.startsWith("a2ui:")
|
|
1122
|
+
) {
|
|
1123
|
+
continue;
|
|
1124
|
+
}
|
|
1125
|
+
out.push({ ...normalizeToolCall(nestedToolCall), part: nestedToolCall });
|
|
1126
|
+
collectNestedToolCalls(nestedToolCall, out);
|
|
1127
|
+
}
|
|
1081
1128
|
}
|
|
1082
1129
|
}
|
|
1083
1130
|
|
|
1131
|
+
function emitToolResult(
|
|
1132
|
+
toolCallId: string,
|
|
1133
|
+
part: ToolCallPart,
|
|
1134
|
+
converted: AgUiMessage[],
|
|
1135
|
+
): void {
|
|
1136
|
+
if (part.result === undefined) return;
|
|
1137
|
+
|
|
1138
|
+
const resultContent =
|
|
1139
|
+
part.modelContent !== undefined
|
|
1140
|
+
? extractText(part.modelContent)
|
|
1141
|
+
: typeof part.result === "string"
|
|
1142
|
+
? part.result
|
|
1143
|
+
: JSON.stringify(part.result);
|
|
1144
|
+
|
|
1145
|
+
converted.push({
|
|
1146
|
+
id: part.unstable_toolMessageId ?? `${toolCallId}:tool`,
|
|
1147
|
+
role: "tool",
|
|
1148
|
+
content: resultContent,
|
|
1149
|
+
toolCallId,
|
|
1150
|
+
...(part.isError ? { error: resultContent } : {}),
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1084
1154
|
function convertToolMessage(
|
|
1085
1155
|
message: NormalizedThreadMessageLike,
|
|
1086
1156
|
converted: AgUiMessage[],
|
|
@@ -100,4 +100,132 @@ describe("RunAggregator tool approval projection", () => {
|
|
|
100
100
|
?.interrupts,
|
|
101
101
|
).toEqual([GATE]);
|
|
102
102
|
});
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* `boundToolCallIds` names every call the run produced, root or nested, so a
|
|
106
|
+
* gate on a call inside a subagent message is bound and projects onto the
|
|
107
|
+
* nested part rather than collapsing the batch. The approval seams walk
|
|
108
|
+
* `ToolCallMessagePart.messages`, which is what makes a subagent-scoped gate
|
|
109
|
+
* answerable the same way a root one is.
|
|
110
|
+
*/
|
|
111
|
+
it("projects a batch that names a subagent-scoped tool call onto both the root and nested parts", () => {
|
|
112
|
+
const emitted: ChatModelRunResult[] = [];
|
|
113
|
+
const aggregator = new RunAggregator({
|
|
114
|
+
showThinking: false,
|
|
115
|
+
logger: noopLogger,
|
|
116
|
+
emit: (result) => {
|
|
117
|
+
emitted.push(result);
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const ROOT_GATE: AgUiInterrupt = {
|
|
122
|
+
id: "int-root",
|
|
123
|
+
reason: "tool_call",
|
|
124
|
+
toolCallId: "tc-root",
|
|
125
|
+
message: "Delete /tmp/root?",
|
|
126
|
+
};
|
|
127
|
+
const SUBAGENT_GATE: AgUiInterrupt = {
|
|
128
|
+
id: "int-sub",
|
|
129
|
+
reason: "tool_call",
|
|
130
|
+
toolCallId: "tc-sub",
|
|
131
|
+
message: "Delete /tmp/sub?",
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
aggregator.handle({ type: "RUN_STARTED", runId: "run-1" });
|
|
135
|
+
aggregator.handle({
|
|
136
|
+
type: "TOOL_CALL_START",
|
|
137
|
+
toolCallId: "tc-root",
|
|
138
|
+
toolCallName: "delete_file",
|
|
139
|
+
});
|
|
140
|
+
aggregator.handle({
|
|
141
|
+
type: "TOOL_CALL_ARGS",
|
|
142
|
+
toolCallId: "tc-root",
|
|
143
|
+
delta: '{"path":"/tmp/root"}',
|
|
144
|
+
});
|
|
145
|
+
aggregator.handle({ type: "TOOL_CALL_END", toolCallId: "tc-root" });
|
|
146
|
+
aggregator.handle({
|
|
147
|
+
type: "SUBAGENT_STARTED",
|
|
148
|
+
subagentRunId: "sub-1",
|
|
149
|
+
name: "cleanup",
|
|
150
|
+
parentToolCallId: "tc-root",
|
|
151
|
+
} as any);
|
|
152
|
+
aggregator.handle({
|
|
153
|
+
type: "TOOL_CALL_START",
|
|
154
|
+
toolCallId: "tc-sub",
|
|
155
|
+
toolCallName: "delete_file",
|
|
156
|
+
subagentRunId: "sub-1",
|
|
157
|
+
} as any);
|
|
158
|
+
aggregator.handle({
|
|
159
|
+
type: "TOOL_CALL_ARGS",
|
|
160
|
+
toolCallId: "tc-sub",
|
|
161
|
+
delta: '{"path":"/tmp/sub"}',
|
|
162
|
+
subagentRunId: "sub-1",
|
|
163
|
+
} as any);
|
|
164
|
+
aggregator.handle({
|
|
165
|
+
type: "TOOL_CALL_END",
|
|
166
|
+
toolCallId: "tc-sub",
|
|
167
|
+
subagentRunId: "sub-1",
|
|
168
|
+
} as any);
|
|
169
|
+
aggregator.handle({
|
|
170
|
+
type: "RUN_FINISHED",
|
|
171
|
+
runId: "run-1",
|
|
172
|
+
outcome: {
|
|
173
|
+
type: "interrupt",
|
|
174
|
+
interrupts: [ROOT_GATE, SUBAGENT_GATE],
|
|
175
|
+
},
|
|
176
|
+
} as any);
|
|
177
|
+
|
|
178
|
+
const result = emitted[emitted.length - 1]!;
|
|
179
|
+
expect(result.status).toMatchObject({
|
|
180
|
+
type: "requires-action",
|
|
181
|
+
reason: "interrupt",
|
|
182
|
+
});
|
|
183
|
+
const content = result.content ?? [];
|
|
184
|
+
const rootPart = content.find(
|
|
185
|
+
(p: any) => p.type === "tool-call" && p.toolCallId === "tc-root",
|
|
186
|
+
) as any;
|
|
187
|
+
expect(rootPart?.approval).toEqual({
|
|
188
|
+
id: "int-root",
|
|
189
|
+
prompt: "Delete /tmp/root?",
|
|
190
|
+
});
|
|
191
|
+
const nestedPart = rootPart?.messages?.[0]?.content.find(
|
|
192
|
+
(p: any) => p.type === "tool-call" && p.toolCallId === "tc-sub",
|
|
193
|
+
);
|
|
194
|
+
expect(nestedPart?.approval).toEqual({
|
|
195
|
+
id: "int-sub",
|
|
196
|
+
prompt: "Delete /tmp/sub?",
|
|
197
|
+
});
|
|
198
|
+
// The interrupts survive on the message metadata so the app's bespoke
|
|
199
|
+
// interrupt hooks (useAgUiSubmitInterruptResponses / steerAway) can still
|
|
200
|
+
// resolve the batch.
|
|
201
|
+
expect(
|
|
202
|
+
(result.metadata?.custom as Record<string, any> | undefined)?.agui
|
|
203
|
+
?.interrupts,
|
|
204
|
+
).toEqual([ROOT_GATE, SUBAGENT_GATE]);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("still projects a root gate whose batch is entirely root-scoped, even while an unrelated subagent tool call is in flight", () => {
|
|
208
|
+
const { aggregator, last } = gatedAggregator();
|
|
209
|
+
|
|
210
|
+
aggregator.handle({
|
|
211
|
+
type: "SUBAGENT_STARTED",
|
|
212
|
+
subagentRunId: "sub-1",
|
|
213
|
+
name: "cleanup",
|
|
214
|
+
} as any);
|
|
215
|
+
aggregator.handle({
|
|
216
|
+
type: "TOOL_CALL_START",
|
|
217
|
+
toolCallId: "tc-unrelated",
|
|
218
|
+
toolCallName: "noop",
|
|
219
|
+
subagentRunId: "sub-1",
|
|
220
|
+
} as any);
|
|
221
|
+
|
|
222
|
+
expect(last().status).toMatchObject({
|
|
223
|
+
type: "requires-action",
|
|
224
|
+
reason: "interrupt",
|
|
225
|
+
});
|
|
226
|
+
expect(approvalOf(last())).toEqual({
|
|
227
|
+
id: "int-1",
|
|
228
|
+
prompt: "Delete /tmp/a?",
|
|
229
|
+
});
|
|
230
|
+
});
|
|
103
231
|
});
|