@assistant-ui/react-ag-ui 0.0.44 → 0.0.46

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 (47) hide show
  1. package/dist/agUiExtras.d.ts +0 -1
  2. package/dist/agUiExtras.d.ts.map +1 -1
  3. package/dist/hooks.d.ts +15 -9
  4. package/dist/hooks.d.ts.map +1 -1
  5. package/dist/hooks.js +17 -8
  6. package/dist/hooks.js.map +1 -1
  7. package/dist/index.d.ts +2 -2
  8. package/dist/index.js +2 -2
  9. package/dist/runtime/AgUiThreadRuntimeCore.d.ts +28 -4
  10. package/dist/runtime/AgUiThreadRuntimeCore.d.ts.map +1 -1
  11. package/dist/runtime/AgUiThreadRuntimeCore.js +379 -114
  12. package/dist/runtime/AgUiThreadRuntimeCore.js.map +1 -1
  13. package/dist/runtime/adapter/conversions.d.ts +0 -1
  14. package/dist/runtime/adapter/conversions.d.ts.map +1 -1
  15. package/dist/runtime/adapter/conversions.js +12 -12
  16. package/dist/runtime/adapter/conversions.js.map +1 -1
  17. package/dist/runtime/adapter/run-aggregator.d.ts +3 -2
  18. package/dist/runtime/adapter/run-aggregator.d.ts.map +1 -1
  19. package/dist/runtime/adapter/run-aggregator.js +48 -11
  20. package/dist/runtime/adapter/run-aggregator.js.map +1 -1
  21. package/dist/runtime/adapter/subscriber.d.ts +0 -1
  22. package/dist/runtime/adapter/subscriber.d.ts.map +1 -1
  23. package/dist/runtime/event-parser.d.ts +0 -1
  24. package/dist/runtime/event-parser.d.ts.map +1 -1
  25. package/dist/runtime/event-parser.js +23 -11
  26. package/dist/runtime/event-parser.js.map +1 -1
  27. package/dist/runtime/logger.d.ts.map +1 -1
  28. package/dist/runtime/mcp-tool-result.d.ts +11 -0
  29. package/dist/runtime/mcp-tool-result.d.ts.map +1 -0
  30. package/dist/runtime/mcp-tool-result.js +20 -0
  31. package/dist/runtime/mcp-tool-result.js.map +1 -0
  32. package/dist/runtime/types.d.ts +15 -1
  33. package/dist/runtime/types.d.ts.map +1 -1
  34. package/dist/useAgUiRuntime.d.ts +0 -1
  35. package/dist/useAgUiRuntime.d.ts.map +1 -1
  36. package/dist/useAgUiRuntime.js +8 -2
  37. package/dist/useAgUiRuntime.js.map +1 -1
  38. package/package.json +7 -7
  39. package/src/hooks.ts +29 -7
  40. package/src/index.ts +2 -0
  41. package/src/runtime/AgUiThreadRuntimeCore.ts +547 -147
  42. package/src/runtime/adapter/conversions.ts +26 -17
  43. package/src/runtime/adapter/run-aggregator.ts +84 -7
  44. package/src/runtime/event-parser.ts +4 -1
  45. package/src/runtime/mcp-tool-result.ts +26 -0
  46. package/src/runtime/types.ts +19 -0
  47. package/src/useAgUiRuntime.ts +8 -1
@@ -1,8 +1,15 @@
1
1
  "use client";
2
2
 
3
3
  import type { InputContent } from "@ag-ui/client";
4
- import type { ThreadMessageLike as CoreThreadMessageLike } from "@assistant-ui/core";
5
- import { getAutoStatus } from "@assistant-ui/core/internal";
4
+ import type {
5
+ ThreadMessageLike as CoreThreadMessageLike,
6
+ ToolModelContentPart,
7
+ } from "@assistant-ui/core";
8
+ import {
9
+ getAutoStatus,
10
+ httpUrlPattern,
11
+ parseDataUrl,
12
+ } from "@assistant-ui/core/internal";
6
13
  import { type Tool, toToolsJSONSchema } from "assistant-stream";
7
14
  import type { ReadonlyJSONObject } from "assistant-stream/utils";
