@assistant-ui/react-ag-ui 0.0.69 → 0.0.71
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 +1 -1
- package/dist/runtime/AgUiThreadRuntimeCore.d.ts +6 -3
- package/dist/runtime/AgUiThreadRuntimeCore.d.ts.map +1 -1
- package/dist/runtime/AgUiThreadRuntimeCore.js +77 -10
- package/dist/runtime/AgUiThreadRuntimeCore.js.map +1 -1
- package/dist/runtime/adapter/conversions.d.ts +5 -0
- package/dist/runtime/adapter/conversions.d.ts.map +1 -1
- package/dist/runtime/adapter/conversions.js +104 -15
- package/dist/runtime/adapter/conversions.js.map +1 -1
- package/dist/runtime/adapter/run-aggregator.d.ts +3 -0
- package/dist/runtime/adapter/run-aggregator.d.ts.map +1 -1
- package/dist/runtime/adapter/run-aggregator.js +4 -2
- package/dist/runtime/adapter/run-aggregator.js.map +1 -1
- package/dist/runtime/types.d.ts +18 -2
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/runtime/types.js.map +1 -1
- package/dist/useAgUiRuntime.d.ts.map +1 -1
- package/dist/useAgUiRuntime.js +4 -0
- package/dist/useAgUiRuntime.js.map +1 -1
- package/package.json +5 -5
- package/src/runtime/AgUiThreadRuntimeCore.interrupt-cancel.test.ts +202 -0
- package/src/runtime/AgUiThreadRuntimeCore.ts +143 -8
- package/src/runtime/adapter/conversions.metadata.test.ts +400 -0
- package/src/runtime/adapter/conversions.ts +170 -28
- package/src/runtime/adapter/run-aggregator.approval.test.ts +4 -4
- package/src/runtime/adapter/run-aggregator.ts +13 -2
- package/src/runtime/types.ts +20 -1
- package/src/useAgUiRuntime.context.test.tsx +227 -0
- package/src/useAgUiRuntime.external-store.test.tsx +39 -0
- package/src/useAgUiRuntime.ts +4 -0
|
@@ -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 { 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 coreRef = useRef<AgUiThreadRuntimeCore | 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 AgUiThreadRuntimeCore({\n agent: options.agent,\n logger,\n showThinking: options.showThinking ?? true,\n resumeTranscript: options.resumeTranscript,\n autoCancelPendingToolCalls: options.autoCancelPendingToolCalls,\n ...(options.onError && { onError: options.onError }),\n ...(options.onCancel && { onCancel: options.onCancel }),\n ...(historyAdapter && { history: historyAdapter }),\n 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 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":";;;;;;;;;AAwCA,MAAM,mBAA6C,CAAC;AACpD,MAAM,oBAA+C,OAAO,OAAO,CAAC,CAAC;AACrE,MAAM,4BAA4B,CAAC;AAkBnC,SAAgB,eACd,SACsB;CACtB,MAAM,SAAS,cAAc,WAAW,QAAQ,MAAM,GAAG,CAAC,QAAQ,MAAM,CAAC;CACzE,MAAM,UAAU,OAAqC,IAAI;CACzD,MAAM,4BAA4B,OAAO,CAAC;CAC1C,MAAM,yBAAyB,OAAsB,IAAI;CACzD,MAAM,kBAAkB,mBAAmB;CAE3C,MAAM,iBAAiB,QAAQ,UAAU,WAAW,iBAAiB;CACrE,MAAM,oBAAoB,QAAQ,UAAU;CAE5C,IAAI,CAAC,QAAQ,SACX,QAAQ,UAAU,IAAI,sBAAsB;EAC1C,OAAO,QAAQ;EACf;EACA,cAAc,QAAQ,gBAAgB;EACtC,kBAAkB,QAAQ;EAC1B,4BAA4B,QAAQ;EACpC,GAAI,QAAQ,WAAW,EAAE,SAAS,QAAQ,QAAQ;EAClD,GAAI,QAAQ,YAAY,EAAE,UAAU,QAAQ,SAAS;EACrD,GAAI,kBAAkB,EAAE,SAAS,eAAe;EAChD,yBAAyB,uBAAuB,YAAY;CAC9D,CAAC;CAGH,MAAM,OAAO,QAAQ;CACrB,MAAM,WAAW,qBACf,KAAK,WACL,KAAK,aACL,KAAK,WACP;CACA,gBAAgB;EACd,KAAK,cAAc;GACjB,OAAO,QAAQ;GACf;GACA,cAAc,QAAQ,gBAAgB;GACtC,kBAAkB,QAAQ;GAC1B,4BAA4B,QAAQ;GACpC,GAAI,QAAQ,WAAW,EAAE,SAAS,QAAQ,QAAQ;GAClD,GAAI,QAAQ,YAAY,EAAE,UAAU,QAAQ,SAAS;GACrD,GAAI,kBAAkB,EAAE,SAAS,eAAe;EAClD,CAAC;CACH,CAAC;CAED,MAAM,CAAC,cAAc,mBAAmB,SAEtC,CAAC,CAAC;CAEJ,MAAM,oBAAoB,OAAO,OAAO,YAAY,CAAC,CAAC,MACnD,MAAM,GAAG,SAAS,WACrB;CACA,MAAM,YAAY,SAAS,aAAa;CAIxC,MAAM,WAAW,OAAsC,IAAI;CAI3D,MAAM,eAAe,OAAO,CAAC;CAC7B,MAAM,mBAAmB,OAGf,IAAI;CACd,IAAI,QAAQ,+BAA+B,CAAC,SAAS,SACnD,SAAS,UAAU,mBAAmB,EACpC,MAAM,YAAY;EAChB,MAAM,aAAa,SAAS;EAG5B,IAAI,uBAAuB,YAAY,MAAM;GAC3C,YAAY,WAAW;GACvB;EACF;EACA,MAAM,kBAAkB,aAAa;EAQrC,MAAM,uBAAuB;GAC3B,IACE,SAAS,YAAY,cACrB,aAAa,YAAY,mBACzB,KAAK,qBAAqB,MAAM,MAEhC;GACF,YAAY,WAAW;EACzB;EACA,KAAU,OAAO,OAAO,CAAC,CAAC,KAAK,iBAAiB,MAAe;GAC7D,eAAe;GACf,OAAO,QAAQ,wCAAwC,CAAC;EAC1D,CAAC;CACH,EACF,CAAC;CAEH,MAAM,kBAAkB,QAAQ,8BAC5B,SAAS,UACT;CACJ,gCAAgC;EAC9B,IAAI,QAAQ,+BAA+B,CAAC,SAAS,SAAS;EAC9D,MAAM,aAAa,SAAS;EAC5B,SAAS,UAAU;EACnB,WAAW,MAAM;CACnB,GAAG,CAAC,QAAQ,2BAA2B,CAAC;CAKxC,MAAM,aAAa,qBACjB,iBAAiB,aAAa,qBACxB,iBAAiB,QAAQ,SAAS,yBAClC,iBACR;CACA,MAAM,kBAAkB,qBACtB,iBAAiB,aAAa,qBACxB,iBAAiB,QAAQ,cAAc,yBACvC,iBACR;CAMA,MAAM,YAAY,aAAa,KAAK,qBAAqB,MAAM;CAC/D,gBAAgB;EACd,IAAI,QAAQ,mBAAmB,MAC7B,iBAAiB,KAAK;OAEtB,iBAAiB,QAAQ;CAE7B,GAAG,CAAC,QAAQ,gBAAgB,eAAe,CAAC;CAE5C,gBAAgB;EACd,IAAI,CAAC,iBAAiB;EACtB,IACE,iBAAiB,SAAS,eAAe,mBACzC,iBAAiB,QAAQ,SAAS,WAElC;EACF,iBAAiB,UAAU;GAAE,YAAY;GAAiB,MAAM;EAAU;EAC1E,IAAI,WAAW;GACb,aAAa;GACb,gBAAgB,WAAW;EAC7B,OACE,gBAAgB,WAAW;CAE/B,GAAG,CAAC,WAAW,eAAe,CAAC;CAE/B,MAAM,aAAa,cAAc;EAC/B,IAAI,CAAC,mBAAmB,OAAO,KAAA;EAE/B,MAAM,EAAE,qBAAqB,kBAAkB,GAAG,SAChD;EAEF,MAAM,8BAA8B;GAClC,SAAS,SAAS,gBAAgB;GAClC,IAAI,KAAK,UAAU,GACjB,KAAU,OAAO,CAAC,CAAC,OAAO,UAAmB;IAC3C,OAAO,QACL,qDACA,KACF;GACF,CAAC;GAEH,SAAS,SAAS,MAAM;EAC1B;EAEA,MAAM,iBAAiB,eAAuB;GAC5C,IAAI,uBAAuB,YAAY,YACrC,uBAAuB,UAAU;EAErC;EAEA,OAAO;GACL,GAAG;GACH,qBAAqB,sBACjB,YAAY;IACV,MAAM,aAAa,EAAE,0BAA0B;IAC/C,uBAAuB,UAAU;IACjC,IAAI;KACF,sBAAsB;KACtB,IAAI,eAAe,0BAA0B,SAAS;KAGtD,KAAK,sBAAsB,CAAC,CAAC;KAC7B,KAAK,iBAAiB;KACtB,MAAM,oBAAoB;KAC1B,IAAI,eAAe,0BAA0B,SAAS;KACtD,sBAAsB;KACtB,IAAI,eAAe,0BAA0B,SAAS;KACtD,KAAK,sBAAsB,CAAC,CAAC;KAC7B,KAAK,iBAAiB;IACxB,UAAU;KACR,cAAc,UAAU;IAC1B;GACF,IACA,KAAA;GACJ,kBAAkB,mBACd,OAAO,aAAqB;IAC1B,MAAM,aAAa,EAAE,0BAA0B;IAC/C,uBAAuB,UAAU;IACjC,IAAI;KACF,sBAAsB;KACtB,IAAI,eAAe,0BAA0B,SAAS;KAGtD,KAAK,sBAAsB,CAAC,CAAC;KAC7B,KAAK,iBAAiB;KACtB,MAAM,SAAS,MAAM,iBAAiB,QAAQ;KAC9C,IAAI,eAAe,0BAA0B,SAAS;KACtD,sBAAsB;KACtB,IAAI,eAAe,0BAA0B,SAAS;KACtD,KAAK,sBAAsB,CAAC,CAAC;KAC7B,KAAK,iBAAiB;KACtB,KAAK,sBAAsB,OAAO,QAAQ;KAC1C,IAAI,OAAO,UAAU,KAAA,GACnB,KAAK,kBAAkB,OAAO,KAAK;KAErC,IAAI,eAAe,0BAA0B,SAAS;KACtD,cAAc,UAAU;KACxB,IAAI,OAAO,iBACT,KAAU,kBAAkB,OAAO,QAAQ;IAE/C,UAAU;KACR,cAAc,UAAU;IAC1B;GACF,IACA,KAAA;EACN;CACF,GAAG;EAAC;EAAmB;EAAM;CAAM,CAAC;CAEpC,MAAM,WAAW,QAAQ;CACzB,MAAM,kBAAkB,eACf;EACL,aAAa,UAAU,eAAe,iBAAiB;EACvD,QAAQ,UAAU;EAClB,WAAW,UAAU;EACrB,OAAO,UAAU;EACjB,UAAU,UAAU;EACpB;CACF,IACA;EAAC;EAAU;EAAiB;CAAU,CACxC;CAEA,MAAM,SAAS,8BAA8B,OAAO;CACpD,MAAM,QAAQ,cACN;EACJ,OAAO;GACL,GAAG;GACH,WAAW,SAAS;GACpB,mBAAmB,SAAS;GAC5B,OAAO,SAAS;GAChB,WAAW,SAAS;GACpB,QAAQ,WAAW,QAAQ;IACzB,YACE,KAAK,qBAAqB,CAAC,EAAE,cAAc;IAC7C,iBAAiB,WAAW,KAAK,eAAe,MAAM;IACtD,2BAA2B,cACzB,KAAK,yBAAyB,SAAS;IACzC,YAAY,SAAS,cAAc,KAAK,UAAU,SAAS,SAAS;IACpE,OAAO,SAAS;IAChB,WAAW,SAAS,KAAK,SAAS,IAAI;GACxC,CAAC;GACD,gCAAgC;GAChC,0BAA0B;GAC1B;GACA,QAAQ,YACN,uBAAuB,YAAY,OAC/B,QAAQ,QAAQ,IAChB,KAAK,OAAO,OAAO;GACzB,oBAAoB,YAAY,KAAK,sBAAsB,OAAO;GAClE,SAAS,YAA2B;IAClC,iBAAiB,MAAM;IACvB,OAAO,KAAK,KAAK,OAAO;GAC1B;GACA,WAAW,UAAyB,WAAgC;IAClE,iBAAiB,MAAM;IACvB,OAAO,KAAK,OAAO,UAAU,MAAM;GACrC;GACA,UAAU,YAAY;IACpB,iBAAiB,gBAAgB;IACjC,KAAK,OAAO;GACd;GACA,kBAAkB,YAAY,KAAK,cAAc,OAAO;GACxD,mCAAmC,YACjC,KAAK,sBAAsB,OAAO;GACpC,0BAA0B,YACxB,KAAK,sBAAsB,OAAO,CAAC,CAAC,OAAO,UAAmB;IAC5D,KAAK,YAAY,KAAK;IACtB,MAAM;GACR,CAAC;GACH,WAAW,WAAW,KAAK,OAAO,MAAM;GACxC,cAAc,aACZ,KAAK,sBAAsB,QAAQ;GACrC,WAAW,aACT,KAAK,sBAAsB,QAAQ;GACrC,sBAAsB,UACpB,KAAK,kBAAkB,KAAK;GAC9B,UAAU;GACV,GAAI,mBAAmB,EAAE,OAAO,gBAAgB,QAAQ;EAC1D;CACF,GAEA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF,CACF;CAEA,MAAM,cAAc,wBAAwB,KAAK;CAEjD,MAAM,sBAA4C;EAChD,OAAO;GACL,SAAS,YAAY;GACrB,IAAI,SAAS;IACX,OAAO,YAAY;GACrB;GACA,+BAA+B,aAC7B,YAAY,6BAA6B,QAAQ;GACnD,qCACE,KAAK,qBAAqB,CAAC,EAAE,cAAc,CAAC;GAC9C,oCAAoC,cAClC,KAAK,yBAAyB,SAAS;EAC3C;CACF;CACA,MAAM,CAAC,eAAe,oBAAoB,gBAAgB;EACxD;EACA,SAAS,cAAc;CACzB,EAAE;CACF,IAAI,iBAAiB;CACrB,IAAI,cAAc,gBAAgB,aAAa;EAC7C,iBAAiB;GAAE;GAAa,SAAS,cAAc;EAAE;EACzD,iBAAiB,cAAc;CACjC;CACA,MAAM,UAAU,eAAe;CAE/B,0BAA0B;EACxB,KAAK,cAAc,OAAO;EAC1B,aAAa;GACX,KAAK,cAAc;EACrB;CACF,GAAG,CAAC,MAAM,OAAO,CAAC;CAElB,gBAAgB;EACd,KAAK,gBAAgB;CACvB,GAAG,CAAC,IAAI,CAAC;CAET,OAAO;AACT"}
|
|
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 { 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 coreRef = useRef<AgUiThreadRuntimeCore | 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 AgUiThreadRuntimeCore({\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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"}
|
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.71",
|
|
4
4
|
"description": "AG-UI protocol adapter for assistant-ui",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ag-ui",
|
|
@@ -30,10 +30,10 @@
|
|
|
30
30
|
"sideEffects": false,
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@ag-ui/client": "^0.0.59",
|
|
33
|
-
"@assistant-ui/core": "^0.3.
|
|
34
|
-
"@assistant-ui/generative-ui": "^0.0.
|
|
35
|
-
"@assistant-ui/store": "^0.3.
|
|
36
|
-
"assistant-stream": "^0.3.
|
|
33
|
+
"@assistant-ui/core": "^0.3.30",
|
|
34
|
+
"@assistant-ui/generative-ui": "^0.0.6",
|
|
35
|
+
"@assistant-ui/store": "^0.3.22",
|
|
36
|
+
"assistant-stream": "^0.3.53",
|
|
37
37
|
"fast-json-patch": "^3.1.1"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import type { AbstractAgent } from "@ag-ui/client";
|
|
3
|
+
import type { AppendMessage, ThreadAssistantMessage } from "@assistant-ui/core";
|
|
4
|
+
import { AgUiThreadRuntimeCore } from "./AgUiThreadRuntimeCore";
|
|
5
|
+
import { makeLogger } from "./logger";
|
|
6
|
+
|
|
7
|
+
type EventSubscriber = {
|
|
8
|
+
onTextMessageStartEvent?: (payload: { event: unknown }) => void;
|
|
9
|
+
onTextMessageContentEvent?: (payload: { event: unknown }) => void;
|
|
10
|
+
onToolCallStartEvent?: (payload: { event: unknown }) => void;
|
|
11
|
+
onToolCallEndEvent?: (payload: { event: unknown }) => void;
|
|
12
|
+
onRunFinishedEvent?: (payload: { event: unknown }) => void;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const userMessage = (text: string) =>
|
|
16
|
+
({
|
|
17
|
+
parentId: null,
|
|
18
|
+
role: "user",
|
|
19
|
+
content: [{ type: "text", text }],
|
|
20
|
+
}) as unknown as AppendMessage;
|
|
21
|
+
|
|
22
|
+
function createOpenRun(finishWithInterrupt: boolean) {
|
|
23
|
+
let releaseFirst!: () => void;
|
|
24
|
+
const firstRunClosed = new Promise<void>((resolve) => {
|
|
25
|
+
releaseFirst = resolve;
|
|
26
|
+
});
|
|
27
|
+
const runAgent = vi.fn(
|
|
28
|
+
async (input: { runId: string }, subscriber: EventSubscriber) => {
|
|
29
|
+
if (runAgent.mock.calls.length === 1) {
|
|
30
|
+
if (finishWithInterrupt) {
|
|
31
|
+
subscriber.onToolCallStartEvent?.({
|
|
32
|
+
event: {
|
|
33
|
+
type: "TOOL_CALL_START",
|
|
34
|
+
toolCallId: "call-1",
|
|
35
|
+
toolCallName: "confirm",
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
subscriber.onToolCallEndEvent?.({
|
|
39
|
+
event: { type: "TOOL_CALL_END", toolCallId: "call-1" },
|
|
40
|
+
});
|
|
41
|
+
subscriber.onRunFinishedEvent?.({
|
|
42
|
+
event: {
|
|
43
|
+
type: "RUN_FINISHED",
|
|
44
|
+
runId: input.runId,
|
|
45
|
+
outcome: {
|
|
46
|
+
type: "interrupt",
|
|
47
|
+
interrupts: [
|
|
48
|
+
{
|
|
49
|
+
id: "int-1",
|
|
50
|
+
reason: "tool_call",
|
|
51
|
+
toolCallId: "call-1",
|
|
52
|
+
message: "Continue?",
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
} else {
|
|
59
|
+
subscriber.onTextMessageStartEvent?.({
|
|
60
|
+
event: { type: "TEXT_MESSAGE_START", messageId: "assistant-1" },
|
|
61
|
+
});
|
|
62
|
+
subscriber.onTextMessageContentEvent?.({
|
|
63
|
+
event: {
|
|
64
|
+
type: "TEXT_MESSAGE_CONTENT",
|
|
65
|
+
messageId: "assistant-1",
|
|
66
|
+
delta: "working",
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
await firstRunClosed;
|
|
71
|
+
} else {
|
|
72
|
+
subscriber.onRunFinishedEvent?.({
|
|
73
|
+
event: { type: "RUN_FINISHED", runId: input.runId },
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
);
|
|
78
|
+
const abortRun = vi.fn(() => releaseFirst());
|
|
79
|
+
const core = new AgUiThreadRuntimeCore({
|
|
80
|
+
agent: { runAgent, abortRun } as unknown as AbstractAgent,
|
|
81
|
+
logger: makeLogger(),
|
|
82
|
+
showThinking: false,
|
|
83
|
+
});
|
|
84
|
+
return { core, runAgent, abortRun };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
describe("AgUiThreadRuntimeCore Stop after an interrupt", () => {
|
|
88
|
+
it("cancels a finished tool call when stopped before the stream closes", async () => {
|
|
89
|
+
let releaseFirst!: () => void;
|
|
90
|
+
const firstRunClosed = new Promise<void>((resolve) => {
|
|
91
|
+
releaseFirst = resolve;
|
|
92
|
+
});
|
|
93
|
+
const runAgent = vi.fn(
|
|
94
|
+
async (input: { runId: string }, subscriber: EventSubscriber) => {
|
|
95
|
+
if (runAgent.mock.calls.length !== 1) return;
|
|
96
|
+
subscriber.onToolCallStartEvent?.({
|
|
97
|
+
event: {
|
|
98
|
+
type: "TOOL_CALL_START",
|
|
99
|
+
toolCallId: "call-1",
|
|
100
|
+
toolCallName: "lookup",
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
subscriber.onToolCallEndEvent?.({
|
|
104
|
+
event: { type: "TOOL_CALL_END", toolCallId: "call-1" },
|
|
105
|
+
});
|
|
106
|
+
subscriber.onRunFinishedEvent?.({
|
|
107
|
+
event: { type: "RUN_FINISHED", runId: input.runId },
|
|
108
|
+
});
|
|
109
|
+
await firstRunClosed;
|
|
110
|
+
},
|
|
111
|
+
);
|
|
112
|
+
const abortRun = vi.fn(() => releaseFirst());
|
|
113
|
+
const core = new AgUiThreadRuntimeCore({
|
|
114
|
+
agent: { runAgent, abortRun } as unknown as AbstractAgent,
|
|
115
|
+
logger: makeLogger(),
|
|
116
|
+
showThinking: false,
|
|
117
|
+
});
|
|
118
|
+
const firstRun = core.append(userMessage("start"));
|
|
119
|
+
|
|
120
|
+
expect(core.getMessages().at(-1)?.status).toEqual({
|
|
121
|
+
type: "requires-action",
|
|
122
|
+
reason: "tool-calls",
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
await core.cancel();
|
|
126
|
+
await firstRun;
|
|
127
|
+
|
|
128
|
+
const assistant = core.getMessages().at(-1) as ThreadAssistantMessage;
|
|
129
|
+
expect(assistant.status).toEqual({
|
|
130
|
+
type: "incomplete",
|
|
131
|
+
reason: "cancelled",
|
|
132
|
+
});
|
|
133
|
+
expect(core.isRunning()).toBe(false);
|
|
134
|
+
expect(abortRun).toHaveBeenCalledOnce();
|
|
135
|
+
|
|
136
|
+
core.addToolResult({
|
|
137
|
+
messageId: assistant.id,
|
|
138
|
+
toolCallId: "call-1",
|
|
139
|
+
toolName: "lookup",
|
|
140
|
+
result: "found",
|
|
141
|
+
isError: false,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
expect(runAgent).toHaveBeenCalledOnce();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("keeps the gate answerable and accepts a steer-away run", async () => {
|
|
148
|
+
const { core, runAgent, abortRun } = createOpenRun(true);
|
|
149
|
+
const firstRun = core.append(userMessage("start"));
|
|
150
|
+
|
|
151
|
+
expect(core.isRunning()).toBe(true);
|
|
152
|
+
expect(core.getMessages().at(-1)?.status).toEqual({
|
|
153
|
+
type: "requires-action",
|
|
154
|
+
reason: "interrupt",
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
await core.cancel();
|
|
158
|
+
await firstRun;
|
|
159
|
+
|
|
160
|
+
const assistant = core.getMessages().at(-1) as ThreadAssistantMessage;
|
|
161
|
+
expect(assistant.status).toEqual({
|
|
162
|
+
type: "requires-action",
|
|
163
|
+
reason: "interrupt",
|
|
164
|
+
});
|
|
165
|
+
expect(
|
|
166
|
+
assistant.content.find((part) => part.type === "tool-call"),
|
|
167
|
+
).toMatchObject({
|
|
168
|
+
approval: { id: "int-1", prompt: "Continue?" },
|
|
169
|
+
});
|
|
170
|
+
expect(core.getPendingInterrupts()?.interrupts).toMatchObject([
|
|
171
|
+
{ id: "int-1" },
|
|
172
|
+
]);
|
|
173
|
+
expect(core.isRunning()).toBe(false);
|
|
174
|
+
expect(abortRun).toHaveBeenCalledOnce();
|
|
175
|
+
|
|
176
|
+
await core.steerAway("do something else");
|
|
177
|
+
|
|
178
|
+
expect(runAgent).toHaveBeenCalledTimes(2);
|
|
179
|
+
expect(runAgent.mock.calls[1]?.[0]).toMatchObject({
|
|
180
|
+
resume: [{ interruptId: "int-1", status: "cancelled" }],
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("still cancels a run that is streaming", async () => {
|
|
185
|
+
const { core } = createOpenRun(false);
|
|
186
|
+
const firstRun = core.append(userMessage("start"));
|
|
187
|
+
|
|
188
|
+
expect(core.getMessages().at(-1)?.status).toEqual({ type: "running" });
|
|
189
|
+
expect(core.getMessages().at(-1)?.content).toMatchObject([
|
|
190
|
+
{ type: "text", text: "working" },
|
|
191
|
+
]);
|
|
192
|
+
|
|
193
|
+
await core.cancel();
|
|
194
|
+
await firstRun;
|
|
195
|
+
|
|
196
|
+
expect(core.getMessages().at(-1)?.status).toEqual({
|
|
197
|
+
type: "incomplete",
|
|
198
|
+
reason: "cancelled",
|
|
199
|
+
});
|
|
200
|
+
expect(core.isRunning()).toBe(false);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
@@ -15,6 +15,7 @@ import type {
|
|
|
15
15
|
ExportedMessageRepository,
|
|
16
16
|
MessageStatus,
|
|
17
17
|
RespondToToolApprovalOptions,
|
|
18
|
+
RunConfig,
|
|
18
19
|
ThreadAssistantMessage,
|
|
19
20
|
ThreadHistoryAdapter,
|
|
20
21
|
ThreadMessage,
|
|
@@ -31,6 +32,7 @@ import {
|
|
|
31
32
|
import type {
|
|
32
33
|
AbstractAgent,
|
|
33
34
|
AgentSubscriber,
|
|
35
|
+
Context,
|
|
34
36
|
RunAgentParameters,
|
|
35
37
|
RunAgentResult,
|
|
36
38
|
} from "@ag-ui/client";
|
|
@@ -43,6 +45,7 @@ import type {
|
|
|
43
45
|
AgUiInterrupt,
|
|
44
46
|
AgUiResumeEntry,
|
|
45
47
|
AgUiResumeTranscript,
|
|
48
|
+
UseAgUiRuntimeOptions,
|
|
46
49
|
} from "./types";
|
|
47
50
|
import { AG_UI_METADATA_NAMESPACE, MCP_APPS_ACTIVITY_TYPE } from "./types";
|
|
48
51
|
import type { ReadonlyJSONValue } from "assistant-stream/utils";
|
|
@@ -85,7 +88,6 @@ const isResolvedToolCall = (
|
|
|
85
88
|
const isActivityPart = (part: ThreadAssistantMessage["content"][number]) =>
|
|
86
89
|
part.type === "data" && part.name.startsWith("agui-activity/");
|
|
87
90
|
|
|
88
|
-
type RunConfig = NonNullable<AppendMessage["runConfig"]>;
|
|
89
91
|
type ResumeStream = (
|
|
90
92
|
options: ChatModelRunOptions,
|
|
91
93
|
) => AsyncGenerator<ChatModelRunResult, void, unknown>;
|
|
@@ -110,6 +112,8 @@ type CoreOptions = {
|
|
|
110
112
|
agent: AbstractAgent;
|
|
111
113
|
logger: Logger;
|
|
112
114
|
showThinking: boolean;
|
|
115
|
+
joinStrategy?: UseAgUiRuntimeOptions["joinStrategy"];
|
|
116
|
+
getRunContext?: UseAgUiRuntimeOptions["getRunContext"];
|
|
113
117
|
resumeTranscript?: AgUiResumeTranscript | undefined;
|
|
114
118
|
autoCancelPendingToolCalls?: boolean | undefined;
|
|
115
119
|
onError?: (error: Error) => void;
|
|
@@ -155,6 +159,8 @@ export class AgUiThreadRuntimeCore {
|
|
|
155
159
|
private agent: AbstractAgent;
|
|
156
160
|
private logger: Logger;
|
|
157
161
|
private showThinking: boolean;
|
|
162
|
+
private joinStrategy: UseAgUiRuntimeOptions["joinStrategy"];
|
|
163
|
+
private getRunContext: UseAgUiRuntimeOptions["getRunContext"];
|
|
158
164
|
private resumeTranscript: AgUiResumeTranscript;
|
|
159
165
|
private autoCancelPendingToolCalls: boolean | undefined;
|
|
160
166
|
private onError: ((error: Error) => void) | undefined;
|
|
@@ -214,6 +220,8 @@ export class AgUiThreadRuntimeCore {
|
|
|
214
220
|
this.agent = options.agent;
|
|
215
221
|
this.logger = options.logger;
|
|
216
222
|
this.showThinking = options.showThinking;
|
|
223
|
+
this.joinStrategy = options.joinStrategy;
|
|
224
|
+
this.getRunContext = options.getRunContext;
|
|
217
225
|
this.resumeTranscript = options.resumeTranscript ?? "full";
|
|
218
226
|
this.autoCancelPendingToolCalls = options.autoCancelPendingToolCalls;
|
|
219
227
|
this.onError = options.onError;
|
|
@@ -244,6 +252,8 @@ export class AgUiThreadRuntimeCore {
|
|
|
244
252
|
this.agent = options.agent;
|
|
245
253
|
this.logger = options.logger;
|
|
246
254
|
this.showThinking = options.showThinking;
|
|
255
|
+
this.joinStrategy = options.joinStrategy;
|
|
256
|
+
this.getRunContext = options.getRunContext;
|
|
247
257
|
this.resumeTranscript = options.resumeTranscript ?? "full";
|
|
248
258
|
this.autoCancelPendingToolCalls = options.autoCancelPendingToolCalls;
|
|
249
259
|
this.onError = options.onError;
|
|
@@ -1291,6 +1301,8 @@ export class AgUiThreadRuntimeCore {
|
|
|
1291
1301
|
const runStartMessageIds = new Set(
|
|
1292
1302
|
this.getMessageRepository().messages.map(({ message }) => message.id),
|
|
1293
1303
|
);
|
|
1304
|
+
const joinStrategy = this.joinStrategy;
|
|
1305
|
+
const hasRunUserBoundary = parent?.message.role === "user" && !resume;
|
|
1294
1306
|
|
|
1295
1307
|
let pendingError: Error | null = null;
|
|
1296
1308
|
const assistantParentId = parent ? parentId : this.session.headId;
|
|
@@ -1386,6 +1398,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
1386
1398
|
const abortSignal = abortController.signal;
|
|
1387
1399
|
const aggregator = new RunAggregator({
|
|
1388
1400
|
showThinking: this.showThinking,
|
|
1401
|
+
joinStrategy,
|
|
1389
1402
|
logger: this.logger,
|
|
1390
1403
|
emit: applyUpdate,
|
|
1391
1404
|
onServerMessageId: (serverId) => {
|
|
@@ -1401,6 +1414,9 @@ export class AgUiThreadRuntimeCore {
|
|
|
1401
1414
|
aggregator,
|
|
1402
1415
|
event,
|
|
1403
1416
|
assistantMessageId,
|
|
1417
|
+
joinStrategy,
|
|
1418
|
+
runStartMessageIds,
|
|
1419
|
+
hasRunUserBoundary,
|
|
1404
1420
|
);
|
|
1405
1421
|
if (event.type === "MESSAGES_SNAPSHOT") {
|
|
1406
1422
|
assistantMessageId = nextAssistantMessageId;
|
|
@@ -1598,6 +1614,8 @@ export class AgUiThreadRuntimeCore {
|
|
|
1598
1614
|
historyMessages: readonly ThreadMessage[] | undefined,
|
|
1599
1615
|
resume?: ResumeDispatch,
|
|
1600
1616
|
) {
|
|
1617
|
+
const pendingA2uiAction = this.pendingA2uiAction;
|
|
1618
|
+
this.pendingA2uiAction = undefined;
|
|
1601
1619
|
const threadId = this.agent.threadId || "main";
|
|
1602
1620
|
const messages = toAgUiMessages(
|
|
1603
1621
|
this.selectRunMessages(
|
|
@@ -1606,26 +1624,36 @@ export class AgUiThreadRuntimeCore {
|
|
|
1606
1624
|
),
|
|
1607
1625
|
);
|
|
1608
1626
|
const context = this.runtime?.thread.getModelContext();
|
|
1627
|
+
let hostContext: readonly Context[] = [];
|
|
1628
|
+
try {
|
|
1629
|
+
hostContext = this.getRunContext?.({ messages, runConfig }) ?? [];
|
|
1630
|
+
} catch (error) {
|
|
1631
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
1632
|
+
this.logger.error("[agui] getRunContext failed", err);
|
|
1633
|
+
invokeRuntimeCallback("onError", this.onError, err);
|
|
1634
|
+
}
|
|
1609
1635
|
const input = {
|
|
1610
1636
|
threadId,
|
|
1611
1637
|
runId,
|
|
1612
1638
|
state: this.stateSnapshot ?? null,
|
|
1613
1639
|
messages,
|
|
1614
1640
|
tools: toAgUiTools(context?.tools),
|
|
1615
|
-
context:
|
|
1616
|
-
|
|
1617
|
-
|
|
1641
|
+
context: [
|
|
1642
|
+
...(context?.system
|
|
1643
|
+
? [{ description: "system", value: context.system }]
|
|
1644
|
+
: []),
|
|
1645
|
+
...hostContext,
|
|
1646
|
+
],
|
|
1618
1647
|
forwardedProps: {
|
|
1619
1648
|
...(context?.callSettings ?? {}),
|
|
1620
1649
|
...(context?.config ?? {}),
|
|
1621
1650
|
...(runConfig?.custom ? { runConfig: runConfig.custom } : {}),
|
|
1622
|
-
...(
|
|
1623
|
-
? { a2uiAction: { userAction:
|
|
1651
|
+
...(pendingA2uiAction
|
|
1652
|
+
? { a2uiAction: { userAction: pendingA2uiAction } }
|
|
1624
1653
|
: {}),
|
|
1625
1654
|
},
|
|
1626
1655
|
...(resume !== undefined ? { resume: resume.entries } : {}),
|
|
1627
1656
|
};
|
|
1628
|
-
this.pendingA2uiAction = undefined;
|
|
1629
1657
|
return input;
|
|
1630
1658
|
}
|
|
1631
1659
|
|
|
@@ -2023,6 +2051,9 @@ export class AgUiThreadRuntimeCore {
|
|
|
2023
2051
|
aggregator: RunAggregator,
|
|
2024
2052
|
event: AgUiEvent,
|
|
2025
2053
|
activeAssistantId: string | undefined,
|
|
2054
|
+
joinStrategy: UseAgUiRuntimeOptions["joinStrategy"],
|
|
2055
|
+
runStartMessageIds: ReadonlySet<string>,
|
|
2056
|
+
hasRunUserBoundary: boolean,
|
|
2026
2057
|
): string | undefined {
|
|
2027
2058
|
switch (event.type) {
|
|
2028
2059
|
case "STATE_SNAPSHOT": {
|
|
@@ -2056,7 +2087,13 @@ export class AgUiThreadRuntimeCore {
|
|
|
2056
2087
|
const hasActiveText =
|
|
2057
2088
|
activeAssistant?.role === "assistant" &&
|
|
2058
2089
|
activeAssistant.content.some((part) => part.type === "text");
|
|
2059
|
-
this.importMessagesSnapshot(
|
|
2090
|
+
this.importMessagesSnapshot(
|
|
2091
|
+
event.messages,
|
|
2092
|
+
activeAssistantId,
|
|
2093
|
+
joinStrategy,
|
|
2094
|
+
runStartMessageIds,
|
|
2095
|
+
hasRunUserBoundary,
|
|
2096
|
+
);
|
|
2060
2097
|
const headId = this.session.headId;
|
|
2061
2098
|
if (hasActiveText && headId !== previousHeadId) {
|
|
2062
2099
|
const head =
|
|
@@ -2265,6 +2302,9 @@ export class AgUiThreadRuntimeCore {
|
|
|
2265
2302
|
private importMessagesSnapshot(
|
|
2266
2303
|
rawMessages: readonly unknown[],
|
|
2267
2304
|
activeAssistantId: string | undefined,
|
|
2305
|
+
joinStrategy: UseAgUiRuntimeOptions["joinStrategy"],
|
|
2306
|
+
runStartMessageIds: ReadonlySet<string>,
|
|
2307
|
+
hasRunUserBoundary: boolean,
|
|
2268
2308
|
) {
|
|
2269
2309
|
try {
|
|
2270
2310
|
const activeAssistantItem = activeAssistantId
|
|
@@ -2319,6 +2359,101 @@ export class AgUiThreadRuntimeCore {
|
|
|
2319
2359
|
};
|
|
2320
2360
|
}
|
|
2321
2361
|
}
|
|
2362
|
+
if (joinStrategy === "concat-content") {
|
|
2363
|
+
const activeIndex = converted.findIndex(
|
|
2364
|
+
(message) => message.id === activeAssistantId,
|
|
2365
|
+
);
|
|
2366
|
+
const lastKnownIndex = converted.findLastIndex((message) =>
|
|
2367
|
+
runStartMessageIds.has(message.id),
|
|
2368
|
+
);
|
|
2369
|
+
const lastUserIndex = hasRunUserBoundary
|
|
2370
|
+
? converted.findLastIndex((message) => message.role === "user")
|
|
2371
|
+
: -1;
|
|
2372
|
+
const startIndex =
|
|
2373
|
+
activeIndex >= 0
|
|
2374
|
+
? activeIndex
|
|
2375
|
+
: lastKnownIndex >= 0 || lastUserIndex >= 0
|
|
2376
|
+
? Math.max(lastKnownIndex, lastUserIndex) + 1
|
|
2377
|
+
: -1;
|
|
2378
|
+
if (startIndex >= 0 && converted[startIndex]?.role === "assistant") {
|
|
2379
|
+
let endIndex = startIndex + 1;
|
|
2380
|
+
while (converted[endIndex]?.role === "assistant") endIndex++;
|
|
2381
|
+
const runMessages = converted.slice(
|
|
2382
|
+
startIndex,
|
|
2383
|
+
endIndex,
|
|
2384
|
+
) as ThreadAssistantMessage[];
|
|
2385
|
+
const includesStreamedPrefix =
|
|
2386
|
+
activeIndex < 0 &&
|
|
2387
|
+
activeAssistant?.content.some((part) => part.type !== "data");
|
|
2388
|
+
if (includesStreamedPrefix && activeAssistant) {
|
|
2389
|
+
runMessages.unshift(activeAssistant);
|
|
2390
|
+
}
|
|
2391
|
+
if (runMessages.length > 1) {
|
|
2392
|
+
const [first, ...rest] = runMessages;
|
|
2393
|
+
let content = runMessages.flatMap((message) => message.content);
|
|
2394
|
+
if (includesStreamedPrefix) {
|
|
2395
|
+
content = [...first!.content];
|
|
2396
|
+
let cursor = 0;
|
|
2397
|
+
const keyOf = (part: (typeof content)[number]) =>
|
|
2398
|
+
part.type === "tool-call"
|
|
2399
|
+
? `tool:${part.toolCallId}`
|
|
2400
|
+
: part.type === "text" || part.type === "reasoning"
|
|
2401
|
+
? `${part.type}:${part.text}`
|
|
2402
|
+
: undefined;
|
|
2403
|
+
for (const message of rest) {
|
|
2404
|
+
for (const part of message.content) {
|
|
2405
|
+
const key = keyOf(part);
|
|
2406
|
+
const match =
|
|
2407
|
+
key === undefined
|
|
2408
|
+
? -1
|
|
2409
|
+
: content.findIndex(
|
|
2410
|
+
(previous, index) =>
|
|
2411
|
+
index >= cursor && keyOf(previous) === key,
|
|
2412
|
+
);
|
|
2413
|
+
if (match < 0) {
|
|
2414
|
+
content.push(part);
|
|
2415
|
+
cursor = content.length;
|
|
2416
|
+
} else {
|
|
2417
|
+
if (part.type === "tool-call") {
|
|
2418
|
+
content[match] = this.preserveToolResults(
|
|
2419
|
+
[content[match]!],
|
|
2420
|
+
[part],
|
|
2421
|
+
)[0]!;
|
|
2422
|
+
}
|
|
2423
|
+
cursor = match + 1;
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
converted.splice(startIndex, endIndex - startIndex, {
|
|
2429
|
+
...first!,
|
|
2430
|
+
content,
|
|
2431
|
+
status: rest.at(-1)!.status,
|
|
2432
|
+
metadata: rest.reduce(
|
|
2433
|
+
(metadata, message) => ({
|
|
2434
|
+
...metadata,
|
|
2435
|
+
...message.metadata,
|
|
2436
|
+
unstable_state: message.metadata.unstable_state,
|
|
2437
|
+
unstable_annotations: [
|
|
2438
|
+
...metadata.unstable_annotations,
|
|
2439
|
+
...message.metadata.unstable_annotations,
|
|
2440
|
+
],
|
|
2441
|
+
unstable_data: [
|
|
2442
|
+
...metadata.unstable_data,
|
|
2443
|
+
...message.metadata.unstable_data,
|
|
2444
|
+
],
|
|
2445
|
+
steps: [...metadata.steps, ...message.metadata.steps],
|
|
2446
|
+
custom: mergeAgUiNamespace(
|
|
2447
|
+
metadata.custom,
|
|
2448
|
+
message.metadata.custom,
|
|
2449
|
+
),
|
|
2450
|
+
}),
|
|
2451
|
+
first!.metadata,
|
|
2452
|
+
),
|
|
2453
|
+
});
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2322
2457
|
const snapshotContainsActiveAssistant = converted.some(
|
|
2323
2458
|
(message) => message.id === activeAssistant?.id,
|
|
2324
2459
|
);
|