@assistant-ui/react-ag-ui 0.0.35 → 0.0.36

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.
@@ -1,7 +1,9 @@
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";
4
5
  import { type Tool, toToolsJSONSchema } from "assistant-stream";
6
+ import type { ReadonlyJSONObject } from "assistant-stream/utils";
5
7
 
6
8
  export type { InputContent };
7
9
 
@@ -46,9 +48,9 @@ export type AgUiMessage =
46
48
  type ToolCallPart = {
47
49
  type: "tool-call";
48
50
  toolCallId?: string;
49
- toolName?: string;
51
+ toolName: string;
50
52
  argsText?: string;
51
- args?: Record<string, unknown>;
53
+ args?: ReadonlyJSONObject;
52
54
  result?: unknown;
53
55
  isError?: boolean;
54
56
  unstable_toolMessageId?: string;
@@ -127,6 +129,40 @@ function parseDataUrl(
127
129
 
128
130
  const httpUrlPattern = /^https?:\/\//i;
129
131
 
132
+ type InputContentSource =
133
+ | { type: "data"; value: string; mimeType: string }
134
+ | { type: "url"; value: string; mimeType?: string };
135
+
136
+ type MediaInputType = "image" | "audio" | "video" | "document";
137
+
138
+ function mediaTypeForMime(mimeType: string | undefined): MediaInputType {
139
+ if (mimeType?.startsWith("image/")) return "image";
140
+ if (mimeType?.startsWith("audio/")) return "audio";
141
+ if (mimeType?.startsWith("video/")) return "video";
142
+ return "document";
143
+ }
144
+
145
+ // Build an AG-UI multimodal source from a data URL, raw base64 payload, or an
146
+ // http(s) URL. A `url` source may omit the mime type; a `data` source always
147
+ // resolves one (falling back to application/octet-stream).
148
+ function buildInputSource(
149
+ value: string,
150
+ declaredMimeType: string | undefined,
151
+ ): InputContentSource {
152
+ if (httpUrlPattern.test(value)) {
153
+ return declaredMimeType !== undefined
154
+ ? { type: "url", value, mimeType: declaredMimeType }
155
+ : { type: "url", value };
156
+ }
157
+ const parsed = parseDataUrl(value);
158
+ return {
159
+ type: "data",
160
+ value: parsed?.data ?? value,
161
+ mimeType:
162
+ parsed?.mimeType ?? declaredMimeType ?? "application/octet-stream",
163
+ };
164
+ }
165
+
130
166
  function toInputContent(
131
167
  part: unknown,
132
168
  fallbackMimeType: string | undefined,
@@ -143,52 +179,26 @@ function toInputContent(
143
179
  if (type === "image") {
144
180
  const image = getString(part, "image");
145
181
  if (image === undefined) return null;
146
- const parsed = parseDataUrl(image);
147
- if (parsed) {
148
- return {
149
- type: "image",
150
- source: {
151
- type: "data",
152
- value: parsed.data,
153
- mimeType: parsed.mimeType,
154
- },
155
- };
156
- }
157
- return {
158
- type: "image",
159
- source: {
160
- type: "url",
161
- value: image,
162
- ...(fallbackMimeType !== undefined
163
- ? { mimeType: fallbackMimeType }
164
- : {}),
165
- },
166
- };
182
+ return { type: "image", source: buildInputSource(image, fallbackMimeType) };
167
183
  }
168
184
 
169
185
  if (type === "file") {
170
186
  const data = getString(part, "data");
171
187
  if (data === undefined) return null;
172
- const partMimeType = getString(part, "mimeType");
188
+ const declaredMimeType = getString(part, "mimeType") || fallbackMimeType;
173
189
  const filename = getString(part, "filename");
174
- const mimeType =
175
- partMimeType || fallbackMimeType || "application/octet-stream";
176
-
177
- if (httpUrlPattern.test(data)) {
178
- return {
179
- type: "binary",
180
- mimeType,
181
- url: data,
182
- ...(filename !== undefined ? { filename } : {}),
183
- };
190
+ const source = buildInputSource(data, declaredMimeType);
191
+ const metadata = filename !== undefined ? { filename } : undefined;
192
+ switch (mediaTypeForMime(source.mimeType)) {
193
+ case "image":
194
+ return { type: "image", source, ...(metadata && { metadata }) };
195
+ case "audio":
196
+ return { type: "audio", source, ...(metadata && { metadata }) };
197
+ case "video":
198
+ return { type: "video", source, ...(metadata && { metadata }) };
199
+ default:
200
+ return { type: "document", source, ...(metadata && { metadata }) };
184
201
  }
185
- const parsed = parseDataUrl(data);
186
- return {
187
- type: "binary",
188
- mimeType: parsed?.mimeType ?? mimeType,
189
- data: parsed?.data ?? data,
190
- ...(filename !== undefined ? { filename } : {}),
191
- };
192
202
  }
193
203
 
194
204
  return null;
@@ -260,9 +270,9 @@ function toToolCallPart(value: unknown): ToolCallPart | null {
260
270
  typeof argsText === "string" ? parseJSONText(argsText) : undefined;
261
271
  const args =
262
272
  isObject(parsedArgs) && !Array.isArray(parsedArgs)
263
- ? (parsedArgs as Record<string, unknown>)
273
+ ? (parsedArgs as ReadonlyJSONObject)
264
274
  : isObject(value.args) && !Array.isArray(value.args)
265
- ? (value.args as Record<string, unknown>)
275
+ ? (value.args as ReadonlyJSONObject)
266
276
  : undefined;
267
277
 
268
278
  const part: ToolCallPart = {
@@ -322,7 +332,7 @@ function extractAssistantToolCalls(
322
332
 
323
333
  function toAssistantSnapshotMessage(
324
334
  rawMessage: Record<string, unknown>,
325
- ): ThreadMessageLike {
335
+ ): CoreThreadMessageLike {
326
336
  const text = extractText(rawMessage.content);
327
337
  const toolCallParts = extractAssistantToolCalls(rawMessage);
328
338
  const assistantContent = [
@@ -341,7 +351,7 @@ function toAssistantSnapshotMessage(
341
351
  function toUserOrSystemSnapshotMessage(
342
352
  role: "user" | "system",
343
353
  rawMessage: Record<string, unknown>,
344
- ): ThreadMessageLike {
354
+ ): CoreThreadMessageLike {
345
355
  const messageName = getString(rawMessage, "name");
346
356
  return {
347
357
  id: getString(rawMessage, "id") ?? generateId(),
@@ -351,10 +361,20 @@ function toUserOrSystemSnapshotMessage(
351
361
  };
352
362
  }
353
363
 
364
+ export type FromAgUiMessagesOptions = {
365
+ /**
366
+ * Whether to convert `reasoning` messages into visible reasoning parts.
367
+ * Matches the `showThinking` option of `useAgUiRuntime`. Defaults to `true`.
368
+ */
369
+ showThinking?: boolean;
370
+ };
371
+
354
372
  export function fromAgUiMessages(
355
373
  messages: readonly unknown[],
356
- ): ThreadMessageLike[] {
357
- const converted: ThreadMessageLike[] = [];
374
+ options?: FromAgUiMessagesOptions,
375
+ ): CoreThreadMessageLike[] {
376
+ const showThinking = options?.showThinking ?? true;
377
+ const converted: CoreThreadMessageLike[] = [];
358
378
 
359
379
  for (const rawMessage of messages) {
360
380
  if (!isObject(rawMessage)) continue;
@@ -454,6 +474,20 @@ export function fromAgUiMessages(
454
474
  continue;
455
475
  }
456
476
 
477
+ if (role === "reasoning") {
478
+ // Gate on showThinking so a cold reload matches the live run: the
479
+ // aggregator never stores reasoning parts when showThinking is false.
480
+ if (!showThinking) continue;
481
+ const text = extractText(rawMessage.content);
482
+ if (text.trim().length === 0) continue;
483
+ converted.push({
484
+ id: getString(rawMessage, "id") ?? generateId(),
485
+ role: "assistant",
486
+ content: [{ type: "reasoning", text }],
487
+ });
488
+ continue;
489
+ }
490
+
457
491
  if (role === "user" || role === "system") {
458
492
  converted.push(toUserOrSystemSnapshotMessage(role, rawMessage));
459
493
  }
@@ -478,6 +512,12 @@ function convertAssistantMessage(
478
512
  part,
479
513
  }));
480
514
 
515
+ // Drop assistant messages with no text or tool calls (e.g. an imported
516
+ // reasoning-only entry) so they are not re-sent as a blank assistant turn.
517
+ if (content.length === 0 && toolCalls.length === 0) {
518
+ return;
519
+ }
520
+
481
521
  const assistantMessage: AgUiMessage = {
482
522
  id: message.id,
483
523
  role: "assistant",
@@ -1,10 +1,11 @@
1
1
  "use client";
2
2
 
3
- import type {
4
- ChatModelRunResult,
5
- MessageTiming,
6
- ThreadAssistantMessagePart,
7
- ToolCallMessagePart,
3
+ import {
4
+ isMcpAppUri,
5
+ type ChatModelRunResult,
6
+ type MessageTiming,
7
+ type ThreadAssistantMessagePart,
8
+ type ToolCallMessagePart,
8
9
  } from "@assistant-ui/core";
9
10
  import type { AgUiEvent, AgUiInterrupt } from "../types";
10
11
  import type { Logger } from "../logger";
@@ -26,8 +27,11 @@ type ToolCallState = {
26
27
  isError: boolean | undefined;
27
28
  parentMessageId?: string;
28
29
  toolMessageId?: string;
30
+ mcpAppResourceUri?: string;
29
31
  };
30
32
 
33
+ const MCP_APPS_ACTIVITY_TYPE = "mcp-apps";
34
+
31
35
  export type RunAggregatorOptions = {
32
36
  showThinking: boolean;
33
37
  logger: Logger;
@@ -58,6 +62,7 @@ export class RunAggregator {
58
62
  private activeReasoningKey: string | undefined;
59
63
  private reasoningPartCounter = 0;
60
64
  private readonly toolCalls = new Map<string, ToolCallState>();
65
+ private lastResolvedToolCallId: string | undefined;
61
66
  private readonly partOrder: (
62
67
  | { kind: "text"; key: string }
63
68
  | { kind: "reasoning"; key: string }
@@ -85,6 +90,7 @@ export class RunAggregator {
85
90
  this.activeReasoningKey = undefined;
86
91
  this.reasoningPartCounter = 0;
87
92
  this.toolCalls.clear();
93
+ this.lastResolvedToolCallId = undefined;
88
94
  this.partOrder.length = 0;
89
95
  this.textPartCounter = 0;
90
96
  this.activeTextMessageId = undefined;
@@ -224,6 +230,21 @@ export class RunAggregator {
224
230
  this.emit();
225
231
  break;
226
232
  }
233
+ case "ACTIVITY_SNAPSHOT": {
234
+ if (event.activityType !== MCP_APPS_ACTIVITY_TYPE) break;
235
+ const id = this.lastResolvedToolCallId;
236
+ const entry = id ? this.toolCalls.get(id) : undefined;
237
+ const resourceUri = event.content.resourceUri;
238
+ if (
239
+ entry &&
240
+ typeof resourceUri === "string" &&
241
+ isMcpAppUri(resourceUri)
242
+ ) {
243
+ entry.mcpAppResourceUri = resourceUri;
244
+ this.emit();
245
+ }
246
+ break;
247
+ }
227
248
 
228
249
  default: {
229
250
  this.logger.debug?.("[agui] aggregator ignored event", event);
@@ -372,6 +393,7 @@ export class RunAggregator {
372
393
  if (toolMessageId) {
373
394
  entry.toolMessageId = toolMessageId;
374
395
  }
396
+ this.lastResolvedToolCallId = id;
375
397
  }
376
398
 
377
399
  private tryParseJSON(value: string): unknown {
@@ -415,6 +437,9 @@ export class RunAggregator {
415
437
  argsText: entry.argsText,
416
438
  ...(entry.result !== undefined ? { result: entry.result } : {}),
417
439
  ...(entry.isError !== undefined ? { isError: entry.isError } : {}),
440
+ ...(entry.mcpAppResourceUri
441
+ ? { mcp: { app: { resourceUri: entry.mcpAppResourceUri } } }
442
+ : {}),
418
443
  ...(entry.parentMessageId ? { parentId: entry.parentMessageId } : {}),
419
444
  ...(entry.toolMessageId
420
445
  ? { unstable_toolMessageId: entry.toolMessageId }
@@ -27,6 +27,7 @@ type Subscriber = {
27
27
  onToolCallEndEvent?: (payload: { event: unknown }) => void;
28
28
  onToolCallChunkEvent?: (payload: { event: unknown }) => void;
29
29
  onToolCallResultEvent?: (payload: { event: unknown }) => void;
30
+ onActivitySnapshotEvent?: (payload: { event: unknown }) => void;
30
31
  onStateSnapshotEvent?: (payload: { event: unknown }) => void;
31
32
  onStateDeltaEvent?: (payload: { event: unknown }) => void;
32
33
  onMessagesSnapshotEvent?: (payload: { event: unknown }) => void;
@@ -122,6 +123,8 @@ export const createAgUiSubscriber = (
122
123
  dispatchIfValid(event, "TOOL_CALL_CHUNK"),
123
124
  onToolCallResultEvent: ({ event }) =>
124
125
  dispatchIfValid(event, "TOOL_CALL_RESULT"),
126
+ onActivitySnapshotEvent: ({ event }) =>
127
+ dispatchIfValid(event, "ACTIVITY_SNAPSHOT"),
125
128
  onStateSnapshotEvent: ({ event }) =>
126
129
  dispatchIfValid(event, "STATE_SNAPSHOT"),
127
130
  onStateDeltaEvent: ({ event }) => dispatchIfValid(event, "STATE_DELTA"),
@@ -235,6 +235,15 @@ export const parseAgUiEvent = (
235
235
  ? (payload.messages as any[])
236
236
  : [],
237
237
  };
238
+ case "ACTIVITY_SNAPSHOT": {
239
+ const activityType = getString("activityType");
240
+ if (!activityType || !isPlainObject(payload.content)) return null;
241
+ return {
242
+ type: "ACTIVITY_SNAPSHOT" as const,
243
+ activityType,
244
+ content: payload.content,
245
+ };
246
+ }
238
247
  case "RAW":
239
248
  return withOptional(
240
249
  { type: "RAW" as const, event: payload.event },
@@ -121,6 +121,11 @@ export type AgUiEvent =
121
121
  content: string;
122
122
  role?: "tool";
123
123
  }
124
+ | {
125
+ type: "ACTIVITY_SNAPSHOT";
126
+ activityType: string;
127
+ content: Record<string, unknown>;
128
+ }
124
129
  | { type: "RAW"; event: any; source?: string }
125
130
  | { type: "CUSTOM"; name: string; value: any }
126
131
  | { type: "STATE_SNAPSHOT"; snapshot: any }