8
15
  import {
@@ -10,6 +17,7 @@ import {
10
17
  type AgUiCustomMetadata,
11
18
  } from "./run-aggregator";
12
19
  import type { AgUiInterrupt } from "../types";
20
+ import { parseMcpToolCallResult } from "../mcp-tool-result";
13
21
 
14
22
  export type { InputContent };
15
23
 
@@ -59,6 +67,7 @@ type ToolCallPart = {
59
67
  args?: ReadonlyJSONObject;
60
68
  result?: unknown;
61
69
  isError?: boolean;
70
+ modelContent?: readonly ToolModelContentPart[];
62
71
  unstable_toolMessageId?: string;
63
72
  };
64
73
 
@@ -125,16 +134,6 @@ function extractText(content: unknown): string {
125
134
  .join("\n");
126
135
  }
127
136
 
128
- function parseDataUrl(
129
- value: string,
130
- ): { mimeType: string; data: string } | null {
131
- const match = value.match(/^data:([^;,]+)(?:;[^;,]+)*;base64,(.+)$/);
132
- if (!match) return null;
133
- return { mimeType: match[1]!, data: match[2]! };
134
- }
135
-
136
- const httpUrlPattern = /^https?:\/\//i;
137
-
138
137
  type InputContentSource =
139
138
  | { type: "data"; value: string; mimeType: string }
140
139
  | { type: "url"; value: string; mimeType?: string };
@@ -536,12 +535,18 @@ export function fromAgUiMessages(
536
535
  if (role === "tool") {
537
536
  const toolCallId = getToolCallId(rawMessage) ?? `tool-${generateId()}`;
538
537
  const toolMessageId = getString(rawMessage, "id");
538
+ const modelContent = extractText(rawMessage.content);
539
+ const mcpResult = parseMcpToolCallResult(rawMessage, modelContent);
540
+ const mcpModelContent = mcpResult
541
+ ? [{ type: "text" as const, text: modelContent }]
542
+ : undefined;
539
543
  const result =
540
- rawMessage.result !== undefined
544
+ mcpResult ??
545
+ (rawMessage.result !== undefined
541
546
  ? rawMessage.result
542
547
  : typeof rawMessage.content === "string"
543
548
  ? parseJSONText(rawMessage.content)
544
- : rawMessage.content;
549
+ : rawMessage.content);
545
550
  const isError =
546
551
  typeof rawMessage.error === "string" ||
547
552
  rawMessage.isError === true ||
@@ -577,6 +582,7 @@ export function fromAgUiMessages(
577
582
  const updatedPart: ToolCallPart = {
578
583
  ...(part as ToolCallPart),
579
584
  result,
585
+ ...(mcpModelContent ? { modelContent: mcpModelContent } : {}),
580
586
  ...(isError !== undefined ? { isError } : {}),
581
587
  ...(toolMessageId !== undefined
582
588
  ? { unstable_toolMessageId: toolMessageId }
@@ -611,6 +617,7 @@ export function fromAgUiMessages(
611
617
  args: {},
612
618
  argsText: "{}",
613
619
  result,
620
+ ...(mcpModelContent ? { modelContent: mcpModelContent } : {}),
614
621
  ...(isError !== undefined ? { isError } : {}),
615
622
  ...(toolMessageId !== undefined
616
623
  ? { unstable_toolMessageId: toolMessageId }
@@ -716,9 +723,11 @@ function convertAssistantMessage(
716
723
  if (part.result === undefined) continue;
717
724
 
718
725
  const resultContent =
719
- typeof part.result === "string"
720
- ? part.result
721
- : JSON.stringify(part.result);
726
+ part.modelContent !== undefined
727
+ ? extractText(part.modelContent)
728
+ : typeof part.result === "string"
729
+ ? part.result
730
+ : JSON.stringify(part.result);
722
731
 
723
732
  const toolMessage: AgUiMessage = {
724
733
  id: part.unstable_toolMessageId ?? `${toolCallId}:tool`,
@@ -6,6 +6,7 @@ import {
6
6
  type MessageTiming,
7
7
  type ThreadAssistantMessagePart,
8
8
  type ToolCallMessagePart,
9
+ type ToolModelContentPart,
9
10
  } from "@assistant-ui/core";
10
11
  import type { AgUiEvent, AgUiInterrupt } from "../types";
11
12
  import type { Logger } from "../logger";
@@ -28,9 +29,17 @@ type ToolCallState = {
28
29
  parentMessageId?: string;
29
30
  toolMessageId?: string;
30
31
  mcpAppResourceUri?: string;
32
+ mcpAppServerId?: string;
33
+ modelContent?: ToolModelContentPart[];
34
+ snapshotResultApplied: boolean;
31
35
  };
32
36
 
33
- const MCP_APPS_ACTIVITY_TYPE = "mcp-apps";
37
+ export const MCP_APPS_ACTIVITY_TYPE = "mcp-apps";
38
+
39
+ export const isPlainObject = (
40
+ value: unknown,
41
+ ): value is Record<string, unknown> =>
42
+ !!value && typeof value === "object" && !Array.isArray(value);
34
43
 
35
44
  export type RunAggregatorOptions = {
36
45
  showThinking: boolean;
@@ -228,16 +237,26 @@ export class RunAggregator {
228
237
  this.finishToolCall(
229
238
  event.toolCallId,
230
239
  event.content ?? "",
231
- event.role === "tool" ? false : undefined,
240
+ typeof event.mcpResult?.isError === "boolean"
241
+ ? event.mcpResult.isError
242
+ : event.role === "tool"
243
+ ? false
244
+ : undefined,
232
245
  event.messageId,
246
+ event.mcpResult,
233
247
  );
234
248
  this.emit();
235
249
  break;
236
250
  }
237
251
  case "ACTIVITY_SNAPSHOT": {
238
252
  if (event.activityType !== MCP_APPS_ACTIVITY_TYPE) break;
239
- const id = this.lastResolvedToolCallId;
240
- const entry = id ? this.toolCalls.get(id) : undefined;
253
+ const toolCallId = event.content.toolCallId;
254
+ const entry =
255
+ typeof toolCallId === "string"
256
+ ? this.toolCalls.get(toolCallId)
257
+ : this.lastResolvedToolCallId
258
+ ? this.toolCalls.get(this.lastResolvedToolCallId)
259
+ : undefined;
241
260
  const resourceUri = event.content.resourceUri;
242
261
  if (
243
262
  entry &&
@@ -245,6 +264,35 @@ export class RunAggregator {
245
264
  isMcpAppUri(resourceUri)
246
265
  ) {
247
266
  entry.mcpAppResourceUri = resourceUri;
267
+ const id = event.content.serverId;
268
+ const hash = event.content.serverHash;
269
+ if (typeof id === "string" && id.length > 0) {
270
+ entry.mcpAppServerId = id;
271
+ } else if (typeof hash === "string" && hash.length > 0) {
272
+ entry.mcpAppServerId = hash;
273
+ }
274
+ const result = event.content.result;
275
+ if (isPlainObject(result)) {
276
+ if (
277
+ entry.result !== undefined &&
278
+ entry.modelContent === undefined
279
+ ) {
280
+ entry.modelContent = [
281
+ {
282
+ type: "text",
283
+ text:
284
+ typeof entry.result === "string"
285
+ ? entry.result
286
+ : JSON.stringify(entry.result),
287
+ },
288
+ ];
289
+ }
290
+ entry.result = result;
291
+ entry.snapshotResultApplied = true;
292
+ if (typeof result.isError === "boolean") {
293
+ entry.isError = result.isError;
294
+ }
295
+ }
248
296
  this.emit();
249
297
  }
250
298
  break;
@@ -343,6 +391,7 @@ export class RunAggregator {
343
391
  parsedArgs: undefined,
344
392
  result: undefined,
345
393
  isError: undefined,
394
+ snapshotResultApplied: false,
346
395
  };
347
396
  if (parentMessageId) {
348
397
  state.parentMessageId = parentMessageId;
@@ -400,6 +449,7 @@ export class RunAggregator {
400
449
  content: string,
401
450
  isError?: boolean,
402
451
  toolMessageId?: string,
452
+ mcpResult?: Record<string, unknown>,
403
453
  ) {
404
454
  if (!id) return;
405
455
  let entry = this.toolCalls.get(id);
@@ -411,6 +461,7 @@ export class RunAggregator {
411
461
  parsedArgs: undefined,
412
462
  result: undefined,
413
463
  isError: undefined,
464
+ snapshotResultApplied: false,
414
465
  };
415
466
  this.toolCalls.set(id, entry);
416
467
  }
@@ -421,8 +472,22 @@ export class RunAggregator {
421
472
  ) {
422
473
  this.partOrder.push({ kind: "tool-call", toolCallId: id });
423
474
  }
424
- entry.result = tryParseJSON(content);
425
- entry.isError = isError;
475
+ if (mcpResult !== undefined && !entry.snapshotResultApplied) {
476
+ entry.result = mcpResult;
477
+ entry.modelContent = [{ type: "text", text: content }];
478
+ entry.snapshotResultApplied = true;
479
+ entry.isError = isError;
480
+ } else if (entry.snapshotResultApplied) {
481
+ if (entry.modelContent === undefined) {
482
+ entry.modelContent = [{ type: "text", text: content }];
483
+ }
484
+ if (entry.isError === undefined) {
485
+ entry.isError = isError;
486
+ }
487
+ } else {
488
+ entry.result = tryParseJSON(content);
489
+ entry.isError = isError;
490
+ }
426
491
  if (toolMessageId) {
427
492
  entry.toolMessageId = toolMessageId;
428
493
  }
@@ -460,9 +525,21 @@ export class RunAggregator {
460
525
  args: (entry.parsedArgs ?? {}) as any,
461
526
  argsText: entry.argsText,
462
527
  ...(entry.result !== undefined ? { result: entry.result } : {}),
528
+ ...(entry.modelContent !== undefined
529
+ ? { modelContent: entry.modelContent }
530
+ : {}),
463
531
  ...(entry.isError !== undefined ? { isError: entry.isError } : {}),
464
532
  ...(entry.mcpAppResourceUri
465
- ? { mcp: { app: { resourceUri: entry.mcpAppResourceUri } } }
533
+ ? {
534
+ mcp: {
535
+ app: {
536
+ resourceUri: entry.mcpAppResourceUri,
537
+ ...(entry.mcpAppServerId
538
+ ? { serverId: entry.mcpAppServerId }
539
+ : {}),
540
+ },
541
+ },
542
+ }
466
543
  : {}),
467
544
  ...(entry.parentMessageId ? { parentId: entry.parentMessageId } : {}),
468
545
  ...(entry.toolMessageId
@@ -1,5 +1,6 @@
1
1
  import type { AgUiEvent, AgUiInterrupt, AgUiRunFinishedOutcome } from "./types";
2
2
  import type { Logger } from "./logger";
3
+ import { parseMcpToolCallResult } from "./mcp-tool-result";
3
4
 
4
5
  export type ParseAgUiEventOptions = {
5
6
  logger?: Logger;
@@ -209,15 +210,17 @@ export const parseAgUiEvent = (
209
210
  case "TOOL_CALL_RESULT": {
210
211
  const toolCallId = getString("toolCallId");
211
212
  if (!toolCallId) return null;
213
+ const content = getString("content") ?? "";
212
214
  return withOptional(
213
215
  {
214
216
  type: "TOOL_CALL_RESULT" as const,
215
217
  toolCallId,
216
- content: getString("content") ?? "",
218
+ content,
217
219
  },
218
220
  {
219
221
  messageId: getString("messageId"),
220
222
  role: payload.role === "tool" ? "tool" : undefined,
223
+ mcpResult: parseMcpToolCallResult(payload, content),
221
224
  },
222
225
  );
223
226
  }
@@ -0,0 +1,26 @@
1
+ import { isRecord } from "@assistant-ui/core/internal";
2
+
3
+ export type McpToolCallResult = Record<string, unknown> & {
4
+ content: unknown[];
5
+ structuredContent?: Record<string, unknown>;
6
+ _meta?: Record<string, unknown>;
7
+ isError?: boolean;
8
+ };
9
+
10
+ export const parseMcpToolCallResult = (
11
+ source: Record<string, unknown>,
12
+ modelContent: string,
13
+ ): McpToolCallResult | undefined => {
14
+ const structuredContent = isRecord(source.structuredContent)
15
+ ? source.structuredContent
16
+ : undefined;
17
+ const meta = isRecord(source._meta) ? source._meta : undefined;
18
+ if (!structuredContent && !meta) return undefined;
19
+
20
+ return {
21
+ content: modelContent ? [{ type: "text", text: modelContent }] : [],
22
+ ...(structuredContent ? { structuredContent } : {}),
23
+ ...(meta ? { _meta: meta } : {}),
24
+ ...(typeof source.isError === "boolean" ? { isError: source.isError } : {}),
25
+ };
26
+ };
@@ -13,6 +13,7 @@ import type {
13
13
  import type { AbstractAgent } from "@ag-ui/client";
14
14
  import type { Logger } from "./logger";
15
15
  import type { ReadonlyJSONValue } from "assistant-stream/utils";
16
+ import type { McpToolCallResult } from "./mcp-tool-result";
16
17
 
17
18
  /**
18
19
  * @experimental This API is still under active development and might change without notice.
@@ -59,6 +60,17 @@ export type UseAgUiRuntimeOptions = ExternalStoreSharedOptions & {
59
60
  agent: AbstractAgent;
60
61
  logger?: Partial<Logger>;
61
62
  showThinking?: boolean;
63
+ /**
64
+ * When the user sends, edits, or reloads a message while client-side tool
65
+ * calls are still pending, automatically cancel the unresolved tool calls
66
+ * with an error result so the agent's tool-call accounting stays
67
+ * consistent. Pending AG-UI interrupts are exempt: they still reject the
68
+ * run and must be answered with `useAgUiSubmitInterruptResponses` or
69
+ * discarded with `useAgUiSteerAway`. When disabled, `useAgUiSteerAway`
70
+ * remains the explicit way to cancel pending tool calls.
71
+ * Defaults to `true`.
72
+ */
73
+ autoCancelPendingToolCalls?: boolean | undefined;
62
74
  onError?: (e: Error) => void;
63
75
  onCancel?: () => void;
64
76
  adapters?: UseAgUiRuntimeAdapters;
@@ -95,6 +107,12 @@ export type AgUiRuntimeExtras = {
95
107
  message: CreateAppendMessage,
96
108
  responses?: readonly AgUiResumeEntry[],
97
109
  ) => Promise<void>;
110
+ state: ReadonlyJSONValue | undefined;
111
+ setState: (
112
+ next:
113
+ | ReadonlyJSONValue
114
+ | ((prev: ReadonlyJSONValue | undefined) => ReadonlyJSONValue),
115
+ ) => void;
98
116
  };
99
117
 
100
118
  export type AgUiRunFinishedOutcome =
@@ -145,6 +163,7 @@ export type AgUiEvent =
145
163
  toolCallId: string;
146
164
  content: string;
147
165
  role?: "tool";
166
+ mcpResult?: McpToolCallResult;
148
167
  }
149
168
  | {
150
169
  type: "ACTIVITY_SNAPSHOT";
@@ -58,6 +58,7 @@ export function useAgUiRuntime(
58
58
  agent: options.agent,
59
59
  logger,
60
60
  showThinking: options.showThinking ?? true,
61
+ autoCancelPendingToolCalls: options.autoCancelPendingToolCalls,
61
62
  ...(options.onError && { onError: options.onError }),
62
63
  ...(options.onCancel && { onCancel: options.onCancel }),
63
64
  ...(historyAdapter && { history: historyAdapter }),
@@ -70,6 +71,7 @@ export function useAgUiRuntime(
70
71
  agent: options.agent,
71
72
  logger,
72
73
  showThinking: options.showThinking ?? true,
74
+ autoCancelPendingToolCalls: options.autoCancelPendingToolCalls,
73
75
  ...(options.onError && { onError: options.onError }),
74
76
  ...(options.onCancel && { onCancel: options.onCancel }),
75
77
  ...(historyAdapter && { history: historyAdapter }),
@@ -95,6 +97,7 @@ export function useAgUiRuntime(
95
97
  ? async () => {
96
98
  await onSwitchToNewThread();
97
99
  core.applyExternalMessages([]);
100
+ core.resetState();
98
101
  }
99
102
  : undefined,
100
103
  onSwitchToThread: onSwitchToThread
@@ -106,6 +109,8 @@ export function useAgUiRuntime(
106
109
  core.applyExternalMessages(result.messages);
107
110
  if (result.state !== undefined) {
108
111
  core.loadExternalState(result.state);
112
+ } else {
113
+ core.resetState();
109
114
  }
110
115
  if (result.unstable_resume) {
111
116
  void core.resumeInFlightRun(result.messages);
@@ -136,7 +141,7 @@ export function useAgUiRuntime(
136
141
  return {
137
142
  ...shared,
138
143
  isLoading: core.isLoading,
139
- messages: core.getMessages(),
144
+ messageRepository: core.getMessageRepository(),
140
145
  state: core.getState(),
141
146
  isRunning: core.isRunning() || hasExecutingTools,
142
147
  extras: agUiExtras.provide({
@@ -145,6 +150,8 @@ export function useAgUiRuntime(
145
150
  submitInterruptResponses: (responses) =>
146
151
  core.submitInterruptResponses(responses),
147
152
  steerAway: (message, responses) => core.steerAway(message, responses),
153
+ state: core.getState(),
154
+ setState: (next) => core.setState(next),
148
155
  }),
149
156
  unstable_enableToolInvocations: true,
150
157
  setToolStatuses,