@assistant-ui/react-ag-ui 0.0.48 → 0.0.49

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.
Files changed (41) hide show
  1. package/dist/agUiExtras.d.ts +1 -1
  2. package/dist/agUiExtras.d.ts.map +1 -1
  3. package/dist/agUiExtras.js +1 -1
  4. package/dist/agUiExtras.js.map +1 -1
  5. package/dist/hooks.d.ts +6 -1
  6. package/dist/hooks.d.ts.map +1 -1
  7. package/dist/hooks.js +10 -1
  8. package/dist/hooks.js.map +1 -1
  9. package/dist/index.d.ts +2 -2
  10. package/dist/index.js +2 -2
  11. package/dist/runtime/AgUiThreadRuntimeCore.d.ts +3 -0
  12. package/dist/runtime/AgUiThreadRuntimeCore.d.ts.map +1 -1
  13. package/dist/runtime/AgUiThreadRuntimeCore.js +78 -16
  14. package/dist/runtime/AgUiThreadRuntimeCore.js.map +1 -1
  15. package/dist/runtime/adapter/conversions.d.ts.map +1 -1
  16. package/dist/runtime/adapter/conversions.js +63 -1
  17. package/dist/runtime/adapter/conversions.js.map +1 -1
  18. package/dist/runtime/adapter/run-aggregator.d.ts +6 -1
  19. package/dist/runtime/adapter/run-aggregator.d.ts.map +1 -1
  20. package/dist/runtime/adapter/run-aggregator.js +57 -1
  21. package/dist/runtime/adapter/run-aggregator.js.map +1 -1
  22. package/dist/runtime/event-parser.js +5 -2
  23. package/dist/runtime/event-parser.js.map +1 -1
  24. package/dist/runtime/mcp-tool-result.d.ts.map +1 -1
  25. package/dist/runtime/mcp-tool-result.js +6 -3
  26. package/dist/runtime/mcp-tool-result.js.map +1 -1
  27. package/dist/runtime/types.d.ts +3 -0
  28. package/dist/runtime/types.d.ts.map +1 -1
  29. package/dist/useAgUiRuntime.js +2 -1
  30. package/dist/useAgUiRuntime.js.map +1 -1
  31. package/package.json +5 -4
  32. package/src/agUiExtras.ts +1 -1
  33. package/src/hooks.ts +14 -0
  34. package/src/index.ts +1 -0
  35. package/src/runtime/AgUiThreadRuntimeCore.ts +132 -16
  36. package/src/runtime/adapter/conversions.ts +107 -1
  37. package/src/runtime/adapter/run-aggregator.ts +82 -0
  38. package/src/runtime/event-parser.ts +12 -5
  39. package/src/runtime/mcp-tool-result.ts +8 -6
  40. package/src/runtime/types.ts +3 -0
  41. package/src/useAgUiRuntime.ts +1 -0
