@assistant-ui/react-ag-ui 0.0.35 → 0.0.38
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.map +1 -1
- package/dist/runtime/AgUiThreadRuntimeCore.js +12 -5
- 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 +158 -29
- 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 +8 -8
- package/src/index.ts +2 -0
- package/src/runtime/AgUiThreadRuntimeCore.ts +11 -9
- package/src/runtime/adapter/conversions.ts +198 -46
- 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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@assistant-ui/react-ag-ui",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.38",
|
|
4
4
|
"description": "AG-UI protocol adapter for assistant-ui",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ag-ui",
|
|
@@ -29,9 +29,9 @@
|
|
|
29
29
|
],
|
|
30
30
|
"sideEffects": false,
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@ag-ui/client": "^0.0.
|
|
33
|
-
"@assistant-ui/core": "^0.2.
|
|
34
|
-
"assistant-stream": "^0.3.
|
|
32
|
+
"@ag-ui/client": "^0.0.55",
|
|
33
|
+
"@assistant-ui/core": "^0.2.14",
|
|
34
|
+
"assistant-stream": "^0.3.21",
|
|
35
35
|
"fast-json-patch": "^3.1.1"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
@@ -44,11 +44,11 @@
|
|
|
44
44
|
}
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
|
-
"@types/react": "^19.2.
|
|
48
|
-
"react": "^19.2.
|
|
49
|
-
"vitest": "^4.1.
|
|
47
|
+
"@types/react": "^19.2.17",
|
|
48
|
+
"react": "^19.2.7",
|
|
49
|
+
"vitest": "^4.1.8",
|
|
50
50
|
"zod": "^4.4.3",
|
|
51
|
-
"@assistant-ui/x-buildutils": "0.0.
|
|
51
|
+
"@assistant-ui/x-buildutils": "0.0.12"
|
|
52
52
|
},
|
|
53
53
|
"publishConfig": {
|
|
54
54
|
"access": "public",
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export { useAgUiRuntime } from "./useAgUiRuntime";
|
|
2
2
|
export type { AgUiAssistantRuntime } from "./useAgUiRuntime";
|
|
3
|
+
export { fromAgUiMessages } from "./runtime/adapter/conversions";
|
|
4
|
+
export type { FromAgUiMessagesOptions } from "./runtime/adapter/conversions";
|
|
3
5
|
export type {
|
|
4
6
|
AgUiInterrupt,
|
|
5
7
|
AgUiInterruptReason,
|
|
@@ -722,6 +722,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
722
722
|
unstable_annotations: [],
|
|
723
723
|
unstable_data: [],
|
|
724
724
|
steps: [],
|
|
725
|
+
isOptimistic: true,
|
|
725
726
|
custom: {},
|
|
726
727
|
},
|
|
727
728
|
};
|
|
@@ -742,9 +743,11 @@ export class AgUiThreadRuntimeCore {
|
|
|
742
743
|
);
|
|
743
744
|
this.messages = this.messages.filter((m) => m.id !== oldId);
|
|
744
745
|
} else {
|
|
745
|
-
this.messages = this.messages.map((m) =>
|
|
746
|
-
m.id
|
|
747
|
-
|
|
746
|
+
this.messages = this.messages.map((m) => {
|
|
747
|
+
if (m.id !== oldId) return m;
|
|
748
|
+
const { isOptimistic: _, ...metadata } = m.metadata;
|
|
749
|
+
return { ...m, id: newId, metadata } as ThreadMessage;
|
|
750
|
+
});
|
|
748
751
|
}
|
|
749
752
|
|
|
750
753
|
const pendingParent = this.assistantHistoryParents.get(oldId);
|
|
@@ -869,6 +872,7 @@ export class AgUiThreadRuntimeCore {
|
|
|
869
872
|
unstable_annotations: annotations,
|
|
870
873
|
unstable_data: data,
|
|
871
874
|
steps,
|
|
875
|
+
...(current.isOptimistic ? { isOptimistic: true } : {}),
|
|
872
876
|
...(incoming.timing ? { timing: incoming.timing } : {}),
|
|
873
877
|
custom: incoming.custom
|
|
874
878
|
? { ...current.custom, ...incoming.custom }
|
|
@@ -911,16 +915,14 @@ export class AgUiThreadRuntimeCore {
|
|
|
911
915
|
|
|
912
916
|
private importMessagesSnapshot(rawMessages: readonly unknown[]) {
|
|
913
917
|
try {
|
|
914
|
-
const normalized = fromAgUiMessages(rawMessages
|
|
918
|
+
const normalized = fromAgUiMessages(rawMessages, {
|
|
919
|
+
showThinking: this.showThinking,
|
|
920
|
+
});
|
|
915
921
|
const converted: ThreadMessage[] = [];
|
|
916
922
|
for (const message of normalized) {
|
|
917
923
|
try {
|
|
918
924
|
converted.push(
|
|
919
|
-
fromThreadMessageLike(
|
|
920
|
-
message as any,
|
|
921
|
-
generateId(),
|
|
922
|
-
FALLBACK_USER_STATUS,
|
|
923
|
-
),
|
|
925
|
+
fromThreadMessageLike(message, generateId(), FALLBACK_USER_STATUS),
|
|
924
926
|
);
|
|
925
927
|
} catch (error) {
|
|
926
928
|
this.logger.error?.(
|
|
@@ -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,57 +179,140 @@ 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
|
-
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
type SnapshotAttachment = NonNullable<
|
|
208
|
+
CoreThreadMessageLike["attachments"]
|
|
209
|
+
>[number];
|
|
210
|
+
|
|
211
|
+
const mediaInputTypes = new Set(["image", "audio", "video", "document"]);
|
|
212
|
+
|
|
213
|
+
// Inverse of buildInputSource.
|
|
214
|
+
function inputSourceToString(
|
|
215
|
+
value: unknown,
|
|
216
|
+
): { value: string; mimeType?: string } | null {
|
|
217
|
+
if (!isObject(value)) return null;
|
|
218
|
+
const sourceValue = getString(value, "value");
|
|
219
|
+
if (sourceValue === undefined) return null;
|
|
220
|
+
const mimeType = getString(value, "mimeType");
|
|
221
|
+
const type = getString(value, "type");
|
|
222
|
+
if (type === "url") {
|
|
223
|
+
return { value: sourceValue, ...(mimeType !== undefined && { mimeType }) };
|
|
224
|
+
}
|
|
225
|
+
if (type === "data") {
|
|
226
|
+
const resolvedMimeType = mimeType ?? "application/octet-stream";
|
|
186
227
|
return {
|
|
187
|
-
|
|
188
|
-
mimeType:
|
|
189
|
-
data: parsed?.data ?? data,
|
|
190
|
-
...(filename !== undefined ? { filename } : {}),
|
|
228
|
+
value: `data:${resolvedMimeType};base64,${sourceValue}`,
|
|
229
|
+
mimeType: resolvedMimeType,
|
|
191
230
|
};
|
|
192
231
|
}
|
|
193
|
-
|
|
194
232
|
return null;
|
|
195
233
|
}
|
|
196
234
|
|
|
235
|
+
// Mirrors @ag-ui/client's BackwardCompatibility_0_0_47 middleware.
|
|
236
|
+
function upgradeBinaryInputPart(
|
|
237
|
+
part: Record<string, unknown>,
|
|
238
|
+
): Record<string, unknown> | null {
|
|
239
|
+
const mimeType = getString(part, "mimeType");
|
|
240
|
+
if (mimeType === undefined) return null;
|
|
241
|
+
const data = getString(part, "data");
|
|
242
|
+
const url = getString(part, "url");
|
|
243
|
+
const source = data
|
|
244
|
+
? { type: "data", value: data, mimeType }
|
|
245
|
+
: url
|
|
246
|
+
? { type: "url", value: url, mimeType }
|
|
247
|
+
: null;
|
|
248
|
+
if (!source) return null;
|
|
249
|
+
const filename = getString(part, "filename");
|
|
250
|
+
return {
|
|
251
|
+
type: mediaTypeForMime(mimeType),
|
|
252
|
+
source,
|
|
253
|
+
...(filename && { metadata: { filename } }),
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function toSnapshotAttachments(content: unknown): SnapshotAttachment[] {
|
|
258
|
+
if (!Array.isArray(content)) return [];
|
|
259
|
+
|
|
260
|
+
const attachments: SnapshotAttachment[] = [];
|
|
261
|
+
for (const rawPart of content) {
|
|
262
|
+
if (!isObject(rawPart)) continue;
|
|
263
|
+
const part =
|
|
264
|
+
getString(rawPart, "type") === "binary"
|
|
265
|
+
? upgradeBinaryInputPart(rawPart)
|
|
266
|
+
: rawPart;
|
|
267
|
+
if (!part) continue;
|
|
268
|
+
const type = getString(part, "type");
|
|
269
|
+
if (type === undefined || !mediaInputTypes.has(type)) continue;
|
|
270
|
+
const source = inputSourceToString(part.source);
|
|
271
|
+
if (!source) continue;
|
|
272
|
+
|
|
273
|
+
const filename = isObject(part.metadata)
|
|
274
|
+
? getString(part.metadata, "filename")
|
|
275
|
+
: undefined;
|
|
276
|
+
const id = attachments.length.toString();
|
|
277
|
+
|
|
278
|
+
if (type === "image") {
|
|
279
|
+
attachments.push({
|
|
280
|
+
id,
|
|
281
|
+
type: "image",
|
|
282
|
+
name: filename ?? "image",
|
|
283
|
+
...(source.mimeType !== undefined && { contentType: source.mimeType }),
|
|
284
|
+
status: { type: "complete" },
|
|
285
|
+
content: [
|
|
286
|
+
{
|
|
287
|
+
type: "image",
|
|
288
|
+
image: source.value,
|
|
289
|
+
...(filename !== undefined && { filename }),
|
|
290
|
+
},
|
|
291
|
+
],
|
|
292
|
+
});
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const mimeType = source.mimeType ?? "application/octet-stream";
|
|
297
|
+
attachments.push({
|
|
298
|
+
id,
|
|
299
|
+
type: type === "document" ? "document" : "file",
|
|
300
|
+
name: filename ?? "file",
|
|
301
|
+
contentType: mimeType,
|
|
302
|
+
status: { type: "complete" },
|
|
303
|
+
content: [
|
|
304
|
+
{
|
|
305
|
+
type: "file",
|
|
306
|
+
data: source.value,
|
|
307
|
+
mimeType,
|
|
308
|
+
...(filename !== undefined && { filename }),
|
|
309
|
+
},
|
|
310
|
+
],
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
return attachments;
|
|
314
|
+
}
|
|
315
|
+
|
|
197
316
|
function buildUserContent(message: ThreadMessageLike): string | InputContent[] {
|
|
198
317
|
// File parts in message.content are intentionally skipped: the canonical
|
|
199
318
|
// binary payload for files always flows through message.attachments.
|
|
@@ -260,9 +379,9 @@ function toToolCallPart(value: unknown): ToolCallPart | null {
|
|
|
260
379
|
typeof argsText === "string" ? parseJSONText(argsText) : undefined;
|
|
261
380
|
const args =
|
|
262
381
|
isObject(parsedArgs) && !Array.isArray(parsedArgs)
|
|
263
|
-
? (parsedArgs as
|
|
382
|
+
? (parsedArgs as ReadonlyJSONObject)
|
|
264
383
|
: isObject(value.args) && !Array.isArray(value.args)
|
|
265
|
-
? (value.args as
|
|
384
|
+
? (value.args as ReadonlyJSONObject)
|
|
266
385
|
: undefined;
|
|
267
386
|
|
|
268
387
|
const part: ToolCallPart = {
|
|
@@ -322,7 +441,7 @@ function extractAssistantToolCalls(
|
|
|
322
441
|
|
|
323
442
|
function toAssistantSnapshotMessage(
|
|
324
443
|
rawMessage: Record<string, unknown>,
|
|
325
|
-
):
|
|
444
|
+
): CoreThreadMessageLike {
|
|
326
445
|
const text = extractText(rawMessage.content);
|
|
327
446
|
const toolCallParts = extractAssistantToolCalls(rawMessage);
|
|
328
447
|
const assistantContent = [
|
|
@@ -341,20 +460,33 @@ function toAssistantSnapshotMessage(
|
|
|
341
460
|
function toUserOrSystemSnapshotMessage(
|
|
342
461
|
role: "user" | "system",
|
|
343
462
|
rawMessage: Record<string, unknown>,
|
|
344
|
-
):
|
|
463
|
+
): CoreThreadMessageLike {
|
|
345
464
|
const messageName = getString(rawMessage, "name");
|
|
465
|
+
const attachments =
|
|
466
|
+
role === "user" ? toSnapshotAttachments(rawMessage.content) : [];
|
|
346
467
|
return {
|
|
347
468
|
id: getString(rawMessage, "id") ?? generateId(),
|
|
348
469
|
role,
|
|
349
470
|
content: extractText(rawMessage.content),
|
|
350
471
|
...(messageName !== undefined ? { name: messageName } : {}),
|
|
472
|
+
...(attachments.length > 0 ? { attachments } : {}),
|
|
351
473
|
};
|
|
352
474
|
}
|
|
353
475
|
|
|
476
|
+
export type FromAgUiMessagesOptions = {
|
|
477
|
+
/**
|
|
478
|
+
* Whether to convert `reasoning` messages into visible reasoning parts.
|
|
479
|
+
* Matches the `showThinking` option of `useAgUiRuntime`. Defaults to `true`.
|
|
480
|
+
*/
|
|
481
|
+
showThinking?: boolean;
|
|
482
|
+
};
|
|
483
|
+
|
|
354
484
|
export function fromAgUiMessages(
|
|
355
485
|
messages: readonly unknown[],
|
|
356
|
-
|
|
357
|
-
|
|
486
|
+
options?: FromAgUiMessagesOptions,
|
|
487
|
+
): CoreThreadMessageLike[] {
|
|
488
|
+
const showThinking = options?.showThinking ?? true;
|
|
489
|
+
const converted: CoreThreadMessageLike[] = [];
|
|
358
490
|
|
|
359
491
|
for (const rawMessage of messages) {
|
|
360
492
|
if (!isObject(rawMessage)) continue;
|
|
@@ -454,6 +586,20 @@ export function fromAgUiMessages(
|
|
|
454
586
|
continue;
|
|
455
587
|
}
|
|
456
588
|
|
|
589
|
+
if (role === "reasoning") {
|
|
590
|
+
// Gate on showThinking so a cold reload matches the live run: the
|
|
591
|
+
// aggregator never stores reasoning parts when showThinking is false.
|
|
592
|
+
if (!showThinking) continue;
|
|
593
|
+
const text = extractText(rawMessage.content);
|
|
594
|
+
if (text.trim().length === 0) continue;
|
|
595
|
+
converted.push({
|
|
596
|
+
id: getString(rawMessage, "id") ?? generateId(),
|
|
597
|
+
role: "assistant",
|
|
598
|
+
content: [{ type: "reasoning", text }],
|
|
599
|
+
});
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
|
|
457
603
|
if (role === "user" || role === "system") {
|
|
458
604
|
converted.push(toUserOrSystemSnapshotMessage(role, rawMessage));
|
|
459
605
|
}
|
|
@@ -478,6 +624,12 @@ function convertAssistantMessage(
|
|
|
478
624
|
part,
|
|
479
625
|
}));
|
|
480
626
|
|
|
627
|
+
// Drop assistant messages with no text or tool calls (e.g. an imported
|
|
628
|
+
// reasoning-only entry) so they are not re-sent as a blank assistant turn.
|
|
629
|
+
if (content.length === 0 && toolCalls.length === 0) {
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
|
|
481
633
|
const assistantMessage: AgUiMessage = {
|
|
482
634
|
id: message.id,
|
|
483
635
|
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 }
|