@assistant-ui/react-ag-ui 0.0.34 → 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.
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/runtime/AgUiThreadRuntimeCore.d.ts +8 -2
- package/dist/runtime/AgUiThreadRuntimeCore.d.ts.map +1 -1
- package/dist/runtime/AgUiThreadRuntimeCore.js +158 -52
- package/dist/runtime/AgUiThreadRuntimeCore.js.map +1 -1
- package/dist/runtime/adapter/conversions.d.ts +12 -4
- package/dist/runtime/adapter/conversions.d.ts.map +1 -1
- package/dist/runtime/adapter/conversions.js +65 -30
- package/dist/runtime/adapter/conversions.js.map +1 -1
- package/dist/runtime/adapter/run-aggregator.d.ts +1 -0
- package/dist/runtime/adapter/run-aggregator.d.ts.map +1 -1
- package/dist/runtime/adapter/run-aggregator.js +17 -0
- package/dist/runtime/adapter/run-aggregator.js.map +1 -1
- package/dist/runtime/adapter/subscriber.d.ts +3 -0
- package/dist/runtime/adapter/subscriber.d.ts.map +1 -1
- package/dist/runtime/adapter/subscriber.js +1 -0
- package/dist/runtime/adapter/subscriber.js.map +1 -1
- package/dist/runtime/event-parser.js +9 -0
- package/dist/runtime/event-parser.js.map +1 -1
- package/dist/runtime/types.d.ts +4 -0
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/useAgUiRuntime.js.map +1 -1
- package/package.json +9 -8
- package/src/index.ts +2 -0
- package/src/runtime/AgUiThreadRuntimeCore.ts +252 -87
- package/src/runtime/adapter/conversions.ts +87 -47
- package/src/runtime/adapter/run-aggregator.ts +30 -5
- package/src/runtime/adapter/subscriber.ts +3 -0
- package/src/runtime/event-parser.ts +9 -0
- package/src/runtime/types.ts +5 -0
|
@@ -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
|
|
51
|
+
toolName: string;
|
|
50
52
|
argsText?: string;
|
|
51
|
-
args?:
|
|
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
|
-
|
|
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
|
|
188
|
+
const declaredMimeType = getString(part, "mimeType") || fallbackMimeType;
|
|
173
189
|
const filename = getString(part, "filename");
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
...(
|
|
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
|
|
273
|
+
? (parsedArgs as ReadonlyJSONObject)
|
|
264
274
|
: isObject(value.args) && !Array.isArray(value.args)
|
|
265
|
-
? (value.args as
|
|
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
|
-
):
|
|
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
|
-
):
|
|
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
|
-
|
|
357
|
-
|
|
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
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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 },
|
package/src/runtime/types.ts
CHANGED
|
@@ -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 }
|