@@ -1 +1 @@
1
- {"version":3,"file":"mcp-tool-result.js","names":[],"sources":["../../src/runtime/mcp-tool-result.ts"],"sourcesContent":["import { isRecord } from \"@assistant-ui/core/internal\";\nimport { isMcpAppUri } from \"@assistant-ui/core\";\n\nexport type McpToolCallResult = Record<string, unknown> & {\n content: unknown[];\n structuredContent?: Record<string, unknown>;\n _meta?: Record<string, unknown>;\n isError?: boolean;\n};\n\nexport const parseMcpToolCallResult = (\n source: Record<string, unknown>,\n modelContent: string,\n): McpToolCallResult | undefined => {\n const structuredContent = isRecord(source.structuredContent)\n ? source.structuredContent\n : undefined;\n const meta = isRecord(source._meta) ? source._meta : undefined;\n if (!structuredContent && !meta) return undefined;\n\n return {\n content: modelContent ? [{ type: \"text\", text: modelContent }] : [],\n ...(structuredContent ? { structuredContent } : {}),\n ...(meta ? { _meta: meta } : {}),\n ...(typeof source.isError === \"boolean\" ? { isError: source.isError } : {}),\n };\n};\n\n// _meta[\"ui/resourceUri\"] is the MCP-UI pointer react-ai-sdk reads off\n// CallToolResult._meta; only ui:// URIs identify an MCP Apps widget. A\n// structured carrier is deliberately not read until upstream AG-UI settles\n// one (agno-agi/agno#9087).\nexport const readMcpAppResourceUri = (meta: unknown): string | undefined => {\n if (!isRecord(meta)) return undefined;\n const uri = meta[\"ui/resourceUri\"];\n return typeof uri === \"string\" && isMcpAppUri(uri) ? uri : undefined;\n};\n"],"mappings":";;;AAUA,MAAa,0BACX,QACA,iBACkC;CAClC,MAAM,oBAAoB,SAAS,OAAO,iBAAiB,IACvD,OAAO,oBACP,KAAA;CACJ,MAAM,OAAO,SAAS,OAAO,KAAK,IAAI,OAAO,QAAQ,KAAA;CACrD,IAAI,CAAC,qBAAqB,CAAC,MAAM,OAAO,KAAA;CAExC,OAAO;EACL,SAAS,eAAe,CAAC;GAAE,MAAM;GAAQ,MAAM;EAAa,CAAC,IAAI,CAAC;EAClE,GAAI,oBAAoB,EAAE,kBAAkB,IAAI,CAAC;EACjD,GAAI,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;EAC9B,GAAI,OAAO,OAAO,YAAY,YAAY,EAAE,SAAS,OAAO,QAAQ,IAAI,CAAC;CAC3E;AACF;AAMA,MAAa,yBAAyB,SAAsC;CAC1E,IAAI,CAAC,SAAS,IAAI,GAAG,OAAO,KAAA;CAC5B,MAAM,MAAM,KAAK;CACjB,OAAO,OAAO,QAAQ,YAAY,YAAY,GAAG,IAAI,MAAM,KAAA;AAC7D"}
1
+ {"version":3,"file":"mcp-tool-result.js","names":[],"sources":["../../src/runtime/mcp-tool-result.ts"],"sourcesContent":["import { isRecord } from \"@assistant-ui/core/internal\";\nimport { isMcpAppUri } from \"@assistant-ui/core\";\n\nexport type McpToolCallResult = Record<string, unknown> & {\n content: unknown[];\n structuredContent?: Record<string, unknown>;\n _meta?: Record<string, unknown>;\n isError?: boolean;\n};\n\nexport const parseMcpToolCallResult = (\n source: Record<string, unknown>,\n modelContent: string,\n): McpToolCallResult | undefined => {\n const structuredContent = isRecord(source.structuredContent)\n ? source.structuredContent\n : undefined;\n const meta = isRecord(source._meta) ? source._meta : undefined;\n if (!structuredContent && !meta) return undefined;\n\n return {\n content: modelContent ? [{ type: \"text\", text: modelContent }] : [],\n ...(structuredContent ? { structuredContent } : {}),\n ...(meta ? { _meta: meta } : {}),\n ...(typeof source.isError === \"boolean\" ? { isError: source.isError } : {}),\n };\n};\n\n// The MCP Apps pointer on CallToolResult._meta: canonical nested `ui.resourceUri`\n// per the Apps spec (2026-01-26), with the deprecated flat `\"ui/resourceUri\"` kept\n// for legacy producers. Only ui:// URIs identify a widget.\nexport const readMcpAppResourceUri = (meta: unknown): string | undefined => {\n if (!isRecord(meta)) return undefined;\n const ui = meta[\"ui\"];\n const nested = isRecord(ui) ? ui[\"resourceUri\"] : undefined;\n if (typeof nested === \"string\" && isMcpAppUri(nested)) return nested;\n const flat = meta[\"ui/resourceUri\"];\n return typeof flat === \"string\" && isMcpAppUri(flat) ? flat : undefined;\n};\n"],"mappings":";;;AAUA,MAAa,0BACX,QACA,iBACkC;CAClC,MAAM,oBAAoB,SAAS,OAAO,iBAAiB,IACvD,OAAO,oBACP,KAAA;CACJ,MAAM,OAAO,SAAS,OAAO,KAAK,IAAI,OAAO,QAAQ,KAAA;CACrD,IAAI,CAAC,qBAAqB,CAAC,MAAM,OAAO,KAAA;CAExC,OAAO;EACL,SAAS,eAAe,CAAC;GAAE,MAAM;GAAQ,MAAM;EAAa,CAAC,IAAI,CAAC;EAClE,GAAI,oBAAoB,EAAE,kBAAkB,IAAI,CAAC;EACjD,GAAI,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;EAC9B,GAAI,OAAO,OAAO,YAAY,YAAY,EAAE,SAAS,OAAO,QAAQ,IAAI,CAAC;CAC3E;AACF;AAKA,MAAa,yBAAyB,SAAsC;CAC1E,IAAI,CAAC,SAAS,IAAI,GAAG,OAAO,KAAA;CAC5B,MAAM,KAAK,KAAK;CAChB,MAAM,SAAS,SAAS,EAAE,IAAI,GAAG,iBAAiB,KAAA;CAClD,IAAI,OAAO,WAAW,YAAY,YAAY,MAAM,GAAG,OAAO;CAC9D,MAAM,OAAO,KAAK;CAClB,OAAO,OAAO,SAAS,YAAY,YAAY,IAAI,IAAI,OAAO,KAAA;AAChE"}
@@ -71,6 +71,7 @@ type AgUiResumeEntry = {
71
71
  };
