@assistant-ui/react-ag-ui 0.0.54 → 0.0.57
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 -2
- package/dist/index.js +2 -2
- package/dist/runtime/AgUiThreadRuntimeCore.d.ts +25 -15
- package/dist/runtime/AgUiThreadRuntimeCore.d.ts.map +1 -1
- package/dist/runtime/AgUiThreadRuntimeCore.js +257 -210
- package/dist/runtime/AgUiThreadRuntimeCore.js.map +1 -1
- package/dist/runtime/adapter/conversions.d.ts +13 -3
- package/dist/runtime/adapter/conversions.d.ts.map +1 -1
- package/dist/runtime/adapter/conversions.js +77 -39
- package/dist/runtime/adapter/conversions.js.map +1 -1
- package/dist/runtime/adapter/run-aggregator.d.ts +23 -2
- package/dist/runtime/adapter/run-aggregator.d.ts.map +1 -1
- package/dist/runtime/adapter/run-aggregator.js +155 -41
- 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 +9 -0
- package/dist/runtime/adapter/subscriber.js.map +1 -1
- package/dist/runtime/adapter/tool-approval.d.ts +56 -0
- package/dist/runtime/adapter/tool-approval.d.ts.map +1 -0
- package/dist/runtime/adapter/tool-approval.js +244 -0
- package/dist/runtime/adapter/tool-approval.js.map +1 -0
- package/dist/runtime/event-parser.d.ts.map +1 -1
- package/dist/runtime/event-parser.js +2 -1
- package/dist/runtime/event-parser.js.map +1 -1
- package/dist/runtime/interrupt-internals.d.ts +8 -0
- package/dist/runtime/interrupt-internals.d.ts.map +1 -0
- package/dist/runtime/interrupt-internals.js +25 -0
- package/dist/runtime/interrupt-internals.js.map +1 -0
- package/dist/useAgUiRuntime.js +14 -9
- package/dist/useAgUiRuntime.js.map +1 -1
- package/package.json +9 -9
- package/src/index.ts +8 -2
- package/src/runtime/AgUiThreadRuntimeCore.test.ts +105 -0
- package/src/runtime/AgUiThreadRuntimeCore.ts +436 -272
- package/src/runtime/adapter/conversions.content.test.ts +61 -3
- package/src/runtime/adapter/conversions.ts +123 -57
- package/src/runtime/adapter/run-aggregator.approval.test.ts +103 -0
- package/src/runtime/adapter/run-aggregator.ts +234 -47
- package/src/runtime/adapter/subscriber.ts +14 -0
- package/src/runtime/adapter/tool-approval.test.ts +681 -0
- package/src/runtime/adapter/tool-approval.ts +366 -0
- package/src/runtime/event-parser.ts +10 -1
- package/src/runtime/interrupt-internals.ts +35 -0
- package/src/useAgUiRuntime.approval.test.tsx +885 -0
- package/src/useAgUiRuntime.history.test.tsx +76 -0
- package/src/useAgUiRuntime.ts +15 -9
|
@@ -1,6 +1,15 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
|
|
1
|
+
import { describe, expect, expectTypeOf, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
MessageSchema,
|
|
4
|
+
UserMessageSchema,
|
|
5
|
+
type Message as AgUiWireMessage,
|
|
6
|
+
} from "@ag-ui/client";
|
|
7
|
+
import type { AppendMessage } from "@assistant-ui/core";
|
|
8
|
+
import {
|
|
9
|
+
fromAgUiMessages,
|
|
10
|
+
toAgUiMessages,
|
|
11
|
+
type AgUiMessage,
|
|
12
|
+
} from "./conversions";
|
|
4
13
|
|
|
5
14
|
type Message = Parameters<typeof toAgUiMessages>[0][number];
|
|
6
15
|
|
|
@@ -20,7 +29,56 @@ const contentOf = (message: Message) => {
|
|
|
20
29
|
return (converted as { content: unknown }).content;
|
|
21
30
|
};
|
|
22
31
|
|
|
32
|
+
const appendMessage = (): AppendMessage => ({
|
|
33
|
+
role: "user",
|
|
34
|
+
content: [{ type: "text", text: "Hi" }],
|
|
35
|
+
attachments: [],
|
|
36
|
+
createdAt: new Date(),
|
|
37
|
+
parentId: null,
|
|
38
|
+
sourceId: null,
|
|
39
|
+
runConfig: undefined,
|
|
40
|
+
metadata: { custom: {} },
|
|
41
|
+
});
|
|
42
|
+
|
|
23
43
|
describe("toAgUiMessages content metadata", () => {
|
|
44
|
+
it("emits AgUiMessage values assignable to AG-UI Message", () => {
|
|
45
|
+
expectTypeOf<AgUiMessage>().toExtend<AgUiWireMessage>();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("emits records MessageSchema accepts", () => {
|
|
49
|
+
const converted = toAgUiMessages([
|
|
50
|
+
{ id: "u-1", role: "user", content: "Hi" },
|
|
51
|
+
{ id: "a-1", role: "assistant", content: "Hello" },
|
|
52
|
+
{ id: "s-1", role: "system", content: "Be brief" },
|
|
53
|
+
{ id: "t-1", role: "tool", content: "ok", toolCallId: "c-1" },
|
|
54
|
+
{ id: "r-1", role: "reasoning", content: "hmm" },
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
expect(converted.map((message) => MessageSchema.parse(message))).toEqual(
|
|
58
|
+
converted,
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("normalizes an AppendMessage without an id", () => {
|
|
63
|
+
const converted = toAgUiMessages([appendMessage()])[0];
|
|
64
|
+
if (!converted) throw new Error("expected a converted message");
|
|
65
|
+
|
|
66
|
+
expect(UserMessageSchema.parse(converted)).toMatchObject({
|
|
67
|
+
role: "user",
|
|
68
|
+
content: "Hi",
|
|
69
|
+
});
|
|
70
|
+
expect(converted.id).toEqual(expect.any(String));
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("preserves a caller-supplied id", () => {
|
|
74
|
+
const converted = toAgUiMessages([
|
|
75
|
+
{ ...appendMessage(), id: "stable-user-1" },
|
|
76
|
+
])[0];
|
|
77
|
+
if (!converted) throw new Error("expected a converted message");
|
|
78
|
+
|
|
79
|
+
expect(UserMessageSchema.parse(converted).id).toBe("stable-user-1");
|
|
80
|
+
});
|
|
81
|
+
|
|
24
82
|
it("carries a part's agui provider metadata onto an image item", () => {
|
|
25
83
|
expect(
|
|
26
84
|
contentOf(
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import type { InputContent, RunAgentParameters } from "@ag-ui/client";
|
|
4
|
+
import { generateId } from "@assistant-ui/core";
|
|
4
5
|
import type {
|
|
5
6
|
ThreadMessageLike as CoreThreadMessageLike,
|
|
6
7
|
PartProviderMetadata,
|
|
@@ -9,8 +10,8 @@ import type {
|
|
|
9
10
|
} from "@assistant-ui/core";
|
|
10
11
|
import {
|
|
11
12
|
getAutoStatus,
|
|
12
|
-
httpUrlPattern,
|
|
13
13
|
parseDataUrl,
|
|
14
|
+
resolveFilePartSource,
|
|
14
15
|
} from "@assistant-ui/core/internal";
|
|
15
16
|
import { type Tool, toToolsJSONSchema } from "assistant-stream";
|
|
16
17
|
import type { ReadonlyJSONObject } from "assistant-stream/utils";
|
|
@@ -27,6 +28,7 @@ import {
|
|
|
27
28
|
type A2uiSurfaceState,
|
|
28
29
|
} from "@assistant-ui/react-generative-ui/a2ui";
|
|
29
30
|
import type { AgUiInterrupt } from "../types";
|
|
31
|
+
import { projectAgUiToolApprovals } from "./tool-approval";
|
|
30
32
|
import {
|
|
31
33
|
parseMcpToolCallResult,
|
|
32
34
|
readMcpAppResourceUri,
|
|
@@ -41,7 +43,7 @@ type AttachmentLike = {
|
|
|
41
43
|
};
|
|
42
44
|
|
|
43
45
|
type ThreadMessageLike = {
|
|
44
|
-
id
|
|
46
|
+
id?: string;
|
|
45
47
|
role: string;
|
|
46
48
|
content: unknown;
|
|
47
49
|
metadata?: unknown;
|
|
@@ -51,6 +53,8 @@ type ThreadMessageLike = {
|
|
|
51
53
|
attachments?: readonly AttachmentLike[];
|
|
52
54
|
};
|
|
53
55
|
|
|
56
|
+
type NormalizedThreadMessageLike = ThreadMessageLike & { id: string };
|
|
57
|
+
|
|
54
58
|
type AgUiToolCall = {
|
|
55
59
|
id: string;
|
|
56
60
|
type: "function";
|
|
@@ -60,11 +64,23 @@ type AgUiToolCall = {
|
|
|
60
64
|
export type AgUiMessage =
|
|
61
65
|
| {
|
|
62
66
|
id: string;
|
|
63
|
-
role:
|
|
67
|
+
role: "user";
|
|
64
68
|
content: string | InputContent[];
|
|
65
69
|
name?: string;
|
|
70
|
+
}
|
|
71
|
+
| {
|
|
72
|
+
id: string;
|
|
73
|
+
role: "assistant";
|
|
74
|
+
content: string;
|
|
75
|
+
name?: string;
|
|
66
76
|
toolCalls?: AgUiToolCall[];
|
|
67
77
|
}
|
|
78
|
+
| {
|
|
79
|
+
id: string;
|
|
80
|
+
role: "system" | "developer";
|
|
81
|
+
content: string;
|
|
82
|
+
name?: string;
|
|
83
|
+
}
|
|
68
84
|
| {
|
|
69
85
|
id: string;
|
|
70
86
|
role: "reasoning";
|
|
@@ -162,13 +178,6 @@ function parseJSONText(value: string): unknown {
|
|
|
162
178
|
}
|
|
163
179
|
}
|
|
164
180
|
|
|
165
|
-
function generateId(): string {
|
|
166
|
-
return (
|
|
167
|
-
(globalThis.crypto as { randomUUID?: () => string })?.randomUUID?.() ??
|
|
168
|
-
Math.random().toString(36).slice(2)
|
|
169
|
-
);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
181
|
function normalizeToolCall(part: ToolCallPart): {
|
|
173
182
|
id: string;
|
|
174
183
|
call: AgUiToolCall;
|
|
@@ -227,17 +236,20 @@ function buildInputSource(
|
|
|
227
236
|
declaredMimeType: string | undefined,
|
|
228
237
|
sourceType?: string,
|
|
229
238
|
): InputContentSource {
|
|
230
|
-
|
|
239
|
+
const source = resolveFilePartSource({
|
|
240
|
+
data: value,
|
|
241
|
+
mimeType: declaredMimeType ?? "application/octet-stream",
|
|
242
|
+
sourceType,
|
|
243
|
+
});
|
|
244
|
+
if (source.kind === "url") {
|
|
231
245
|
return declaredMimeType !== undefined
|
|
232
|
-
? { type: "url", value, mimeType: declaredMimeType }
|
|
233
|
-
: { type: "url", value };
|
|
246
|
+
? { type: "url", value: source.url, mimeType: declaredMimeType }
|
|
247
|
+
: { type: "url", value: source.url };
|
|
234
248
|
}
|
|
235
|
-
const parsed = parseDataUrl(value);
|
|
236
249
|
return {
|
|
237
250
|
type: "data",
|
|
238
|
-
value:
|
|
239
|
-
mimeType:
|
|
240
|
-
parsed?.mimeType ?? declaredMimeType ?? "application/octet-stream",
|
|
251
|
+
value: source.data,
|
|
252
|
+
mimeType: source.mimeType,
|
|
241
253
|
};
|
|
242
254
|
}
|
|
243
255
|
|
|
@@ -606,13 +618,27 @@ function toAssistantSnapshotMessage(
|
|
|
606
618
|
rawMessage: Record<string, unknown>,
|
|
607
619
|
): CoreThreadMessageLike {
|
|
608
620
|
const text = extractText(rawMessage.content);
|
|
609
|
-
const
|
|
621
|
+
const interrupts = readPersistedInterrupts(rawMessage.metadata);
|
|
622
|
+
const restoredToolCalls = extractAssistantToolCalls(rawMessage);
|
|
623
|
+
const approvals = projectAgUiToolApprovals(
|
|
624
|
+
interrupts,
|
|
625
|
+
new Set(
|
|
626
|
+
restoredToolCalls
|
|
627
|
+
.map((part) => part.toolCallId)
|
|
628
|
+
.filter((id): id is string => !!id),
|
|
629
|
+
),
|
|
630
|
+
);
|
|
631
|
+
const toolCallParts = restoredToolCalls.map((part) => {
|
|
632
|
+
const approval = part.toolCallId
|
|
633
|
+
? approvals.get(part.toolCallId)
|
|
634
|
+
: undefined;
|
|
635
|
+
return approval ? { ...part, approval } : part;
|
|
636
|
+
});
|
|
610
637
|
const assistantContent = [
|
|
611
638
|
...(text.length > 0 ? [{ type: "text" as const, text }] : []),
|
|
612
639
|
...toolCallParts,
|
|
613
640
|
];
|
|
614
641
|
const messageName = getString(rawMessage, "name");
|
|
615
|
-
const interrupts = readPersistedInterrupts(rawMessage.metadata);
|
|
616
642
|
return {
|
|
617
643
|
id: getString(rawMessage, "id") ?? generateId(),
|
|
618
644
|
role: "assistant",
|
|
@@ -633,7 +659,7 @@ function toAssistantSnapshotMessage(
|
|
|
633
659
|
}
|
|
634
660
|
|
|
635
661
|
function toUserOrSystemSnapshotMessage(
|
|
636
|
-
role: "user" | "system",
|
|
662
|
+
role: "user" | "system" | "developer",
|
|
637
663
|
rawMessage: Record<string, unknown>,
|
|
638
664
|
): CoreThreadMessageLike {
|
|
639
665
|
const messageName = getString(rawMessage, "name");
|
|
@@ -641,10 +667,23 @@ function toUserOrSystemSnapshotMessage(
|
|
|
641
667
|
role === "user" ? toSnapshotAttachments(rawMessage.content) : [];
|
|
642
668
|
return {
|
|
643
669
|
id: getString(rawMessage, "id") ?? generateId(),
|
|
644
|
-
role
|
|
670
|
+
// The internal message model has no developer role; it rides as a system
|
|
671
|
+
// message with its wire role kept in metadata so the export restores it.
|
|
672
|
+
role: role === "developer" ? "system" : role,
|
|
645
673
|
content: extractText(rawMessage.content),
|
|
646
674
|
...(messageName !== undefined ? { name: messageName } : {}),
|
|
647
675
|
...(attachments.length > 0 ? { attachments } : {}),
|
|
676
|
+
...(role === "developer"
|
|
677
|
+
? {
|
|
678
|
+
metadata: {
|
|
679
|
+
custom: {
|
|
680
|
+
[AG_UI_METADATA_NAMESPACE]: {
|
|
681
|
+
role: "developer",
|
|
682
|
+
} satisfies AgUiCustomMetadata,
|
|
683
|
+
},
|
|
684
|
+
},
|
|
685
|
+
}
|
|
686
|
+
: {}),
|
|
648
687
|
};
|
|
649
688
|
}
|
|
650
689
|
|
|
@@ -910,7 +949,7 @@ export function fromAgUiMessages(
|
|
|
910
949
|
continue;
|
|
911
950
|
}
|
|
912
951
|
|
|
913
|
-
if (role === "user" || role === "system") {
|
|
952
|
+
if (role === "user" || role === "system" || role === "developer") {
|
|
914
953
|
converted.push(toUserOrSystemSnapshotMessage(role, rawMessage));
|
|
915
954
|
}
|
|
916
955
|
}
|
|
@@ -960,7 +999,7 @@ export function fromAgUiMessages(
|
|
|
960
999
|
}
|
|
961
1000
|
|
|
962
1001
|
function convertAssistantMessage(
|
|
963
|
-
message:
|
|
1002
|
+
message: NormalizedThreadMessageLike,
|
|
964
1003
|
converted: AgUiMessage[],
|
|
965
1004
|
): void {
|
|
966
1005
|
const content = extractText(message.content);
|
|
@@ -1012,18 +1051,15 @@ function convertAssistantMessage(
|
|
|
1012
1051
|
return;
|
|
1013
1052
|
}
|
|
1014
1053
|
|
|
1015
|
-
|
|
1054
|
+
converted.push({
|
|
1016
1055
|
id: message.id,
|
|
1017
1056
|
role: "assistant",
|
|
1018
1057
|
content,
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
assistantMessage.toolCalls = toolCalls.map((entry) => entry.call);
|
|
1025
|
-
}
|
|
1026
|
-
converted.push(assistantMessage);
|
|
1058
|
+
...(message.name ? { name: message.name } : {}),
|
|
1059
|
+
...(toolCalls.length > 0
|
|
1060
|
+
? { toolCalls: toolCalls.map((entry) => entry.call) }
|
|
1061
|
+
: {}),
|
|
1062
|
+
});
|
|
1027
1063
|
|
|
1028
1064
|
for (const { id: toolCallId, part } of toolCalls) {
|
|
1029
1065
|
if (part.result === undefined) continue;
|
|
@@ -1035,36 +1071,30 @@ function convertAssistantMessage(
|
|
|
1035
1071
|
? part.result
|
|
1036
1072
|
: JSON.stringify(part.result);
|
|
1037
1073
|
|
|
1038
|
-
|
|
1074
|
+
converted.push({
|
|
1039
1075
|
id: part.unstable_toolMessageId ?? `${toolCallId}:tool`,
|
|
1040
1076
|
role: "tool",
|
|
1041
1077
|
content: resultContent,
|
|
1042
1078
|
toolCallId,
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
toolMessage.error = resultContent;
|
|
1046
|
-
}
|
|
1047
|
-
converted.push(toolMessage);
|
|
1079
|
+
...(part.isError ? { error: resultContent } : {}),
|
|
1080
|
+
});
|
|
1048
1081
|
}
|
|
1049
1082
|
}
|
|
1050
1083
|
|
|
1051
1084
|
function convertToolMessage(
|
|
1052
|
-
message:
|
|
1085
|
+
message: NormalizedThreadMessageLike,
|
|
1053
1086
|
converted: AgUiMessage[],
|
|
1054
1087
|
): void {
|
|
1055
1088
|
const content = extractText(message.content);
|
|
1056
1089
|
const toolCallId = message.toolCallId ?? generateId();
|
|
1057
1090
|
|
|
1058
|
-
|
|
1091
|
+
converted.push({
|
|
1059
1092
|
id: message.id,
|
|
1060
1093
|
role: "tool",
|
|
1061
1094
|
content,
|
|
1062
1095
|
toolCallId,
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
toolMessage.error = message.error;
|
|
1066
|
-
}
|
|
1067
|
-
converted.push(toolMessage);
|
|
1096
|
+
...(typeof message.error === "string" ? { error: message.error } : {}),
|
|
1097
|
+
});
|
|
1068
1098
|
}
|
|
1069
1099
|
|
|
1070
1100
|
export function toAgUiMessages(
|
|
@@ -1072,7 +1102,11 @@ export function toAgUiMessages(
|
|
|
1072
1102
|
): AgUiMessage[] {
|
|
1073
1103
|
const converted: AgUiMessage[] = [];
|
|
1074
1104
|
|
|
1075
|
-
for (const
|
|
1105
|
+
for (const rawMessage of messages) {
|
|
1106
|
+
const message: NormalizedThreadMessageLike = {
|
|
1107
|
+
...rawMessage,
|
|
1108
|
+
id: rawMessage.id ?? generateId(),
|
|
1109
|
+
};
|
|
1076
1110
|
const opaqueReasoning = readOpaqueReasoning(message.metadata);
|
|
1077
1111
|
const toOpaqueRecord = (entry: AgUiOpaqueReasoning): AgUiMessage => ({
|
|
1078
1112
|
id: entry.id,
|
|
@@ -1101,18 +1135,50 @@ export function toAgUiMessages(
|
|
|
1101
1135
|
continue;
|
|
1102
1136
|
}
|
|
1103
1137
|
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
message
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
genericMessage.name = message.name;
|
|
1138
|
+
if (message.role === "user") {
|
|
1139
|
+
converted.push({
|
|
1140
|
+
id: message.id,
|
|
1141
|
+
role: "user",
|
|
1142
|
+
content: buildUserContent(message),
|
|
1143
|
+
...(message.name ? { name: message.name } : {}),
|
|
1144
|
+
});
|
|
1145
|
+
flushTrailingOpaqueReasoning();
|
|
1146
|
+
continue;
|
|
1114
1147
|
}
|
|
1115
|
-
|
|
1148
|
+
|
|
1149
|
+
if (message.role === "system" || message.role === "developer") {
|
|
1150
|
+
const custom = isObject(message.metadata)
|
|
1151
|
+
? message.metadata.custom
|
|
1152
|
+
: undefined;
|
|
1153
|
+
const namespaced = isObject(custom)
|
|
1154
|
+
? custom[AG_UI_METADATA_NAMESPACE]
|
|
1155
|
+
: undefined;
|
|
1156
|
+
const wireRole =
|
|
1157
|
+
message.role === "system" &&
|
|
1158
|
+
isObject(namespaced) &&
|
|
1159
|
+
namespaced.role === "developer"
|
|
1160
|
+
? ("developer" as const)
|
|
1161
|
+
: message.role;
|
|
1162
|
+
converted.push({
|
|
1163
|
+
id: message.id,
|
|
1164
|
+
role: wireRole,
|
|
1165
|
+
content: extractText(message.content),
|
|
1166
|
+
...(message.name ? { name: message.name } : {}),
|
|
1167
|
+
});
|
|
1168
|
+
flushTrailingOpaqueReasoning();
|
|
1169
|
+
continue;
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
if (message.role === "reasoning") {
|
|
1173
|
+
converted.push({
|
|
1174
|
+
id: message.id,
|
|
1175
|
+
role: "reasoning",
|
|
1176
|
+
content: extractText(message.content),
|
|
1177
|
+
});
|
|
1178
|
+
flushTrailingOpaqueReasoning();
|
|
1179
|
+
continue;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1116
1182
|
flushTrailingOpaqueReasoning();
|
|
1117
1183
|
}
|
|
1118
1184
|
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import type { ChatModelRunResult } from "@assistant-ui/core";
|
|
3
|
+
import { RunAggregator } from "./run-aggregator";
|
|
4
|
+
import type { AgUiInterrupt } from "../types";
|
|
5
|
+
|
|
6
|
+
const GATE: AgUiInterrupt = {
|
|
7
|
+
id: "int-1",
|
|
8
|
+
reason: "tool_call",
|
|
9
|
+
toolCallId: "tc-1",
|
|
10
|
+
message: "Delete /tmp/a?",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const noopLogger = {
|
|
14
|
+
debug: vi.fn(),
|
|
15
|
+
info: vi.fn(),
|
|
16
|
+
warn: vi.fn(),
|
|
17
|
+
error: vi.fn(),
|
|
18
|
+
} as any;
|
|
19
|
+
|
|
20
|
+
const gatedAggregator = () => {
|
|
21
|
+
const emitted: ChatModelRunResult[] = [];
|
|
22
|
+
const aggregator = new RunAggregator({
|
|
23
|
+
showThinking: false,
|
|
24
|
+
logger: noopLogger,
|
|
25
|
+
emit: (result) => {
|
|
26
|
+
emitted.push(result);
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
aggregator.handle({ type: "RUN_STARTED", runId: "run-1" });
|
|
31
|
+
aggregator.handle({
|
|
32
|
+
type: "TOOL_CALL_START",
|
|
33
|
+
toolCallId: "tc-1",
|
|
34
|
+
toolCallName: "delete_file",
|
|
35
|
+
});
|
|
36
|
+
aggregator.handle({
|
|
37
|
+
type: "TOOL_CALL_ARGS",
|
|
38
|
+
toolCallId: "tc-1",
|
|
39
|
+
delta: '{"path":"/tmp/a"}',
|
|
40
|
+
});
|
|
41
|
+
aggregator.handle({ type: "TOOL_CALL_END", toolCallId: "tc-1" });
|
|
42
|
+
aggregator.handle({
|
|
43
|
+
type: "RUN_FINISHED",
|
|
44
|
+
runId: "run-1",
|
|
45
|
+
outcome: { type: "interrupt", interrupts: [GATE] },
|
|
46
|
+
} as any);
|
|
47
|
+
|
|
48
|
+
return { aggregator, emitted, last: () => emitted[emitted.length - 1]! };
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const approvalOf = (result: ChatModelRunResult) => {
|
|
52
|
+
const part = (result.content ?? []).find((p) => p.type === "tool-call");
|
|
53
|
+
return (part as { approval?: unknown } | undefined)?.approval;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
describe("RunAggregator tool approval projection", () => {
|
|
57
|
+
it("projects the gate while the run awaits the interrupt", () => {
|
|
58
|
+
const { last } = gatedAggregator();
|
|
59
|
+
|
|
60
|
+
expect(last().status).toMatchObject({
|
|
61
|
+
type: "requires-action",
|
|
62
|
+
reason: "interrupt",
|
|
63
|
+
});
|
|
64
|
+
expect(approvalOf(last())).toMatchObject({ id: "int-1" });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The gate cannot be clicked once the run is unresumable, but the interrupts
|
|
69
|
+
* stay on the message: the bespoke hooks read that payload, and dropping it
|
|
70
|
+
* would take a shipped surface away with the projection.
|
|
71
|
+
*/
|
|
72
|
+
it("drops the gate when the run then errors, keeping the interrupts", () => {
|
|
73
|
+
const { aggregator, last } = gatedAggregator();
|
|
74
|
+
|
|
75
|
+
aggregator.handle({ type: "RUN_ERROR", message: "stream dropped" } as any);
|
|
76
|
+
|
|
77
|
+
expect(last().status).toMatchObject({
|
|
78
|
+
type: "incomplete",
|
|
79
|
+
reason: "error",
|
|
80
|
+
});
|
|
81
|
+
expect(approvalOf(last())).toBeUndefined();
|
|
82
|
+
expect(
|
|
83
|
+
(last().metadata?.custom as Record<string, any> | undefined)?.agui
|
|
84
|
+
?.interrupts,
|
|
85
|
+
).toEqual([GATE]);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("drops the gate when the run is then cancelled, keeping the interrupts", () => {
|
|
89
|
+
const { aggregator, last } = gatedAggregator();
|
|
90
|
+
|
|
91
|
+
aggregator.handle({ type: "RUN_CANCELLED" } as any);
|
|
92
|
+
|
|
93
|
+
expect(last().status).toMatchObject({
|
|
94
|
+
type: "incomplete",
|
|
95
|
+
reason: "cancelled",
|
|
96
|
+
});
|
|
97
|
+
expect(approvalOf(last())).toBeUndefined();
|
|
98
|
+
expect(
|
|
99
|
+
(last().metadata?.custom as Record<string, any> | undefined)?.agui
|
|
100
|
+
?.interrupts,
|
|
101
|
+
).toEqual([GATE]);
|
|
102
|
+
});
|
|
103
|
+
});
|