@assistant-ui/react-ag-ui 0.0.64 → 0.0.66
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/runtime/AgUiThreadRuntimeCore.d.ts +19 -3
- package/dist/runtime/AgUiThreadRuntimeCore.d.ts.map +1 -1
- package/dist/runtime/AgUiThreadRuntimeCore.js +193 -39
- package/dist/runtime/AgUiThreadRuntimeCore.js.map +1 -1
- package/dist/runtime/adapter/conversions.d.ts.map +1 -1
- package/dist/runtime/adapter/conversions.js +19 -29
- package/dist/runtime/adapter/conversions.js.map +1 -1
- package/dist/runtime/adapter/run-aggregator.d.ts +4 -16
- package/dist/runtime/adapter/run-aggregator.d.ts.map +1 -1
- package/dist/runtime/adapter/run-aggregator.js +100 -23
- package/dist/runtime/adapter/run-aggregator.js.map +1 -1
- package/dist/runtime/event-parser.d.ts.map +1 -1
- package/dist/runtime/event-parser.js +16 -8
- package/dist/runtime/event-parser.js.map +1 -1
- package/dist/runtime/types.d.ts +30 -5
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/runtime/types.js +8 -0
- package/dist/runtime/types.js.map +1 -0
- package/dist/useAgUiRuntime.d.ts.map +1 -1
- package/dist/useAgUiRuntime.js +80 -33
- package/dist/useAgUiRuntime.js.map +1 -1
- package/package.json +7 -6
- package/src/runtime/AgUiThreadRuntimeCore.test.ts +380 -6
- package/src/runtime/AgUiThreadRuntimeCore.ts +314 -70
- package/src/runtime/adapter/conversions.content.test.ts +41 -0
- package/src/runtime/adapter/conversions.nested-tool-calls.test.ts +107 -0
- package/src/runtime/adapter/conversions.ts +37 -64
- package/src/runtime/adapter/run-aggregator.reasoning.test.ts +143 -0
- package/src/runtime/adapter/run-aggregator.ts +163 -40
- package/src/runtime/event-parser.ts +18 -4
- package/src/runtime/types.ts +43 -11
- package/src/useAgUiRuntime.external-store.test.tsx +51 -0
- package/src/useAgUiRuntime.queue.test.tsx +63 -2
- package/src/useAgUiRuntime.thread-switch.test.tsx +587 -3
- package/src/useAgUiRuntime.ts +106 -44
|
@@ -40,6 +40,47 @@ const appendMessage = (): AppendMessage => ({
|
|
|
40
40
|
metadata: { custom: {} },
|
|
41
41
|
});
|
|
42
42
|
|
|
43
|
+
describe("toAgUiMessages data URLs", () => {
|
|
44
|
+
it.each(["application/pdf", ""])(
|
|
45
|
+
"unwraps a media-less data URL while retaining the adapter's %j MIME fallback",
|
|
46
|
+
(mimeType) => {
|
|
47
|
+
expect(
|
|
48
|
+
contentOf(
|
|
49
|
+
userMessage([
|
|
50
|
+
{ type: "file", data: "data:;base64,SGVsbG8=", mimeType },
|
|
51
|
+
]),
|
|
52
|
+
),
|
|
53
|
+
).toEqual([
|
|
54
|
+
{
|
|
55
|
+
type: "document",
|
|
56
|
+
source: {
|
|
57
|
+
type: "data",
|
|
58
|
+
value: "SGVsbG8=",
|
|
59
|
+
mimeType: mimeType || "application/octet-stream",
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
]);
|
|
63
|
+
},
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
it("sniffs a media-less image data URL", () => {
|
|
67
|
+
expect(
|
|
68
|
+
contentOf(
|
|
69
|
+
userMessage([{ type: "image", image: "data:;base64,iVBORw0KGgo=" }]),
|
|
70
|
+
),
|
|
71
|
+
).toEqual([
|
|
72
|
+
{
|
|
73
|
+
type: "image",
|
|
74
|
+
source: {
|
|
75
|
+
type: "data",
|
|
76
|
+
value: "iVBORw0KGgo=",
|
|
77
|
+
mimeType: "image/png",
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
]);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
43
84
|
describe("toAgUiMessages content metadata", () => {
|
|
44
85
|
it("emits AgUiMessage values assignable to AG-UI Message", () => {
|
|
45
86
|
expectTypeOf<AgUiMessage>().toExtend<AgUiWireMessage>();
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { ThreadMessageLike } from "@assistant-ui/core";
|
|
3
|
+
import { toAgUiMessages } from "./conversions";
|
|
4
|
+
|
|
5
|
+
type Message = ThreadMessageLike;
|
|
6
|
+
type ToolCall = Extract<
|
|
7
|
+
Exclude<Message["content"], string>[number],
|
|
8
|
+
{ type: "tool-call" }
|
|
9
|
+
>;
|
|
10
|
+
|
|
11
|
+
const toolCall = (
|
|
12
|
+
toolCallId: string,
|
|
13
|
+
messages?: readonly Message[],
|
|
14
|
+
): ToolCall =>
|
|
15
|
+
({
|
|
16
|
+
type: "tool-call",
|
|
17
|
+
toolCallId,
|
|
18
|
+
toolName: "test",
|
|
19
|
+
args: {},
|
|
20
|
+
...(messages ? { messages } : {}),
|
|
21
|
+
}) as ToolCall;
|
|
22
|
+
|
|
23
|
+
const assistant = (id: string, content: readonly ToolCall[]): Message =>
|
|
24
|
+
({ id, role: "assistant", content }) as Message;
|
|
25
|
+
|
|
26
|
+
const deepToolCall = (depth: number) => {
|
|
27
|
+
let part = toolCall(String(depth - 1));
|
|
28
|
+
for (let index = depth - 2; index >= 0; index--) {
|
|
29
|
+
part = toolCall(String(index), [assistant(`nested-${index}`, [part])]);
|
|
30
|
+
}
|
|
31
|
+
return part;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
describe("toAgUiMessages nested tool calls", () => {
|
|
35
|
+
it("preserves document order", () => {
|
|
36
|
+
const [converted] = toAgUiMessages([
|
|
37
|
+
assistant("root", [
|
|
38
|
+
toolCall("root-call", [
|
|
39
|
+
assistant("nested", [
|
|
40
|
+
toolCall("first", [assistant("deep", [toolCall("first-child")])]),
|
|
41
|
+
toolCall("second"),
|
|
42
|
+
]),
|
|
43
|
+
]),
|
|
44
|
+
]),
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
expect(converted).toMatchObject({ role: "assistant" });
|
|
48
|
+
if (converted?.role !== "assistant") throw new Error("expected assistant");
|
|
49
|
+
expect(converted.toolCalls?.map(({ id }) => id)).toEqual([
|
|
50
|
+
"root-call",
|
|
51
|
+
"first",
|
|
52
|
+
"first-child",
|
|
53
|
+
"second",
|
|
54
|
+
]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("prunes nested tool calls below filtered a2ui parts", () => {
|
|
58
|
+
const [converted] = toAgUiMessages([
|
|
59
|
+
assistant("root", [
|
|
60
|
+
toolCall("root-call", [
|
|
61
|
+
assistant("nested", [
|
|
62
|
+
toolCall("a2ui:surface", [
|
|
63
|
+
assistant("hidden", [toolCall("hidden")]),
|
|
64
|
+
]),
|
|
65
|
+
toolCall("visible"),
|
|
66
|
+
]),
|
|
67
|
+
]),
|
|
68
|
+
]),
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
expect(converted).toMatchObject({ role: "assistant" });
|
|
72
|
+
if (converted?.role !== "assistant") throw new Error("expected assistant");
|
|
73
|
+
expect(converted.toolCalls?.map(({ id }) => id)).toEqual([
|
|
74
|
+
"root-call",
|
|
75
|
+
"visible",
|
|
76
|
+
]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("converts deeply nested tool calls without overflowing the stack", () => {
|
|
80
|
+
const depth = 10_000;
|
|
81
|
+
|
|
82
|
+
const [converted] = toAgUiMessages([
|
|
83
|
+
assistant("root", [deepToolCall(depth)]),
|
|
84
|
+
]);
|
|
85
|
+
|
|
86
|
+
expect(converted).toMatchObject({ role: "assistant" });
|
|
87
|
+
if (converted?.role !== "assistant") throw new Error("expected assistant");
|
|
88
|
+
expect(converted.toolCalls).toHaveLength(depth);
|
|
89
|
+
expect(converted.toolCalls?.at(-1)?.id).toBe(String(depth - 1));
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("terminates cyclic nested tool-call messages without throwing", () => {
|
|
93
|
+
const messages: Message[] = [];
|
|
94
|
+
messages.push(assistant("nested", [toolCall("nested-call", messages)]));
|
|
95
|
+
|
|
96
|
+
const [converted] = toAgUiMessages([
|
|
97
|
+
assistant("root", [toolCall("root-call", messages)]),
|
|
98
|
+
]);
|
|
99
|
+
|
|
100
|
+
expect(converted).toMatchObject({ role: "assistant" });
|
|
101
|
+
if (converted?.role !== "assistant") throw new Error("expected assistant");
|
|
102
|
+
expect(converted.toolCalls?.map(({ id }) => id)).toEqual([
|
|
103
|
+
"root-call",
|
|
104
|
+
"nested-call",
|
|
105
|
+
]);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -6,14 +6,13 @@ import type {
|
|
|
6
6
|
ThreadMessageLike as CoreThreadMessageLike,
|
|
7
7
|
PartProviderMetadata,
|
|
8
8
|
ReasoningMessagePart,
|
|
9
|
-
ThreadMessage,
|
|
10
|
-
ToolCallMessagePartMcpMetadata,
|
|
11
|
-
ToolModelContentPart,
|
|
12
9
|
} from "@assistant-ui/core";
|
|
13
10
|
import {
|
|
14
11
|
getAutoStatus,
|
|
15
12
|
parseDataUrl,
|
|
16
13
|
resolveFilePartSource,
|
|
14
|
+
resolveImageMediaType,
|
|
15
|
+
walkToolCallTree,
|
|
17
16
|
} from "@assistant-ui/core/internal";
|
|
18
17
|
import { type Tool, toToolsJSONSchema } from "assistant-stream";
|
|
19
18
|
import type { ReadonlyJSONObject } from "assistant-stream/utils";
|
|
@@ -23,14 +22,13 @@ import {
|
|
|
23
22
|
MCP_APPS_ACTIVITY_TYPE,
|
|
24
23
|
type AgUiCustomMetadata,
|
|
25
24
|
type AgUiOpaqueReasoning,
|
|
26
|
-
} from "
|
|
25
|
+
} from "../types";
|
|
27
26
|
import {
|
|
28
27
|
applyA2uiOperations,
|
|
29
|
-
|
|
30
|
-
surfaceToOperations,
|
|
28
|
+
surfaceToPresentToolCall,
|
|
31
29
|
type A2uiState,
|
|
32
30
|
type A2uiSurfaceState,
|
|
33
|
-
} from "@assistant-ui/
|
|
31
|
+
} from "@assistant-ui/generative-ui/a2ui";
|
|
34
32
|
import type { AgUiInterrupt } from "../types";
|
|
35
33
|
import { projectAgUiToolApprovals } from "./tool-approval";
|
|
36
34
|
import {
|
|
@@ -99,29 +97,18 @@ export type AgUiMessage =
|
|
|
99
97
|
error?: string;
|
|
100
98
|
};
|
|
101
99
|
|
|
102
|
-
type
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
100
|
+
type CoreToolCallPart = Extract<
|
|
101
|
+
Exclude<CoreThreadMessageLike["content"], string>[number],
|
|
102
|
+
{ type: "tool-call" }
|
|
103
|
+
>;
|
|
104
|
+
|
|
105
|
+
type ToolCallPart = Omit<CoreToolCallPart, "result" | "isError"> & {
|
|
108
106
|
result?: unknown;
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
modelContent?: readonly ToolModelContentPart[];
|
|
107
|
+
isError?: boolean | undefined;
|
|
108
|
+
/** @deprecated Experimental since 2026-03-08. Not scheduled for removal; the API may change in any release. */
|
|
112
109
|
unstable_toolMessageId?: string;
|
|
113
|
-
mcp?: ToolCallMessagePartMcpMetadata;
|
|
114
|
-
messages?: readonly ThreadMessage[];
|
|
115
|
-
approval?: CoreToolCallPartApproval;
|
|
116
110
|
};
|
|
117
111
|
|
|
118
|
-
type CoreToolCallPartApproval = NonNullable<
|
|
119
|
-
Extract<
|
|
120
|
-
Exclude<CoreThreadMessageLike["content"], string>[number],
|
|
121
|
-
{ type: "tool-call" }
|
|
122
|
-
>["approval"]
|
|
123
|
-
>;
|
|
124
|
-
|
|
125
112
|
const isObject = (value: unknown): value is Record<string, unknown> =>
|
|
126
113
|
typeof value === "object" && value !== null;
|
|
127
114
|
|
|
@@ -215,6 +202,9 @@ function normalizeToolCall(part: ToolCallPart): {
|
|
|
215
202
|
};
|
|
216
203
|
}
|
|
217
204
|
|
|
205
|
+
const isExportableNestedToolCall = (part: { readonly toolCallId?: unknown }) =>
|
|
206
|
+
typeof part.toolCallId === "string" && !part.toolCallId.startsWith("a2ui:");
|
|
207
|
+
|
|
218
208
|
function extractText(content: unknown): string {
|
|
219
209
|
if (typeof content === "string") return content;
|
|
220
210
|
if (!Array.isArray(content)) return "";
|
|
@@ -287,9 +277,16 @@ function toInputContent(
|
|
|
287
277
|
const image = getString(part, "image");
|
|
288
278
|
if (image === undefined) return null;
|
|
289
279
|
const metadata = buildInputMetadata(part, getString(part, "filename"));
|
|
280
|
+
const source = buildInputSource(image, fallbackMimeType);
|
|
290
281
|
return {
|
|
291
282
|
type: "image",
|
|
292
|
-
source:
|
|
283
|
+
source:
|
|
284
|
+
source.type === "data"
|
|
285
|
+
? {
|
|
286
|
+
...source,
|
|
287
|
+
mimeType: resolveImageMediaType(image, fallbackMimeType),
|
|
288
|
+
}
|
|
289
|
+
: source,
|
|
293
290
|
...(metadata && { metadata }),
|
|
294
291
|
};
|
|
295
292
|
}
|
|
@@ -513,7 +510,6 @@ function toToolCallPart(value: unknown): ToolCallPart | null {
|
|
|
513
510
|
: isObject(value.args) && !Array.isArray(value.args)
|
|
514
511
|
? (value.args as ReadonlyJSONObject)
|
|
515
512
|
: undefined;
|
|
516
|
-
|
|
517
513
|
const part: ToolCallPart = {
|
|
518
514
|
type: "tool-call",
|
|
519
515
|
...(toolCallId !== undefined ? { toolCallId } : {}),
|
|
@@ -704,27 +700,17 @@ function toUserOrSystemSnapshotMessage(
|
|
|
704
700
|
};
|
|
705
701
|
}
|
|
706
702
|
|
|
707
|
-
// Rebuilds the a2ui:<surfaceId> "present" tool-call parts for one owning
|
|
708
|
-
// assistant message from a bucket of rebuilt surface state, mirroring the
|
|
709
|
-
// live RunAggregator.synthesizeA2uiToolCalls shape. Non-a2ui parts (text,
|
|
710
|
-
// other tool calls) are preserved; existing a2ui parts are replaced wholesale
|
|
711
|
-
// so create/update/delete within the bucket all converge on the rebuilt set.
|
|
712
703
|
function attachA2uiSurfaces(
|
|
713
704
|
message: CoreThreadMessageLike,
|
|
714
705
|
state: A2uiState,
|
|
715
706
|
): CoreThreadMessageLike {
|
|
716
707
|
const a2uiParts: ToolCallPart[] = [];
|
|
717
708
|
for (const [surfaceId, surface] of state) {
|
|
718
|
-
const {
|
|
719
|
-
if (!
|
|
709
|
+
const { toolCall } = surfaceToPresentToolCall(surfaceId, surface);
|
|
710
|
+
if (!toolCall) continue;
|
|
720
711
|
a2uiParts.push({
|
|
721
712
|
type: "tool-call",
|
|
722
|
-
|
|
723
|
-
toolName: "present",
|
|
724
|
-
args: spec as unknown as ReadonlyJSONObject,
|
|
725
|
-
argsText: JSON.stringify(spec),
|
|
726
|
-
result: {},
|
|
727
|
-
artifact: { a2ui: surfaceToOperations(surface) },
|
|
713
|
+
...toolCall,
|
|
728
714
|
});
|
|
729
715
|
}
|
|
730
716
|
|
|
@@ -1316,7 +1302,16 @@ function convertAssistantMessage(
|
|
|
1316
1302
|
part: ToolCallPart;
|
|
1317
1303
|
}[] = [];
|
|
1318
1304
|
for (const { part } of toolCalls) {
|
|
1319
|
-
|
|
1305
|
+
for (const { part: nestedToolCall } of walkToolCallTree(
|
|
1306
|
+
part.messages ?? [],
|
|
1307
|
+
{ shouldDescend: isExportableNestedToolCall },
|
|
1308
|
+
)) {
|
|
1309
|
+
if (!isExportableNestedToolCall(nestedToolCall)) continue;
|
|
1310
|
+
nestedToolCalls.push({
|
|
1311
|
+
...normalizeToolCall(nestedToolCall),
|
|
1312
|
+
part: nestedToolCall,
|
|
1313
|
+
});
|
|
1314
|
+
}
|
|
1320
1315
|
}
|
|
1321
1316
|
|
|
1322
1317
|
converted.push({
|
|
@@ -1348,28 +1343,6 @@ function convertAssistantMessage(
|
|
|
1348
1343
|
}
|
|
1349
1344
|
}
|
|
1350
1345
|
|
|
1351
|
-
function collectNestedToolCalls(
|
|
1352
|
-
part: ToolCallPart,
|
|
1353
|
-
out: { id: string; call: AgUiToolCall; part: ToolCallPart }[],
|
|
1354
|
-
): void {
|
|
1355
|
-
for (const nested of part.messages ?? []) {
|
|
1356
|
-
if (!isObject(nested) || nested.role !== "assistant") continue;
|
|
1357
|
-
const nestedContent = Array.isArray(nested.content) ? nested.content : [];
|
|
1358
|
-
for (const nestedPart of nestedContent) {
|
|
1359
|
-
if (!isObject(nestedPart) || nestedPart.type !== "tool-call") continue;
|
|
1360
|
-
const nestedToolCall = nestedPart as ToolCallPart;
|
|
1361
|
-
if (
|
|
1362
|
-
typeof nestedToolCall.toolCallId !== "string" ||
|
|
1363
|
-
nestedToolCall.toolCallId.startsWith("a2ui:")
|
|
1364
|
-
) {
|
|
1365
|
-
continue;
|
|
1366
|
-
}
|
|
1367
|
-
out.push({ ...normalizeToolCall(nestedToolCall), part: nestedToolCall });
|
|
1368
|
-
collectNestedToolCalls(nestedToolCall, out);
|
|
1369
|
-
}
|
|
1370
|
-
}
|
|
1371
|
-
}
|
|
1372
|
-
|
|
1373
1346
|
function emitToolResult(
|
|
1374
1347
|
toolCallId: string,
|
|
1375
1348
|
part: ToolCallPart,
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { ChatModelRunResult } from "@assistant-ui/core";
|
|
3
|
+
import { RunAggregator } from "./run-aggregator";
|
|
4
|
+
import type { AgUiCustomMetadata } from "../types";
|
|
5
|
+
import { toAgUiMessages } from "./conversions";
|
|
6
|
+
|
|
7
|
+
describe.each([true, false])(
|
|
8
|
+
"reasoning ownership (showThinking=%s)",
|
|
9
|
+
(showThinking) => {
|
|
10
|
+
it.each([
|
|
11
|
+
{ parentText: "", nextReasoning: false },
|
|
12
|
+
{ parentText: "", nextReasoning: true },
|
|
13
|
+
{ parentText: "Looking it up.", nextReasoning: false },
|
|
14
|
+
{ parentText: "Looking it up.", nextReasoning: true },
|
|
15
|
+
])(
|
|
16
|
+
"keeps pre-tool reasoning on its parent ($parentText, nextReasoning=$nextReasoning)",
|
|
17
|
+
({ parentText, nextReasoning }) => {
|
|
18
|
+
let latest: ChatModelRunResult = {};
|
|
19
|
+
let previous: ChatModelRunResult | undefined;
|
|
20
|
+
const aggregator = new RunAggregator({
|
|
21
|
+
showThinking,
|
|
22
|
+
logger: { debug: () => {}, error: () => {} },
|
|
23
|
+
emit: (update) => {
|
|
24
|
+
latest = update;
|
|
25
|
+
},
|
|
26
|
+
onTextMessageStart: () => {
|
|
27
|
+
previous = latest;
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
const reason = (messageId: string, delta: string) => {
|
|
31
|
+
aggregator.handle({ type: "REASONING_MESSAGE_START", messageId });
|
|
32
|
+
aggregator.handle({
|
|
33
|
+
type: "REASONING_MESSAGE_CONTENT",
|
|
34
|
+
messageId,
|
|
35
|
+
delta,
|
|
36
|
+
});
|
|
37
|
+
aggregator.handle({ type: "REASONING_MESSAGE_END", messageId });
|
|
38
|
+
aggregator.handle({
|
|
39
|
+
type: "REASONING_ENCRYPTED_VALUE",
|
|
40
|
+
subtype: "message",
|
|
41
|
+
entityId: messageId,
|
|
42
|
+
encryptedValue: `sig-${messageId}`,
|
|
43
|
+
});
|
|
44
|
+
};
|
|
45
|
+
const reasoningOf = (result: ChatModelRunResult | undefined) =>
|
|
46
|
+
result?.content?.flatMap((part) =>
|
|
47
|
+
part.type === "reasoning" ? [part.text] : [],
|
|
48
|
+
);
|
|
49
|
+
const opaqueOf = (result: ChatModelRunResult | undefined) =>
|
|
50
|
+
(result?.metadata?.custom?.agui as AgUiCustomMetadata | undefined)
|
|
51
|
+
?.opaqueReasoning ?? [];
|
|
52
|
+
|
|
53
|
+
aggregator.handle({ type: "RUN_STARTED", runId: "run-1" });
|
|
54
|
+
aggregator.handle({
|
|
55
|
+
type: "TEXT_MESSAGE_START",
|
|
56
|
+
messageId: "assistant-1",
|
|
57
|
+
});
|
|
58
|
+
if (parentText)
|
|
59
|
+
aggregator.handle({
|
|
60
|
+
type: "TEXT_MESSAGE_CONTENT",
|
|
61
|
+
messageId: "assistant-1",
|
|
62
|
+
delta: parentText,
|
|
63
|
+
});
|
|
64
|
+
reason("r-1", "plan the call");
|
|
65
|
+
aggregator.handle({
|
|
66
|
+
type: "TOOL_CALL_START",
|
|
67
|
+
toolCallId: "call-1",
|
|
68
|
+
toolCallName: "lookup",
|
|
69
|
+
parentMessageId: "assistant-1",
|
|
70
|
+
});
|
|
71
|
+
aggregator.handle({ type: "TOOL_CALL_END", toolCallId: "call-1" });
|
|
72
|
+
aggregator.handle({
|
|
73
|
+
type: "TOOL_CALL_RESULT",
|
|
74
|
+
toolCallId: "call-1",
|
|
75
|
+
messageId: "tool-1",
|
|
76
|
+
content: "ok",
|
|
77
|
+
});
|
|
78
|
+
if (nextReasoning) reason("r-2", "write the answer");
|
|
79
|
+
aggregator.handle({
|
|
80
|
+
type: "TEXT_MESSAGE_START",
|
|
81
|
+
messageId: "assistant-2",
|
|
82
|
+
});
|
|
83
|
+
aggregator.handle({
|
|
84
|
+
type: "TEXT_MESSAGE_CONTENT",
|
|
85
|
+
messageId: "assistant-2",
|
|
86
|
+
delta: "Done.",
|
|
87
|
+
});
|
|
88
|
+
aggregator.handle({ type: "RUN_FINISHED", runId: "run-1" });
|
|
89
|
+
|
|
90
|
+
expect(
|
|
91
|
+
previous?.content?.filter((part) => part.type === "tool-call"),
|
|
92
|
+
).toHaveLength(1);
|
|
93
|
+
expect(
|
|
94
|
+
latest.content?.filter((part) => part.type === "tool-call"),
|
|
95
|
+
).toHaveLength(0);
|
|
96
|
+
if (showThinking) {
|
|
97
|
+
expect(reasoningOf(previous)).toEqual(["plan the call"]);
|
|
98
|
+
expect(reasoningOf(latest)).toEqual(
|
|
99
|
+
nextReasoning ? ["write the answer"] : [],
|
|
100
|
+
);
|
|
101
|
+
} else {
|
|
102
|
+
expect(opaqueOf(previous)).toEqual([
|
|
103
|
+
{ id: "r-1", encryptedValue: "sig-r-1" },
|
|
104
|
+
]);
|
|
105
|
+
expect(opaqueOf(latest)).toEqual(
|
|
106
|
+
nextReasoning ? [{ id: "r-2", encryptedValue: "sig-r-2" }] : [],
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
const exported = toAgUiMessages([
|
|
110
|
+
{
|
|
111
|
+
id: "assistant-1",
|
|
112
|
+
role: "assistant",
|
|
113
|
+
content: previous?.content ?? [],
|
|
114
|
+
metadata: previous?.metadata,
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
id: "assistant-2",
|
|
118
|
+
role: "assistant",
|
|
119
|
+
content: latest.content ?? [],
|
|
120
|
+
metadata: latest.metadata,
|
|
121
|
+
},
|
|
122
|
+
]);
|
|
123
|
+
expect(exported.map((message) => message.id)).toEqual([
|
|
124
|
+
"r-1",
|
|
125
|
+
"assistant-1",
|
|
126
|
+
"tool-1",
|
|
127
|
+
...(nextReasoning ? ["r-2"] : []),
|
|
128
|
+
"assistant-2",
|
|
129
|
+
]);
|
|
130
|
+
expect(
|
|
131
|
+
exported.flatMap((message) =>
|
|
132
|
+
message.role === "reasoning"
|
|
133
|
+
? [[message.id, message.encryptedValue]]
|
|
134
|
+
: [],
|
|
135
|
+
),
|
|
136
|
+
).toEqual([
|
|
137
|
+
["r-1", "sig-r-1"],
|
|
138
|
+
...(nextReasoning ? [["r-2", "sig-r-2"]] : []),
|
|
139
|
+
]);
|
|
140
|
+
},
|
|
141
|
+
);
|
|
142
|
+
},
|
|
143
|
+
);
|