72
72
  type AgUiRuntimeExtras = {
73
73
  interrupts: readonly AgUiInterrupt[];
74
+ sendA2uiAction: (action: Record<string, unknown>) => void;
74
75
  submitInterruptResponses: (responses: readonly AgUiResumeEntry[]) => Promise<void>;
75
76
  steerAway: (message: CreateAppendMessage, responses?: readonly AgUiResumeEntry[]) => Promise<void>;
76
77
  state: ReadonlyJSONValue | undefined;
@@ -166,6 +167,8 @@ type AgUiEvent = {
166
167
  type: "ACTIVITY_SNAPSHOT";
167
168
  activityType: string;
168
169
  content: Record<string, unknown>;
170
+ messageId?: string;
171
+ replace?: boolean;
169
172
  } | {
170
173
  type: "RAW";
171
174
  event: any;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","names":[],"sources":["../../src/runtime/types.ts"],"mappings":";;;;;;;;;;;;KAuBK;EACH,mBAAmB;EACnB,QAAQ;;;;;;EAMR;;KAGU,2BAA2B,KACrC;EAGA,qBAEM,qBACG,QAAQ,wBAAwB;;KAI/B;EACV,cAAc;EACd,SAAS;EACT,YAAY;EACZ,QAAQ;EACR,WAAW;EACX,UAAU;;;;EAIV,aAAa;;KAGH,wBAAwB;EAClC,OAAO;EACP,SAAS,QAAQ;EACjB;;;;;;;;;;;EAWA;EACA,WAAW,GAAG;EACd;EACA,WAAW;;KAGD;KAMA;EACV;EACA,QAAQ;EACR;EACA;EACA,iBAAiB;EACjB;EACA,WAAW;;KAGD;EACV;EACA;EACA;;KAGU;EACV,qBAAqB;EACrB,2BACE,oBAAoB,sBACjB;EACL,YACE,SAAS,qBACT,qBAAqB,sBAClB;EACL,OAAO;EACP,WACE,MACI,sBACE,MAAM,kCAAkC;;KAItC;EACN;;EACA;EAAmB,YAAY;;KAEzB;EACN;EAAqB;;EAErB;EACA;EACA,UAAU;;EAEV;EAAuB;;EACvB;EAAmB;EAAkB;;EACrC;EAA4B;;EAC5B;EAA8B;EAAoB;;EAClD;EAA0B;;EAC1B;EAA4B;;EAC5B;EAAwB;;EACxB;;EACA;EAAuC;;EACvC;;EACA;;EACA;EAAyB;;EACzB;EAAiC;;EACjC;EAAmC;EAAoB;;EACvD;EAA+B;;EAC/B;EAAuB;;EAEvB;EACA;EACA;EACA;;EAEA;EAAwB;EAAoB;;EAC5C;EAAuB;;EAEvB;EACA;EACA;EACA;EACA;;EAGA;EACA;EACA;EACA;EACA;EACA,YAAY;;EAGZ;EACA;EACA,SAAS;;EAET;EAAa;EAAY;;EACzB;EAAgB;EAAc;;EAC9B;EAAwB;;EACxB;EAAqB;;EACrB;EAA2B"}
1
+ {"version":3,"file":"types.d.ts","names":[],"sources":["../../src/runtime/types.ts"],"mappings":";;;;;;;;;;;;KAuBK;EACH,mBAAmB;EACnB,QAAQ;;;;;;EAMR;;KAGU,2BAA2B,KACrC;EAGA,qBAEM,qBACG,QAAQ,wBAAwB;;KAI/B;EACV,cAAc;EACd,SAAS;EACT,YAAY;EACZ,QAAQ;EACR,WAAW;EACX,UAAU;;;;EAIV,aAAa;;KAGH,wBAAwB;EAClC,OAAO;EACP,SAAS,QAAQ;EACjB;;;;;;;;;;;EAWA;EACA,WAAW,GAAG;EACd;EACA,WAAW;;KAGD;KAMA;EACV;EACA,QAAQ;EACR;EACA;EACA,iBAAiB;EACjB;EACA,WAAW;;KAGD;EACV;EACA;EACA;;KAGU;EACV,qBAAqB;EACrB,iBAAiB,QAAQ;EACzB,2BACE,oBAAoB,sBACjB;EACL,YACE,SAAS,qBACT,qBAAqB,sBAClB;EACL,OAAO;EACP,WACE,MACI,sBACE,MAAM,kCAAkC;;KAItC;EACN;;EACA;EAAmB,YAAY;;KAEzB;EACN;EAAqB;;EAErB;EACA;EACA,UAAU;;EAEV;EAAuB;;EACvB;EAAmB;EAAkB;;EACrC;EAA4B;;EAC5B;EAA8B;EAAoB;;EAClD;EAA0B;;EAC1B;EAA4B;;EAC5B;EAAwB;;EACxB;;EACA;EAAuC;;EACvC;;EACA;;EACA;EAAyB;;EACzB;EAAiC;;EACjC;EAAmC;EAAoB;;EACvD;EAA+B;;EAC/B;EAAuB;;EAEvB;EACA;EACA;EACA;;EAEA;EAAwB;EAAoB;;EAC5C;EAAuB;;EAEvB;EACA;EACA;EACA;EACA;;EAGA;EACA;EACA;EACA;EACA;EACA,YAAY;;EAGZ;EACA;EACA,SAAS;EACT;EACA;;EAEA;EAAa;EAAY;;EACzB;EAAgB;EAAc;;EAC9B;EAAwB;;EACxB;EAAqB;;EACrB;EAA2B"}
@@ -2,8 +2,8 @@
2
2
  import { agUiExtras } from "./agUiExtras.js";
3
3
  import { makeLogger } from "./runtime/logger.js";
4
4
  import { AgUiThreadRuntimeCore } from "./runtime/AgUiThreadRuntimeCore.js";
5
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
6
5
  import { useExternalStoreRuntime, useExternalStoreSharedOptions, useRuntimeAdapters } from "@assistant-ui/core/react";
6
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
7
7
  //#region src/useAgUiRuntime.ts
8
8
  const EMPTY_INTERRUPTS = [];
9
9
  function useAgUiRuntime(options) {
@@ -79,6 +79,7 @@ function useAgUiRuntime(options) {
79
79
  isRunning: core.isRunning() || hasExecutingTools,
80
80
  extras: agUiExtras.provide({
81
81
  interrupts: core.getPendingInterrupts()?.interrupts ?? EMPTY_INTERRUPTS,
82
+ sendA2uiAction: (action) => core.sendA2uiAction(action),
82
83
  submitInterruptResponses: (responses) => core.submitInterruptResponses(responses),
83
84
  steerAway: (message, responses) => core.steerAway(message, responses),
84
85
  state: core.getState(),
@@ -1 +1 @@
1
- {"version":3,"file":"useAgUiRuntime.js","names":[],"sources":["../src/useAgUiRuntime.ts"],"sourcesContent":["\"use client\";\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport {\n useExternalStoreRuntime,\n useExternalStoreSharedOptions,\n useRuntimeAdapters,\n} from \"@assistant-ui/core/react\";\nimport type { ToolExecutionStatus } 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\";\n\nconst EMPTY_INTERRUPTS: readonly AgUiInterrupt[] = [];\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 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 const [toolStatuses, setToolStatuses] = useState<\n Record<string, ToolExecutionStatus>\n >({});\n\n const hasExecutingTools = Object.values(toolStatuses).some(\n (s) => s?.type === \"executing\",\n );\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() || hasExecutingTools,\n extras: agUiExtras.provide({\n interrupts:\n core.getPendingInterrupts()?.interrupts ?? EMPTY_INTERRUPTS,\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) => core.edit(message),\n onReload: (parentId: string | null, config: { runConfig?: any }) =>\n core.reload(parentId, config),\n onCancel: async () => {\n core.cancel();\n },\n onAddToolResult: (options) => core.addToolResult(options),\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 } 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 [adapterAdapters, core, _version, hasExecutingTools, shared],\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":";;;;;;;AAyBA,MAAM,mBAA6C,CAAC;AAkBpD,SAAgB,eACd,SACsB;CACtB,MAAM,SAAS,cAAc,WAAW,QAAQ,MAAM,GAAG,CAAC,QAAQ,MAAM,CAAC;CACzE,MAAM,CAAC,UAAU,cAAc,SAAS,CAAC;CACzC,MAAM,eAAe,kBAAkB,YAAY,MAAM,IAAI,CAAC,GAAG,CAAC,CAAC;CACnE,MAAM,UAAU,OAAqC,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,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;CACF,CAAC;CAGH,MAAM,OAAO,QAAQ;CACrB,KAAK,cAAc;EACjB,OAAO,QAAQ;EACf;EACA,cAAc,QAAQ,gBAAgB;EACtC,4BAA4B,QAAQ;EACpC,GAAI,QAAQ,WAAW,EAAE,SAAS,QAAQ,QAAQ;EAClD,GAAI,QAAQ,YAAY,EAAE,UAAU,QAAQ,SAAS;EACrD,GAAI,kBAAkB,EAAE,SAAS,eAAe;CAClD,CAAC;CAED,MAAM,CAAC,cAAc,mBAAmB,SAEtC,CAAC,CAAC;CAEJ,MAAM,oBAAoB,OAAO,OAAO,YAAY,CAAC,CAAC,MACnD,MAAM,GAAG,SAAS,WACrB;CAEA,MAAM,aAAa,cAAc;EAC/B,IAAI,CAAC,mBAAmB,OAAO,KAAA;EAE/B,MAAM,EAAE,qBAAqB,kBAAkB,GAAG,SAChD;EAEF,OAAO;GACL,GAAG;GACH,qBAAqB,sBACjB,YAAY;IACV,MAAM,oBAAoB;IAC1B,KAAK,sBAAsB,CAAC,CAAC;IAC7B,KAAK,WAAW;GAClB,IACA,KAAA;GACJ,kBAAkB,mBACd,OAAO,aAAqB;IAG1B,KAAK,sBAAsB,CAAC,CAAC;IAC7B,MAAM,SAAS,MAAM,iBAAiB,QAAQ;IAC9C,KAAK,sBAAsB,OAAO,QAAQ;IAC1C,IAAI,OAAO,UAAU,KAAA,GACnB,KAAK,kBAAkB,OAAO,KAAK;SAEnC,KAAK,WAAW;IAElB,IAAI,OAAO,iBACT,KAAU,kBAAkB,OAAO,QAAQ;GAE/C,IACA,KAAA;EACN;CACF,GAAG,CAAC,mBAAmB,IAAI,CAAC;CAE5B,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;CA6CpD,MAAM,cAAc,wBA5CN,cACN;EAGJ,OAAO;GACL,GAAG;GACH,WAAW,KAAK;GAChB,mBAAmB,KAAK,qBAAqB;GAC7C,OAAO,KAAK,SAAS;GACrB,WAAW,KAAK,UAAU,KAAK;GAC/B,QAAQ,WAAW,QAAQ;IACzB,YACE,KAAK,qBAAqB,CAAC,EAAE,cAAc;IAC7C,2BAA2B,cACzB,KAAK,yBAAyB,SAAS;IACzC,YAAY,SAAS,cAAc,KAAK,UAAU,SAAS,SAAS;IACpE,OAAO,KAAK,SAAS;IACrB,WAAW,SAAS,KAAK,SAAS,IAAI;GACxC,CAAC;GACD,gCAAgC;GAChC;GACA,QAAQ,YAA2B,KAAK,OAAO,OAAO;GACtD,SAAS,YAA2B,KAAK,KAAK,OAAO;GACrD,WAAW,UAAyB,WAClC,KAAK,OAAO,UAAU,MAAM;GAC9B,UAAU,YAAY;IACpB,KAAK,OAAO;GACd;GACA,kBAAkB,YAAY,KAAK,cAAc,OAAO;GACxD,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;EACZ;CACF,GAGA;EAAC;EAAiB;EAAM;EAAU;EAAmB;CAAM,CAGb,CAAC;CAEjD,MAAM,UAAU,cAAoC;EAClD,MAAM,UAAU,OAAO,OAAO,WAAW;EACzC,QAAQ,sCACN,KAAK,qBAAqB,CAAC,EAAE,cAAc,CAAC;EAC9C,QAAQ,qCAAqC,cAC3C,KAAK,yBAAyB,SAAS;EACzC,OAAO;CACT,GAAG,CAAC,aAAa,IAAI,CAAC;CAEtB,gBAAgB;EACd,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 { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport {\n useExternalStoreRuntime,\n useExternalStoreSharedOptions,\n useRuntimeAdapters,\n} from \"@assistant-ui/core/react\";\nimport type { ToolExecutionStatus } 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\";\n\nconst EMPTY_INTERRUPTS: readonly AgUiInterrupt[] = [];\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 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 const [toolStatuses, setToolStatuses] = useState<\n Record<string, ToolExecutionStatus>\n >({});\n\n const hasExecutingTools = Object.values(toolStatuses).some(\n (s) => s?.type === \"executing\",\n );\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() || hasExecutingTools,\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) => core.edit(message),\n onReload: (parentId: string | null, config: { runConfig?: any }) =>\n core.reload(parentId, config),\n onCancel: async () => {\n core.cancel();\n },\n onAddToolResult: (options) => core.addToolResult(options),\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 } 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 [adapterAdapters, core, _version, hasExecutingTools, shared],\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.48",
3
+ "version": "0.0.49",
4
4
  "description": "AG-UI protocol adapter for assistant-ui",
5
5
  "keywords": [
6
6
  "ag-ui",
@@ -30,9 +30,10 @@
30
30
  "sideEffects": false,
31
31
  "dependencies": {
32
32
  "@ag-ui/client": "^0.0.57",
33
- "@assistant-ui/core": "^0.3.0",
34
- "@assistant-ui/store": "^0.3.0",
35
- "assistant-stream": "^0.3.29",
33
+ "@assistant-ui/core": "^0.3.2",
34
+ "@assistant-ui/react-generative-ui": "^0.0.11",
35
+ "@assistant-ui/store": "^0.3.2",
36
+ "assistant-stream": "^0.3.31",
36
37
  "fast-json-patch": "^3.1.1"
37
38
  },
38
39
  "peerDependencies": {
package/src/agUiExtras.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { createRuntimeExtras } from "@assistant-ui/core/internal";
1
+ import { createRuntimeExtras } from "@assistant-ui/core/react";
2
2
  import type { AgUiRuntimeExtras } from "./runtime/types";
3
3
 
4
4
  export const agUiExtras =
package/src/hooks.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
 
3
+ import { useCallback } from "react";
3
4
  import { useAui } from "@assistant-ui/store";
4
5
  import type { CreateAppendMessage } from "@assistant-ui/core";
5
6
  import type { ReadonlyJSONValue } from "assistant-stream/utils";
@@ -15,6 +16,19 @@ const EMPTY_INTERRUPTS: readonly AgUiInterrupt[] = [];
15
16
  export const useAgUiInterrupts = (): readonly AgUiInterrupt[] =>
16
17
  agUiExtras.use((e) => e.interrupts, EMPTY_INTERRUPTS);
17
18
 
19
+ /**
20
+ * Returns a stable function that sends an A2UI action back to the agent and
21
+ * resumes the run.
22
+ */
23
+ export const useAgUiSendA2uiAction = () => {
24
+ const aui = useAui();
25
+ return useCallback(
26
+ (action: Record<string, unknown>): void =>
27
+ agUiExtras.get(aui).sendA2uiAction(action),
28
+ [aui],
29
+ );
30
+ };
31
+
18
32
  /**
19
33
  * Returns a function that submits responses (each `resolved` or `cancelled`)
20
34
  * for the pending AG-UI interrupts and resumes the run.
package/src/index.ts CHANGED
@@ -2,6 +2,7 @@ export { useAgUiRuntime } from "./useAgUiRuntime";
2
2
  export type { AgUiAssistantRuntime } from "./useAgUiRuntime";
3
3
  export {
4
4
  useAgUiInterrupts,
5
+ useAgUiSendA2uiAction,
5
6
  useAgUiSubmitInterruptResponses,
6
7
  useAgUiSteerAway,
7
8
  useAgUiState,
@@ -99,6 +99,8 @@ export class AgUiThreadRuntimeCore {
99
99
  private _isLoading = false;
100
100
  private _loadPromise: Promise<void> | undefined;
101
101
  private pendingResumeMessageId: string | null = null;
102
+ private pendingA2uiResume = false;
103
+ private pendingA2uiAction: Record<string, unknown> | undefined;
102
104
 
103
105
  constructor(options: CoreOptions) {
104
106
  this.agent = options.agent;
@@ -688,6 +690,31 @@ export class AgUiThreadRuntimeCore {
688
690
  this.maybeResumeAfterToolResults(options.messageId);
689
691
  }
690
692
 
693
+ sendA2uiAction(action: Record<string, unknown>): void {
694
+ this.assertNoPendingInterrupts();
695
+ this.maybeAutoCancelPendingToolCalls();
696
+ const parentId = this.repository.headId;
697
+ if (parentId === null) {
698
+ this.logger.debug(
699
+ "[agui] sendA2uiAction: no messages to resume, dropping action",
700
+ );
701
+ return;
702
+ }
703
+
704
+ const userAction = { ...action };
705
+ delete userAction.type;
706
+ if (!("timestamp" in userAction)) {
707
+ userAction.timestamp = new Date().toISOString();
708
+ }
709
+ this.pendingA2uiAction = userAction;
710
+
711
+ if (this.isRunningFlag) {
712
+ this.pendingA2uiResume = true;
713
+ return;
714
+ }
715
+ this.startResumeRun(parentId);
716
+ }
717
+
691
718
  // The continuation fires whether the frontend result lands before
692
719
  // RUN_FINISHED (the status flips to requires-action only later, while the
693
720
  // run is still draining) or after it.
@@ -739,6 +766,8 @@ export class AgUiThreadRuntimeCore {
739
766
  }
740
767
 
741
768
  applyExternalMessages(messages: readonly ThreadMessage[]): void {
769
+ this.pendingA2uiResume = false;
770
+ this.pendingA2uiAction = undefined;
742
771
  this.assistantHistoryParents.clear();
743
772
 
744
773
  if (messages.length === 0) {
@@ -892,20 +921,38 @@ export class AgUiThreadRuntimeCore {
892
921
  this.pendingError = null;
893
922
  const assistantParentId = parent ? parentId : this.repository.headId;
894
923
  let assistantMessageId: string | undefined;
895
- const ensureAssistant = () => {
896
- if (assistantMessageId) return assistantMessageId;
897
- const created = this.insertAssistantPlaceholder(
898
- assistantParentId ?? null,
899
- );
924
+ // A snapshot the preserve gate declines still evicts the in-flight
925
+ // assistant; recreating under the cached id on the next content-bearing
926
+ // emit keeps both the stream and the message identity. Status-only emits
927
+ // and server-id collisions must not recreate.
928
+ let assistantCollided = false;
929
+ const ensureAssistant = (allowRecreate = false): string => {
930
+ const cached = assistantMessageId;
931
+ if (cached !== undefined && this.tryGetMessage(cached)) return cached;
932
+ if (cached !== undefined && (assistantCollided || !allowRecreate)) {
933
+ return cached;
934
+ }
935
+ const parentId =
936
+ cached === undefined &&
937
+ assistantParentId &&
938
+ this.hasMessage(assistantParentId)
939
+ ? assistantParentId
940
+ : this.repository.headId;
941
+ const created = this.insertAssistantPlaceholder(parentId, cached);
900
942
  assistantMessageId = created;
901
- this.markPendingAssistantHistory(created, assistantParentId ?? null);
943
+ this.markPendingAssistantHistory(created, parentId);
902
944
  return created;
903
945
  };
904
946
 
905
947
  if (shouldEagerlyInsertAssistant) ensureAssistant();
906
948
 
907
949
  const applyUpdate = (update: ChatModelRunResult) => {
908
- const resolved = this.updateAssistantMessage(ensureAssistant(), update);
950
+ const hasContent =
951
+ Array.isArray(update.content) && update.content.length > 0;
952
+ const resolved = this.updateAssistantMessage(
953
+ ensureAssistant(hasContent),
954
+ update,
955
+ );
909
956
  if (resolved !== assistantMessageId) {
910
957
  assistantMessageId = resolved;
911
958
  }
@@ -916,14 +963,17 @@ export class AgUiThreadRuntimeCore {
916
963
  logger: this.logger,
917
964
  emit: applyUpdate,
918
965
  onServerMessageId: (serverId) => {
919
- const placeholder = ensureAssistant();
966
+ const placeholder = ensureAssistant(true);
920
967
  if (placeholder === serverId) return;
921
968
  if (this.reassignAssistantId(placeholder, serverId)) {
922
969
  assistantMessageId = serverId;
970
+ } else {
971
+ assistantCollided = true;
923
972
  }
924
973
  },
925
974
  });
926
- const dispatch = (event: AgUiEvent) => this.handleEvent(aggregator, event);
975
+ const dispatch = (event: AgUiEvent) =>
976
+ this.handleEvent(aggregator, event, assistantMessageId);
927
977
 
928
978
  const abortController = new AbortController();
929
979
  const abortSignal = abortController.signal;
@@ -1001,6 +1051,8 @@ export class AgUiThreadRuntimeCore {
1001
1051
  const err = this.pendingError;
1002
1052
  this.pendingError = null;
1003
1053
  this.pendingResumeMessageId = null;
1054
+ this.pendingA2uiResume = false;
1055
+ this.pendingA2uiAction = undefined;
1004
1056
  throw err;
1005
1057
  }
1006
1058
 
@@ -1011,6 +1063,33 @@ export class AgUiThreadRuntimeCore {
1011
1063
  this.pendingResumeMessageId = null;
1012
1064
  if (!abortSignal.aborted) {
1013
1065
  this.startResumeRun(resumeMessageId);
1066
+ } else {
1067
+ this.pendingA2uiAction = undefined;
1068
+ }
1069
+ }
1070
+
1071
+ if (this.pendingA2uiResume) {
1072
+ this.pendingA2uiResume = false;
1073
+ if (!abortSignal.aborted && this.pendingA2uiAction !== undefined) {
1074
+ if (this.getPendingInterrupts()) {
1075
+ this.pendingA2uiAction = undefined;
1076
+ this.logger.debug(
1077
+ "[agui] sendA2uiAction: pending interrupts, dropping action",
1078
+ );
1079
+ return;
1080
+ }
1081
+ this.maybeAutoCancelPendingToolCalls();
1082
+ const parentId = this.repository.headId;
1083
+ if (parentId !== null) {
1084
+ this.startResumeRun(parentId);
1085
+ } else {
1086
+ this.pendingA2uiAction = undefined;
1087
+ this.logger.debug(
1088
+ "[agui] sendA2uiAction: no messages to resume, dropping action",
1089
+ );
1090
+ }
1091
+ } else {
1092
+ this.pendingA2uiAction = undefined;
1014
1093
  }
1015
1094
  }
1016
1095
  }
@@ -1029,6 +1108,8 @@ export class AgUiThreadRuntimeCore {
1029
1108
  getAssistantMessageId: () => string | undefined;
1030
1109
  },
1031
1110
  ): Promise<void> {
1111
+ this.pendingA2uiAction = undefined;
1112
+ this.pendingA2uiResume = false;
1032
1113
  const assistantId = ctx.ensureAssistant();
1033
1114
  const currentId = () => ctx.getAssistantMessageId() ?? assistantId;
1034
1115
  const options: ChatModelRunOptions = {
@@ -1084,7 +1165,7 @@ export class AgUiThreadRuntimeCore {
1084
1165
  historyMessages ?? this.repository.getMessages(),
1085
1166
  );
1086
1167
  const context = this.runtime?.thread.getModelContext();
1087
- return {
1168
+ const input = {
1088
1169
  threadId,
1089
1170
  runId,
1090
1171
  state: this.stateSnapshot ?? null,
@@ -1097,9 +1178,14 @@ export class AgUiThreadRuntimeCore {
1097
1178
  ...(context?.callSettings ?? {}),
1098
1179
  ...(context?.config ?? {}),
1099
1180
  ...(runConfig?.custom ? { runConfig: runConfig.custom } : {}),
1181
+ ...(this.pendingA2uiAction
1182
+ ? { a2uiAction: { userAction: this.pendingA2uiAction } }
1183
+ : {}),
1100
1184
  },
1101
1185
  ...(resume !== undefined ? { resume } : {}),
1102
1186
  };
1187
+ this.pendingA2uiAction = undefined;
1188
+ return input;
1103
1189
  }
1104
1190
 
1105
1191
  private installResumeShim(): void {
@@ -1135,8 +1221,10 @@ export class AgUiThreadRuntimeCore {
1135
1221
  this.setRunning(false);
1136
1222
  }
1137
1223
 
1138
- private insertAssistantPlaceholder(parentId: string | null): string {
1139
- const id = generateOptimisticId();
1224
+ private insertAssistantPlaceholder(
1225
+ parentId: string | null,
1226
+ id: string = generateOptimisticId(),
1227
+ ): string {
1140
1228
  const assistant: ThreadAssistantMessage = {
1141
1229
  id,
1142
1230
  role: "assistant",
@@ -1148,7 +1236,7 @@ export class AgUiThreadRuntimeCore {
1148
1236
  unstable_annotations: [],
1149
1237
  unstable_data: [],
1150
1238
  steps: [],
1151
- isOptimistic: true,
1239
+ isOptimistic: isOptimisticId(id),
1152
1240
  custom: {},
1153
1241
  },
1154
1242
  };
@@ -1307,7 +1395,11 @@ export class AgUiThreadRuntimeCore {
1307
1395
  };
1308
1396
  }
1309
1397
 
1310
- private handleEvent(aggregator: RunAggregator, event: AgUiEvent) {
1398
+ private handleEvent(
1399
+ aggregator: RunAggregator,
1400
+ event: AgUiEvent,
1401
+ activeAssistantId: string | undefined,
1402
+ ) {
1311
1403
  switch (event.type) {
1312
1404
  case "STATE_SNAPSHOT": {
1313
1405
  this.stateSnapshot = event.snapshot as ReadonlyJSONValue;
@@ -1332,7 +1424,7 @@ export class AgUiThreadRuntimeCore {
1332
1424
  return;
1333
1425
  }
1334
1426
  case "MESSAGES_SNAPSHOT": {
1335
- this.importMessagesSnapshot(event.messages);
1427
+ this.importMessagesSnapshot(event.messages, activeAssistantId);
1336
1428
  return;
1337
1429
  }
1338
1430
  case "TOOL_CALL_RESULT": {
@@ -1504,8 +1596,16 @@ export class AgUiThreadRuntimeCore {
1504
1596
  this.maybeCompleteAfterToolResults(messageId);
1505
1597
  }
1506
1598
 
1507
- private importMessagesSnapshot(rawMessages: readonly unknown[]) {
1599
+ private importMessagesSnapshot(
1600
+ rawMessages: readonly unknown[],
1601
+ activeAssistantId: string | undefined,
1602
+ ) {
1508
1603
  try {
1604
+ const activeMessage = activeAssistantId
1605
+ ? this.tryGetMessage(activeAssistantId)?.message
1606
+ : undefined;
1607
+ const activeAssistant =
1608
+ activeMessage?.role === "assistant" ? activeMessage : undefined;
1509
1609
  const normalized = fromAgUiMessages(rawMessages, {
1510
1610
  showThinking: this.showThinking,
1511
1611
  });
@@ -1522,7 +1622,23 @@ export class AgUiThreadRuntimeCore {
1522
1622
  );
1523
1623
  }
1524
1624
  }
1625
+ const snapshotHeadId = converted.at(-1)?.id ?? null;
1626
+ const snapshotContainsActiveAssistant = converted.some(
1627
+ (message) => message.id === activeAssistant?.id,
1628
+ );
1629
+ const preservesActiveAssistant =
1630
+ activeAssistant !== undefined &&
1631
+ !snapshotContainsActiveAssistant &&
1632
+ (activeAssistant.metadata.isOptimistic !== true ||
1633
+ converted.at(-1)?.role !== "assistant");
1634
+ if (preservesActiveAssistant) {
1635
+ converted.push(activeAssistant);
1636
+ }
1525
1637
  this.applyExternalMessages(converted);
1638
+ if (preservesActiveAssistant) {
1639
+ this.recordedHistoryIds.delete(activeAssistant.id);
1640
+ this.markPendingAssistantHistory(activeAssistant.id, snapshotHeadId);
1641
+ }
1526
1642
  } catch (error) {
1527
1643
  this.logger.error?.("[agui] failed to import messages snapshot", error);
1528
1644
  }
@@ -15,8 +15,15 @@ import { type Tool, toToolsJSONSchema } from "assistant-stream";
15
15
  import type { ReadonlyJSONObject } from "assistant-stream/utils";
16
16
  import {
17
17
  AG_UI_METADATA_NAMESPACE,
18
+ A2UI_SURFACE_ACTIVITY_TYPE,
18
19
  type AgUiCustomMetadata,
19
20
  } from "./run-aggregator";
21
+ import {
22
+ applyA2uiOperations,
23
+ convertSurfaceToUISpec,
24
+ type A2uiState,
25
+ type A2uiSurfaceState,
26
+ } from "@assistant-ui/react-generative-ui/a2ui";
20
27
  import type { AgUiInterrupt } from "../types";
21
28
  import {
22
29
  parseMcpToolCallResult,
@@ -211,6 +218,21 @@ function toInputContent(
211
218
  }
212
219
  }
213
220
 
221
+ if (type === "audio") {
222
+ const audio = part.audio;
223
+ if (!isObject(audio)) return null;
224
+ const data = getString(audio, "data");
225
+ const format = getString(audio, "format");
226
+ if (data === undefined || format === undefined) return null;
227
+ return {
228
+ type: "audio",
229
+ source: buildInputSource(
230
+ parseDataUrl(data)?.data ?? data,
231
+ `audio/${format}`,
232
+ ),
233
+ };
234
+ }
235
+
214
236
  return null;
215
237
  }
216
238
 
@@ -517,6 +539,42 @@ function toUserOrSystemSnapshotMessage(
517
539
  };
518
540
  }
519
541
 
542
+ // Rebuilds the a2ui:<surfaceId> "present" tool-call parts for one owning
543
+ // assistant message from a bucket of rebuilt surface state, mirroring the
544
+ // live RunAggregator.synthesizeA2uiToolCalls shape. Non-a2ui parts (text,
545
+ // other tool calls) are preserved; existing a2ui parts are replaced wholesale
546
+ // so create/update/delete within the bucket all converge on the rebuilt set.
547
+ function attachA2uiSurfaces(
548
+ message: CoreThreadMessageLike,
549
+ state: A2uiState,
550
+ ): CoreThreadMessageLike {
551
+ const a2uiParts: ToolCallPart[] = [];
552
+ for (const [surfaceId, surface] of state) {
553
+ const { spec } = convertSurfaceToUISpec(surface);
554
+ if (!spec) continue;
555
+ a2uiParts.push({
556
+ type: "tool-call",
557
+ toolCallId: `a2ui:${surfaceId}`,
558
+ toolName: "present",
559
+ args: spec as unknown as ReadonlyJSONObject,
560
+ argsText: JSON.stringify(spec),
561
+ result: {},
562
+ });
563
+ }
564
+
565
+ const content = Array.isArray(message.content) ? message.content : [];
566
+ const preserved = content.filter(
567
+ (part) =>
568
+ !(
569
+ isObject(part) &&
570
+ part.type === "tool-call" &&
571
+ typeof part.toolCallId === "string" &&
572
+ part.toolCallId.startsWith("a2ui:")
573
+ ),
574
+ );
575
+ return { ...message, content: [...preserved, ...a2uiParts] };
576
+ }
577
+
520
578
  export type FromAgUiMessagesOptions = {
521
579
  /**
522
580
  * Whether to convert `reasoning` messages into visible reasoning parts.
@@ -531,6 +589,8 @@ export function fromAgUiMessages(
531
589
  ): CoreThreadMessageLike[] {
532
590
  const showThinking = options?.showThinking ?? true;
533
591
  const converted: CoreThreadMessageLike[] = [];
592
+ const a2uiBuckets = new Map<string, A2uiState>();
593
+ const a2uiBucketOwnerIndices = new Map<string, number>();
534
594
 
535
595
  for (const rawMessage of messages) {
536
596
  if (!isObject(rawMessage)) continue;
@@ -638,6 +698,47 @@ export function fromAgUiMessages(
638
698
  continue;
639
699
  }
640
700
 
701
+ if (role === "activity") {
702
+ // Only a2ui-surface activity messages have an assistant-part equivalent
703
+ // to rehydrate; other activity types still have no surface to repaint.
704
+ const activityType = getString(rawMessage, "activityType");
705
+ if (activityType !== A2UI_SURFACE_ACTIVITY_TYPE) continue;
706
+ const activityContent = isObject(rawMessage.content)
707
+ ? (rawMessage.content as Record<string, unknown>)
708
+ : null;
709
+ const operations = activityContent?.["a2ui_operations"];
710
+ if (!Array.isArray(operations)) continue;
711
+
712
+ let ownerIndex = -1;
713
+ for (let i = converted.length - 1; i >= 0; i--) {
714
+ const candidate = converted[i];
715
+ if (candidate && candidate.role === "assistant") {
716
+ ownerIndex = i;
717
+ break;
718
+ }
719
+ }
720
+ if (ownerIndex === -1) continue;
721
+
722
+ const owner = converted[ownerIndex]!;
723
+ const bucketKey = getString(rawMessage, "id") ?? "a2ui:anonymous";
724
+ const { state } = applyA2uiOperations(new Map(), operations);
725
+ a2uiBuckets.delete(bucketKey);
726
+ a2uiBucketOwnerIndices.delete(bucketKey);
727
+ a2uiBuckets.set(bucketKey, state);
728
+ a2uiBucketOwnerIndices.set(bucketKey, ownerIndex);
729
+
730
+ const ownerState = new Map<string, A2uiSurfaceState>();
731
+ for (const [candidateBucketKey, candidateState] of a2uiBuckets) {
732
+ if (a2uiBucketOwnerIndices.get(candidateBucketKey) !== ownerIndex)
733
+ continue;
734
+ for (const [surfaceId, surface] of candidateState) {
735
+ ownerState.set(surfaceId, surface);
736
+ }
737
+ }
738
+ converted[ownerIndex] = attachA2uiSurfaces(owner, ownerState);
739
+ continue;
740
+ }
741
+
641
742
  if (role === "assistant") {
642
743
  converted.push(toAssistantSnapshotMessage(rawMessage));
643
744
  continue;
@@ -702,7 +803,12 @@ function convertAssistantMessage(
702
803
  const contentArray = Array.isArray(message.content) ? message.content : [];
703
804
 
704
805
  const toolCallParts = contentArray.filter(
705
- (part): part is ToolCallPart => part?.type === "tool-call",
806
+ (part): part is ToolCallPart =>
807
+ part?.type === "tool-call" &&
808
+ !(
809
+ typeof part.toolCallId === "string" &&
810
+ part.toolCallId.startsWith("a2ui:")
811
+ ),
706
812
  );
707
813
 
708
814
  const toolCalls = toolCallParts.map((part) => ({