@assistant-ui/eve 0.0.23 → 0.0.25
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/convertEveMessages.d.ts +40 -51
- package/dist/convertEveMessages.d.ts.map +1 -1
- package/dist/convertEveMessages.js +151 -122
- package/dist/convertEveMessages.js.map +1 -1
- package/dist/deriveCreatedAt.d.ts +9 -9
- package/dist/deriveCreatedAt.d.ts.map +1 -1
- package/dist/deriveCreatedAt.js +25 -34
- package/dist/deriveCreatedAt.js.map +1 -1
- package/dist/eveCloudSessions.js +1 -2
- package/dist/eveExtras.d.ts +4 -2
- package/dist/eveExtras.d.ts.map +1 -1
- package/dist/eveExtras.js +1 -2
- package/dist/eveExtras.js.map +1 -1
- package/dist/hooks.d.ts +4 -3
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +5 -5
- package/dist/hooks.js.map +1 -1
- package/dist/sdkIdentity.js +2 -3
- package/dist/useEveAgentRuntime.d.ts +3 -5
- package/dist/useEveAgentRuntime.d.ts.map +1 -1
- package/dist/useEveAgentRuntime.js +34 -23
- package/dist/useEveAgentRuntime.js.map +1 -1
- package/package.json +10 -10
- package/src/convertEveMessages.test.ts +940 -775
- package/src/convertEveMessages.ts +290 -233
- package/src/deriveCreatedAt.test.ts +90 -88
- package/src/deriveCreatedAt.ts +32 -36
- package/src/eveExtras.ts +4 -4
- package/src/hooks.ts +4 -3
- package/src/testUtils.ts +189 -8
- package/src/useEveAgentRuntime.cloud.test.tsx +105 -136
- package/src/useEveAgentRuntime.eveResume.test.tsx +22 -104
- package/src/useEveAgentRuntime.fast-refresh.test.tsx +195 -37
- package/src/useEveAgentRuntime.interruptedTurn.test.tsx +86 -40
- package/src/useEveAgentRuntime.refetch.test.tsx +19 -58
- package/src/useEveAgentRuntime.test.tsx +372 -291
- package/src/useEveAgentRuntime.ts +100 -113
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
|
-
import type {
|
|
2
|
+
import type { EveInteractionPart, EveToolPart, Interaction } from "eve/react";
|
|
3
3
|
import {
|
|
4
|
-
|
|
5
|
-
type
|
|
6
|
-
type
|
|
4
|
+
conversationOf,
|
|
5
|
+
type ConversationState,
|
|
6
|
+
type SessionEvent,
|
|
7
|
+
type SessionStreamEvent,
|
|
7
8
|
} from "eve/client";
|
|
8
9
|
import {
|
|
9
10
|
collectInterruptedTurnEvents,
|
|
11
|
+
convertEveMessage,
|
|
10
12
|
convertEveMessages,
|
|
11
13
|
type InterruptedTurnEventCache,
|
|
12
14
|
findEveInputRequest,
|
|
@@ -15,36 +17,137 @@ import {
|
|
|
15
17
|
} from "./convertEveMessages";
|
|
16
18
|
import type { AppendMessage } from "@assistant-ui/core";
|
|
17
19
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
type MessageData = Pick<ConversationState, "messages">;
|
|
21
|
+
type InputRequest = Interaction["request"];
|
|
22
|
+
|
|
23
|
+
const approvalInteraction = (
|
|
24
|
+
request: InputRequest = { kind: "approval", prompt: "Send the email?" },
|
|
25
|
+
): Interaction => ({
|
|
26
|
+
interactionId: "req_1",
|
|
27
|
+
turnId: "turn_1",
|
|
28
|
+
stepIndex: 0,
|
|
29
|
+
status: "open",
|
|
30
|
+
request,
|
|
31
|
+
call: { callId: "call_1", name: "send_email", input: {} },
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const toolPart = (
|
|
35
|
+
overrides: {
|
|
36
|
+
state?: "requested" | "approval" | "rejected";
|
|
37
|
+
input?: unknown;
|
|
38
|
+
interaction?: Interaction;
|
|
39
|
+
name?: string;
|
|
40
|
+
label?: string;
|
|
41
|
+
} = {},
|
|
42
|
+
): Exclude<EveToolPart, { state: "streaming" }> => ({
|
|
43
|
+
type: "tool",
|
|
44
|
+
callId: "call_1",
|
|
45
|
+
stepIndex: 0,
|
|
46
|
+
kind: "tool",
|
|
47
|
+
name: "send_email",
|
|
48
|
+
label: "send_email",
|
|
49
|
+
state: overrides.interaction ? "approval" : "requested",
|
|
50
|
+
input: {},
|
|
51
|
+
...overrides,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const signInPart = (
|
|
55
|
+
request: InputRequest,
|
|
56
|
+
overrides: Partial<Interaction> = {},
|
|
57
|
+
): EveInteractionPart => ({
|
|
58
|
+
type: "interaction",
|
|
59
|
+
interactionId: "auth_1",
|
|
60
|
+
turnId: "turn_1",
|
|
61
|
+
stepIndex: 0,
|
|
62
|
+
status: "open",
|
|
63
|
+
request,
|
|
64
|
+
...overrides,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const withApprovalPart = (request?: InputRequest): MessageData => ({
|
|
23
68
|
messages: [
|
|
24
69
|
{
|
|
25
70
|
id: "a1",
|
|
26
71
|
role: "assistant",
|
|
27
|
-
parts: [
|
|
28
|
-
{
|
|
29
|
-
type: "dynamic-tool",
|
|
30
|
-
state: "approval-requested",
|
|
31
|
-
toolCallId: "call_1",
|
|
32
|
-
toolName: "send_email",
|
|
33
|
-
input: {},
|
|
34
|
-
approval: { id: "req_1" },
|
|
35
|
-
...(eve && { toolMetadata: { eve } }),
|
|
36
|
-
},
|
|
37
|
-
],
|
|
72
|
+
parts: [toolPart({ interaction: approvalInteraction(request) })],
|
|
38
73
|
},
|
|
39
74
|
],
|
|
40
75
|
});
|
|
41
76
|
|
|
42
77
|
const eventMeta = (sequence: number) => ({
|
|
43
78
|
at: "2026-01-01T00:00:00.000Z",
|
|
44
|
-
|
|
79
|
+
position: { line: sequence, index: 0 },
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const replay = (events: readonly SessionEvent[]) =>
|
|
83
|
+
conversationOf(
|
|
84
|
+
events.map((event, index) => ({ ...event, meta: eventMeta(index) })),
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const initialTurnEvents = [
|
|
88
|
+
{ type: "session.started", data: {} },
|
|
89
|
+
{ type: "delivery.admitted", data: { deliveryId: "delivery_1" } },
|
|
90
|
+
{
|
|
91
|
+
type: "turn.started",
|
|
92
|
+
data: {
|
|
93
|
+
turnId: "turn_1",
|
|
94
|
+
cause: { deliveryId: "delivery_1" },
|
|
95
|
+
follows: null,
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
type: "delivery.consumed",
|
|
100
|
+
data: {
|
|
101
|
+
deliveryId: "delivery_1",
|
|
102
|
+
turnId: "turn_1",
|
|
103
|
+
parts: [{ kind: "text", text: "hi" }],
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
type: "model.requested",
|
|
108
|
+
scope: { turnId: "turn_1" },
|
|
109
|
+
data: { runId: "run_1", owner: { turnId: "turn_1" } },
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
type: "model.started",
|
|
113
|
+
scope: { turnId: "turn_1", runId: "run_1" },
|
|
114
|
+
data: { runId: "run_1", modelId: "test-model", attempt: 0 },
|
|
115
|
+
},
|
|
116
|
+
] as const satisfies readonly SessionEvent[];
|
|
117
|
+
|
|
118
|
+
const signInOpened = (
|
|
119
|
+
name: string,
|
|
120
|
+
signIn: Partial<NonNullable<InputRequest["signIn"]>> = {},
|
|
121
|
+
): SessionEvent => ({
|
|
122
|
+
type: "interaction.opened",
|
|
123
|
+
scope: { turnId: "turn_1" },
|
|
124
|
+
data: {
|
|
125
|
+
interactionId: `auth_${name}`,
|
|
126
|
+
subject: { turnId: "turn_1" },
|
|
127
|
+
request: {
|
|
128
|
+
kind: "sign-in",
|
|
129
|
+
prompt: `Authorization required for ${signIn.displayName ?? name}`,
|
|
130
|
+
signIn: { name, ...signIn },
|
|
131
|
+
},
|
|
132
|
+
},
|
|
45
133
|
});
|
|
46
134
|
|
|
47
135
|
describe("convertEveMessages", () => {
|
|
136
|
+
it("converts a single completed message with default options", () => {
|
|
137
|
+
const message = {
|
|
138
|
+
id: "a1",
|
|
139
|
+
role: "assistant",
|
|
140
|
+
parts: [{ type: "text", text: "Hello" }],
|
|
141
|
+
} as const satisfies MessageData["messages"][number];
|
|
142
|
+
|
|
143
|
+
expect(convertEveMessage(message, 0, [message])).toMatchObject({
|
|
144
|
+
id: "a1",
|
|
145
|
+
role: "assistant",
|
|
146
|
+
content: [{ type: "text", text: "Hello" }],
|
|
147
|
+
status: { type: "complete", reason: "stop" },
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
48
151
|
it("converts text and reasoning parts", () => {
|
|
49
152
|
const data = {
|
|
50
153
|
messages: [
|
|
@@ -63,7 +166,7 @@ describe("convertEveMessages", () => {
|
|
|
63
166
|
],
|
|
64
167
|
},
|
|
65
168
|
],
|
|
66
|
-
} satisfies
|
|
169
|
+
} satisfies MessageData;
|
|
67
170
|
|
|
68
171
|
const messages = convertEveMessages(data, { isRunning: true });
|
|
69
172
|
|
|
@@ -97,7 +200,7 @@ describe("convertEveMessages", () => {
|
|
|
97
200
|
],
|
|
98
201
|
},
|
|
99
202
|
],
|
|
100
|
-
} satisfies
|
|
203
|
+
} satisfies MessageData;
|
|
101
204
|
|
|
102
205
|
const messages = convertEveMessages(data, { isRunning: true });
|
|
103
206
|
|
|
@@ -119,7 +222,7 @@ describe("convertEveMessages", () => {
|
|
|
119
222
|
],
|
|
120
223
|
},
|
|
121
224
|
],
|
|
122
|
-
} satisfies
|
|
225
|
+
} satisfies MessageData;
|
|
123
226
|
|
|
124
227
|
const messages = convertEveMessages(data);
|
|
125
228
|
|
|
@@ -144,7 +247,7 @@ describe("convertEveMessages", () => {
|
|
|
144
247
|
],
|
|
145
248
|
},
|
|
146
249
|
],
|
|
147
|
-
} satisfies
|
|
250
|
+
} satisfies MessageData;
|
|
148
251
|
|
|
149
252
|
const messages = convertEveMessages(data);
|
|
150
253
|
|
|
@@ -153,42 +256,30 @@ describe("convertEveMessages", () => {
|
|
|
153
256
|
}
|
|
154
257
|
});
|
|
155
258
|
|
|
156
|
-
it("converts
|
|
259
|
+
it("converts tool parts with approval options", () => {
|
|
157
260
|
const data = {
|
|
158
261
|
messages: [
|
|
159
262
|
{
|
|
160
263
|
id: "a1",
|
|
161
264
|
role: "assistant",
|
|
162
265
|
parts: [
|
|
163
|
-
{
|
|
164
|
-
type: "dynamic-tool",
|
|
165
|
-
state: "approval-requested",
|
|
166
|
-
toolCallId: "call_1",
|
|
167
|
-
toolName: "send_email",
|
|
266
|
+
toolPart({
|
|
168
267
|
input: { to: "dev@example.com" },
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
{ id: "approve", label: "Approve" },
|
|
181
|
-
{ id: "cancel", label: "Cancel", style: "danger" },
|
|
182
|
-
{ id: "escalate", label: "Escalate" },
|
|
183
|
-
],
|
|
184
|
-
},
|
|
185
|
-
},
|
|
186
|
-
},
|
|
187
|
-
},
|
|
268
|
+
interaction: approvalInteraction({
|
|
269
|
+
kind: "approval",
|
|
270
|
+
prompt: "Send the email?",
|
|
271
|
+
display: "confirmation",
|
|
272
|
+
options: [
|
|
273
|
+
{ id: "approve", label: "Approve" },
|
|
274
|
+
{ id: "cancel", label: "Cancel", style: "danger" },
|
|
275
|
+
{ id: "escalate", label: "Escalate" },
|
|
276
|
+
],
|
|
277
|
+
}),
|
|
278
|
+
}),
|
|
188
279
|
],
|
|
189
280
|
},
|
|
190
281
|
],
|
|
191
|
-
} satisfies
|
|
282
|
+
} satisfies MessageData;
|
|
192
283
|
|
|
193
284
|
const [message] = convertEveMessages(data);
|
|
194
285
|
|
|
@@ -204,7 +295,7 @@ describe("convertEveMessages", () => {
|
|
|
204
295
|
id: "req_1",
|
|
205
296
|
options: [
|
|
206
297
|
{ id: "approve", kind: "allow-once", label: "Approve" },
|
|
207
|
-
{ id: "cancel", kind: "
|
|
298
|
+
{ id: "cancel", kind: "_cancel", label: "Cancel" },
|
|
208
299
|
{ id: "escalate", kind: "_escalate", label: "Escalate" },
|
|
209
300
|
],
|
|
210
301
|
},
|
|
@@ -213,7 +304,7 @@ describe("convertEveMessages", () => {
|
|
|
213
304
|
});
|
|
214
305
|
});
|
|
215
306
|
|
|
216
|
-
it("keeps streaming
|
|
307
|
+
it("keeps streaming tool args incomplete until input is available", () => {
|
|
217
308
|
const streamingData = {
|
|
218
309
|
messages: [
|
|
219
310
|
{
|
|
@@ -221,17 +312,19 @@ describe("convertEveMessages", () => {
|
|
|
221
312
|
role: "assistant",
|
|
222
313
|
parts: [
|
|
223
314
|
{
|
|
224
|
-
type: "
|
|
225
|
-
state: "
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
315
|
+
type: "tool",
|
|
316
|
+
state: "streaming",
|
|
317
|
+
callId: "call_1",
|
|
318
|
+
stepIndex: 0,
|
|
319
|
+
kind: "tool",
|
|
320
|
+
name: "search",
|
|
321
|
+
label: "search",
|
|
229
322
|
inputText: "",
|
|
230
323
|
},
|
|
231
324
|
],
|
|
232
325
|
},
|
|
233
326
|
],
|
|
234
|
-
} satisfies
|
|
327
|
+
} satisfies MessageData;
|
|
235
328
|
|
|
236
329
|
const [streamingMessage] = convertEveMessages(streamingData);
|
|
237
330
|
|
|
@@ -247,17 +340,15 @@ describe("convertEveMessages", () => {
|
|
|
247
340
|
id: "a1",
|
|
248
341
|
role: "assistant",
|
|
249
342
|
parts: [
|
|
250
|
-
{
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
toolCallId: "call_1",
|
|
254
|
-
toolName: "search",
|
|
343
|
+
toolPart({
|
|
344
|
+
name: "search",
|
|
345
|
+
label: "search",
|
|
255
346
|
input: { query: "badge" },
|
|
256
|
-
},
|
|
347
|
+
}),
|
|
257
348
|
],
|
|
258
349
|
},
|
|
259
350
|
],
|
|
260
|
-
} satisfies
|
|
351
|
+
} satisfies MessageData;
|
|
261
352
|
|
|
262
353
|
const [availableMessage] = convertEveMessages(availableData);
|
|
263
354
|
|
|
@@ -272,8 +363,7 @@ describe("convertEveMessages", () => {
|
|
|
272
363
|
[
|
|
273
364
|
"a confirmation request",
|
|
274
365
|
{
|
|
275
|
-
|
|
276
|
-
kind: "tool-approval",
|
|
366
|
+
kind: "approval",
|
|
277
367
|
prompt: "Send the email?",
|
|
278
368
|
display: "confirmation",
|
|
279
369
|
options: [
|
|
@@ -286,7 +376,6 @@ describe("convertEveMessages", () => {
|
|
|
286
376
|
[
|
|
287
377
|
"a select question that also takes a typed answer",
|
|
288
378
|
{
|
|
289
|
-
requestId: "req_1",
|
|
290
379
|
kind: "question",
|
|
291
380
|
prompt: "Which environment?",
|
|
292
381
|
display: "select",
|
|
@@ -302,7 +391,6 @@ describe("convertEveMessages", () => {
|
|
|
302
391
|
[
|
|
303
392
|
"a text question",
|
|
304
393
|
{
|
|
305
|
-
requestId: "req_1",
|
|
306
394
|
kind: "question",
|
|
307
395
|
prompt: "What should the subject line be?",
|
|
308
396
|
display: "text",
|
|
@@ -315,19 +403,13 @@ describe("convertEveMessages", () => {
|
|
|
315
403
|
],
|
|
316
404
|
[
|
|
317
405
|
"a question with neither a display nor options",
|
|
318
|
-
{
|
|
406
|
+
{ kind: "question", prompt: "Anything else?" },
|
|
319
407
|
{ prompt: "Anything else?", display: "text", allowFreeform: true },
|
|
320
408
|
],
|
|
321
|
-
] satisfies [string,
|
|
409
|
+
] satisfies [string, InputRequest, object][])(
|
|
322
410
|
"describes %s on the approval itself",
|
|
323
411
|
(_label, inputRequest, expected) => {
|
|
324
|
-
const [message] = convertEveMessages(
|
|
325
|
-
withApprovalPart({
|
|
326
|
-
kind: "tool-call",
|
|
327
|
-
name: "send_email",
|
|
328
|
-
inputRequest,
|
|
329
|
-
}),
|
|
330
|
-
);
|
|
412
|
+
const [message] = convertEveMessages(withApprovalPart(inputRequest));
|
|
331
413
|
|
|
332
414
|
expect(message!.content[0]).toMatchObject({ approval: expected });
|
|
333
415
|
},
|
|
@@ -342,14 +424,9 @@ describe("convertEveMessages", () => {
|
|
|
342
424
|
(_label, display) => {
|
|
343
425
|
const [message] = convertEveMessages(
|
|
344
426
|
withApprovalPart({
|
|
345
|
-
kind: "
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
requestId: "req_1",
|
|
349
|
-
kind: "tool-approval",
|
|
350
|
-
prompt: "Send the email?",
|
|
351
|
-
...(display && { display }),
|
|
352
|
-
},
|
|
427
|
+
kind: "approval",
|
|
428
|
+
prompt: "Send the email?",
|
|
429
|
+
...(display && { display }),
|
|
353
430
|
}),
|
|
354
431
|
);
|
|
355
432
|
|
|
@@ -365,13 +442,8 @@ describe("convertEveMessages", () => {
|
|
|
365
442
|
it("does not offer a typed answer on a tool approval whose options were dropped", () => {
|
|
366
443
|
const [message] = convertEveMessages(
|
|
367
444
|
withApprovalPart({
|
|
368
|
-
kind: "
|
|
369
|
-
|
|
370
|
-
inputRequest: {
|
|
371
|
-
requestId: "req_1",
|
|
372
|
-
kind: "tool-approval",
|
|
373
|
-
prompt: "Send the email?",
|
|
374
|
-
},
|
|
445
|
+
kind: "approval",
|
|
446
|
+
prompt: "Send the email?",
|
|
375
447
|
}),
|
|
376
448
|
);
|
|
377
449
|
|
|
@@ -385,18 +457,13 @@ describe("convertEveMessages", () => {
|
|
|
385
457
|
it("does not offer a typed answer on a confirmation request", () => {
|
|
386
458
|
const [message] = convertEveMessages(
|
|
387
459
|
withApprovalPart({
|
|
388
|
-
kind: "
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
options: [
|
|
396
|
-
{ id: "approve", label: "Approve" },
|
|
397
|
-
{ id: "cancel", label: "Cancel" },
|
|
398
|
-
],
|
|
399
|
-
},
|
|
460
|
+
kind: "approval",
|
|
461
|
+
prompt: "Send the email?",
|
|
462
|
+
display: "confirmation",
|
|
463
|
+
options: [
|
|
464
|
+
{ id: "approve", label: "Approve" },
|
|
465
|
+
{ id: "cancel", label: "Cancel" },
|
|
466
|
+
],
|
|
400
467
|
}),
|
|
401
468
|
);
|
|
402
469
|
|
|
@@ -409,7 +476,6 @@ describe("convertEveMessages", () => {
|
|
|
409
476
|
[
|
|
410
477
|
"the full input request",
|
|
411
478
|
{
|
|
412
|
-
requestId: "req_1",
|
|
413
479
|
prompt: "Which environment?",
|
|
414
480
|
kind: "question",
|
|
415
481
|
display: "select",
|
|
@@ -423,40 +489,29 @@ describe("convertEveMessages", () => {
|
|
|
423
489
|
[
|
|
424
490
|
"only the fields the request defines",
|
|
425
491
|
{
|
|
426
|
-
requestId: "req_1",
|
|
427
492
|
prompt: "What should the subject line be?",
|
|
428
493
|
kind: "question",
|
|
429
494
|
},
|
|
430
495
|
],
|
|
431
|
-
] satisfies [string,
|
|
496
|
+
] satisfies [string, InputRequest][])(
|
|
432
497
|
"projects %s onto providerMetadata.eve",
|
|
433
498
|
(_label, inputRequest) => {
|
|
434
|
-
const [message] = convertEveMessages(
|
|
435
|
-
withApprovalPart({
|
|
436
|
-
kind: "tool-call",
|
|
437
|
-
name: "send_email",
|
|
438
|
-
inputRequest,
|
|
439
|
-
}),
|
|
440
|
-
);
|
|
499
|
+
const [message] = convertEveMessages(withApprovalPart(inputRequest));
|
|
441
500
|
const part = message!.content[0];
|
|
442
501
|
|
|
443
502
|
expect(part).toMatchObject({ type: "tool-call" });
|
|
444
503
|
expect((part as { providerMetadata?: unknown }).providerMetadata).toEqual(
|
|
445
504
|
{
|
|
446
|
-
eve: { inputRequest },
|
|
505
|
+
eve: { interaction: { interactionId: "req_1", ...inputRequest } },
|
|
447
506
|
},
|
|
448
507
|
);
|
|
449
508
|
},
|
|
450
509
|
);
|
|
451
510
|
|
|
452
|
-
it
|
|
453
|
-
[
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
],
|
|
457
|
-
["no eve metadata at all", undefined],
|
|
458
|
-
])("omits providerMetadata for a tool part with %s", (_label, eve) => {
|
|
459
|
-
const [message] = convertEveMessages(withApprovalPart(eve));
|
|
511
|
+
it("omits providerMetadata for a tool part without an interaction", () => {
|
|
512
|
+
const [message] = convertEveMessages({
|
|
513
|
+
messages: [{ id: "a1", role: "assistant", parts: [toolPart()] }],
|
|
514
|
+
});
|
|
460
515
|
|
|
461
516
|
expect(message!.content[0]).not.toHaveProperty("providerMetadata");
|
|
462
517
|
});
|
|
@@ -469,17 +524,20 @@ describe("convertEveMessages", () => {
|
|
|
469
524
|
role: "assistant",
|
|
470
525
|
parts: [
|
|
471
526
|
{
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
527
|
+
...toolPart({
|
|
528
|
+
state: "rejected",
|
|
529
|
+
input: { to: "dev@example.com" },
|
|
530
|
+
interaction: {
|
|
531
|
+
...approvalInteraction(),
|
|
532
|
+
status: "settled",
|
|
533
|
+
outcome: "declined",
|
|
534
|
+
},
|
|
535
|
+
}),
|
|
478
536
|
},
|
|
479
537
|
],
|
|
480
538
|
},
|
|
481
539
|
],
|
|
482
|
-
} satisfies
|
|
540
|
+
} satisfies MessageData;
|
|
483
541
|
|
|
484
542
|
const [message] = convertEveMessages(data);
|
|
485
543
|
|
|
@@ -496,6 +554,161 @@ describe("convertEveMessages", () => {
|
|
|
496
554
|
});
|
|
497
555
|
});
|
|
498
556
|
|
|
557
|
+
it.each([
|
|
558
|
+
["completed", { answer: "sent" }, { answer: "sent" }, false],
|
|
559
|
+
["failed", undefined, { error: "Tool call failed" }, true],
|
|
560
|
+
["cancelled", undefined, { error: "Tool call cancelled" }, true],
|
|
561
|
+
] as const)(
|
|
562
|
+
"maps a %s tool call to its result and error state",
|
|
563
|
+
(state, output, result, isError) => {
|
|
564
|
+
const [message] = convertEveMessages({
|
|
565
|
+
messages: [
|
|
566
|
+
{
|
|
567
|
+
id: "a1",
|
|
568
|
+
role: "assistant",
|
|
569
|
+
parts: [{ ...toolPart(), state, output }],
|
|
570
|
+
},
|
|
571
|
+
],
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
expect(message?.content[0]).toMatchObject({ type: "tool-call", result });
|
|
575
|
+
expect(Object.hasOwn(message?.content[0] ?? {}, "isError")).toBe(isError);
|
|
576
|
+
expect(message?.content[0]).toMatchObject(isError ? { isError } : {});
|
|
577
|
+
},
|
|
578
|
+
);
|
|
579
|
+
|
|
580
|
+
it("uses a failed tool's error before its interaction reason", () => {
|
|
581
|
+
const [message] = convertEveMessages({
|
|
582
|
+
messages: [
|
|
583
|
+
{
|
|
584
|
+
id: "a1",
|
|
585
|
+
role: "assistant",
|
|
586
|
+
parts: [
|
|
587
|
+
{
|
|
588
|
+
...toolPart({
|
|
589
|
+
interaction: { ...approvalInteraction(), reason: "Denied" },
|
|
590
|
+
}),
|
|
591
|
+
state: "failed",
|
|
592
|
+
error: { code: "internal", message: "Service unavailable" },
|
|
593
|
+
},
|
|
594
|
+
],
|
|
595
|
+
},
|
|
596
|
+
],
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
expect(message?.content[0]).toMatchObject({
|
|
600
|
+
result: { error: "Service unavailable" },
|
|
601
|
+
isError: true,
|
|
602
|
+
});
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
it("exposes preliminary tool output before the call settles", () => {
|
|
606
|
+
const [message] = convertEveMessages({
|
|
607
|
+
messages: [
|
|
608
|
+
{
|
|
609
|
+
id: "a1",
|
|
610
|
+
role: "assistant",
|
|
611
|
+
parts: [{ ...toolPart({ input: ["first"] }), output: "partial" }],
|
|
612
|
+
},
|
|
613
|
+
],
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
expect(message?.content[0]).toMatchObject({
|
|
617
|
+
args: { value: ["first"] },
|
|
618
|
+
argsText: '["first"]',
|
|
619
|
+
result: "partial",
|
|
620
|
+
isPreliminary: true,
|
|
621
|
+
});
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
it.each([
|
|
625
|
+
[
|
|
626
|
+
"accepted",
|
|
627
|
+
{ approved: true, optionId: "approve", text: "Proceed" },
|
|
628
|
+
false,
|
|
629
|
+
],
|
|
630
|
+
["declined", { approved: false, optionId: "deny", text: "Later" }, true],
|
|
631
|
+
["expired", { resolution: "expired" }, true],
|
|
632
|
+
["withdrawn", { resolution: "cancelled" }, true],
|
|
633
|
+
] as const)(
|
|
634
|
+
"projects a settled %s interaction with its answer or resolution",
|
|
635
|
+
(outcome, approval, isError) => {
|
|
636
|
+
const interaction = signInPart(
|
|
637
|
+
{ kind: "question", prompt: "Proceed?", options: approveCancel },
|
|
638
|
+
{
|
|
639
|
+
status: "settled",
|
|
640
|
+
outcome,
|
|
641
|
+
reason: "Session finished",
|
|
642
|
+
...(outcome === "accepted" || outcome === "declined"
|
|
643
|
+
? {
|
|
644
|
+
answer: {
|
|
645
|
+
interactionId: "auth_1",
|
|
646
|
+
optionId: outcome === "accepted" ? "approve" : "deny",
|
|
647
|
+
text: outcome === "accepted" ? "Proceed" : "Later",
|
|
648
|
+
},
|
|
649
|
+
}
|
|
650
|
+
: {}),
|
|
651
|
+
},
|
|
652
|
+
);
|
|
653
|
+
const [message] = convertEveMessages({
|
|
654
|
+
messages: [{ id: "a1", role: "assistant", parts: [interaction] }],
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
expect(message?.content[0]).toMatchObject({
|
|
658
|
+
type: "tool-call",
|
|
659
|
+
toolName: "question",
|
|
660
|
+
approval: { ...approval, reason: "Session finished" },
|
|
661
|
+
});
|
|
662
|
+
expect(Object.hasOwn(message?.content[0] ?? {}, "isError")).toBe(isError);
|
|
663
|
+
expect(message?.content[0]).toMatchObject(isError ? { isError } : {});
|
|
664
|
+
expect(message?.content[0]).toHaveProperty(
|
|
665
|
+
"result",
|
|
666
|
+
outcome === "accepted" || outcome === "declined"
|
|
667
|
+
? {
|
|
668
|
+
interactionId: "auth_1",
|
|
669
|
+
optionId: outcome === "accepted" ? "approve" : "deny",
|
|
670
|
+
text: outcome === "accepted" ? "Proceed" : "Later",
|
|
671
|
+
}
|
|
672
|
+
: { outcome, reason: "Session finished" },
|
|
673
|
+
);
|
|
674
|
+
},
|
|
675
|
+
);
|
|
676
|
+
|
|
677
|
+
it.each([
|
|
678
|
+
["declined", "declined"],
|
|
679
|
+
["invalid", "declined"],
|
|
680
|
+
["expired", "timed-out"],
|
|
681
|
+
["failed", "failed"],
|
|
682
|
+
["withdrawn", "cancelled"],
|
|
683
|
+
] as const)("maps a %s sign-in to %s", (outcome, mapped) => {
|
|
684
|
+
const [message] = convertEveMessages({
|
|
685
|
+
messages: [
|
|
686
|
+
{
|
|
687
|
+
id: "a1",
|
|
688
|
+
role: "assistant",
|
|
689
|
+
parts: [
|
|
690
|
+
signInPart(
|
|
691
|
+
{ kind: "sign-in", prompt: "Connect", title: "Account" },
|
|
692
|
+
{ status: "settled", outcome, reason: "Session finished" },
|
|
693
|
+
),
|
|
694
|
+
],
|
|
695
|
+
},
|
|
696
|
+
],
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
expect(message?.content[0]).toEqual({
|
|
700
|
+
type: "data",
|
|
701
|
+
name: "authorization",
|
|
702
|
+
data: {
|
|
703
|
+
state: "completed",
|
|
704
|
+
name: "Account",
|
|
705
|
+
description: "Connect",
|
|
706
|
+
outcome: mapped,
|
|
707
|
+
reason: "Session finished",
|
|
708
|
+
},
|
|
709
|
+
});
|
|
710
|
+
});
|
|
711
|
+
|
|
499
712
|
it("drops empty and whitespace-only assistant text and reasoning parts", () => {
|
|
500
713
|
const data = {
|
|
501
714
|
messages: [
|
|
@@ -509,7 +722,7 @@ describe("convertEveMessages", () => {
|
|
|
509
722
|
],
|
|
510
723
|
},
|
|
511
724
|
],
|
|
512
|
-
} satisfies
|
|
725
|
+
} satisfies MessageData;
|
|
513
726
|
|
|
514
727
|
const [message] = convertEveMessages(data);
|
|
515
728
|
|
|
@@ -526,7 +739,7 @@ describe("convertEveMessages", () => {
|
|
|
526
739
|
parts: [{ type: "text", text: "Hello" }],
|
|
527
740
|
},
|
|
528
741
|
],
|
|
529
|
-
} satisfies
|
|
742
|
+
} satisfies MessageData;
|
|
530
743
|
|
|
531
744
|
const [message] = convertEveMessages(data);
|
|
532
745
|
|
|
@@ -543,7 +756,7 @@ describe("convertEveMessages", () => {
|
|
|
543
756
|
parts: [{ type: "text", text: "Hello" }],
|
|
544
757
|
},
|
|
545
758
|
],
|
|
546
|
-
} satisfies
|
|
759
|
+
} satisfies MessageData;
|
|
547
760
|
|
|
548
761
|
const [message] = convertEveMessages(data);
|
|
549
762
|
|
|
@@ -559,7 +772,7 @@ describe("convertEveMessages", () => {
|
|
|
559
772
|
parts: [{ type: "file", mediaType: "image/png" }],
|
|
560
773
|
},
|
|
561
774
|
],
|
|
562
|
-
} satisfies
|
|
775
|
+
} satisfies MessageData;
|
|
563
776
|
|
|
564
777
|
const [message] = convertEveMessages(data);
|
|
565
778
|
|
|
@@ -579,14 +792,14 @@ describe("convertEveMessages", () => {
|
|
|
579
792
|
],
|
|
580
793
|
},
|
|
581
794
|
],
|
|
582
|
-
} satisfies
|
|
795
|
+
} satisfies MessageData;
|
|
583
796
|
|
|
584
797
|
const [message] = convertEveMessages(data);
|
|
585
798
|
|
|
586
799
|
expect(message?.content).toEqual([{ type: "text", text: "Hello" }]);
|
|
587
800
|
});
|
|
588
801
|
|
|
589
|
-
it("converts a user file part into
|
|
802
|
+
it("converts a user file part into a file attachment and keeps it out of content", () => {
|
|
590
803
|
const data = {
|
|
591
804
|
messages: [
|
|
592
805
|
{
|
|
@@ -604,19 +817,12 @@ describe("convertEveMessages", () => {
|
|
|
604
817
|
],
|
|
605
818
|
},
|
|
606
819
|
],
|
|
607
|
-
} satisfies
|
|
820
|
+
} satisfies MessageData;
|
|
608
821
|
|
|
609
822
|
const [message] = convertEveMessages(data);
|
|
610
823
|
|
|
611
824
|
expect(message?.content).toEqual([
|
|
612
825
|
{ type: "text", text: "See the report" },
|
|
613
|
-
{
|
|
614
|
-
type: "file",
|
|
615
|
-
data: "https://example.com/report.pdf",
|
|
616
|
-
mimeType: "application/pdf",
|
|
617
|
-
filename: "report.pdf",
|
|
618
|
-
sourceType: "url",
|
|
619
|
-
},
|
|
620
826
|
]);
|
|
621
827
|
expect(message?.attachments).toEqual([
|
|
622
828
|
{
|
|
@@ -654,19 +860,11 @@ describe("convertEveMessages", () => {
|
|
|
654
860
|
],
|
|
655
861
|
},
|
|
656
862
|
],
|
|
657
|
-
} satisfies
|
|
863
|
+
} satisfies MessageData;
|
|
658
864
|
|
|
659
865
|
const [message] = convertEveMessages(data);
|
|
660
866
|
|
|
661
|
-
expect(message?.content).toEqual([
|
|
662
|
-
{
|
|
663
|
-
type: "file",
|
|
664
|
-
data: "https://example.com/photo.png",
|
|
665
|
-
mimeType: "image/png",
|
|
666
|
-
filename: "photo.png",
|
|
667
|
-
sourceType: "url",
|
|
668
|
-
},
|
|
669
|
-
]);
|
|
867
|
+
expect(message?.content).toEqual([]);
|
|
670
868
|
expect(message?.attachments).toEqual([
|
|
671
869
|
{
|
|
672
870
|
id: "0",
|
|
@@ -700,17 +898,11 @@ describe("convertEveMessages", () => {
|
|
|
700
898
|
],
|
|
701
899
|
},
|
|
702
900
|
],
|
|
703
|
-
} satisfies
|
|
901
|
+
} satisfies MessageData;
|
|
704
902
|
|
|
705
903
|
const [message] = convertEveMessages(data);
|
|
706
904
|
|
|
707
|
-
expect(message?.content).toEqual([
|
|
708
|
-
{
|
|
709
|
-
type: "file",
|
|
710
|
-
data: "data:application/pdf;base64,QUJD",
|
|
711
|
-
mimeType: "application/pdf",
|
|
712
|
-
},
|
|
713
|
-
]);
|
|
905
|
+
expect(message?.content).toEqual([]);
|
|
714
906
|
expect(message?.attachments).toEqual([
|
|
715
907
|
{
|
|
716
908
|
id: "0",
|
|
@@ -750,7 +942,7 @@ describe("convertEveMessages", () => {
|
|
|
750
942
|
],
|
|
751
943
|
},
|
|
752
944
|
],
|
|
753
|
-
} satisfies
|
|
945
|
+
} satisfies MessageData;
|
|
754
946
|
|
|
755
947
|
const [message] = convertEveMessages(data);
|
|
756
948
|
|
|
@@ -760,71 +952,6 @@ describe("convertEveMessages", () => {
|
|
|
760
952
|
]);
|
|
761
953
|
});
|
|
762
954
|
|
|
763
|
-
it("defaults a file part with a missing mediaType to application/octet-stream", () => {
|
|
764
|
-
const data = {
|
|
765
|
-
messages: [
|
|
766
|
-
{
|
|
767
|
-
id: "u1",
|
|
768
|
-
role: "user",
|
|
769
|
-
parts: [{ type: "file", url: "https://example.com/blob" }],
|
|
770
|
-
},
|
|
771
|
-
],
|
|
772
|
-
} as unknown as EveMessageData;
|
|
773
|
-
|
|
774
|
-
const [message] = convertEveMessages(data);
|
|
775
|
-
|
|
776
|
-
expect(message?.content).toEqual([
|
|
777
|
-
{
|
|
778
|
-
type: "file",
|
|
779
|
-
data: "https://example.com/blob",
|
|
780
|
-
mimeType: "application/octet-stream",
|
|
781
|
-
sourceType: "url",
|
|
782
|
-
},
|
|
783
|
-
]);
|
|
784
|
-
expect(message?.attachments).toEqual([
|
|
785
|
-
{
|
|
786
|
-
id: "0",
|
|
787
|
-
type: "file",
|
|
788
|
-
name: "file",
|
|
789
|
-
content: [
|
|
790
|
-
{
|
|
791
|
-
type: "file",
|
|
792
|
-
data: "https://example.com/blob",
|
|
793
|
-
mimeType: "application/octet-stream",
|
|
794
|
-
sourceType: "url",
|
|
795
|
-
},
|
|
796
|
-
],
|
|
797
|
-
contentType: "application/octet-stream",
|
|
798
|
-
status: { type: "complete" },
|
|
799
|
-
},
|
|
800
|
-
]);
|
|
801
|
-
});
|
|
802
|
-
|
|
803
|
-
it("reads the media type from a data URL when eve omits mediaType", () => {
|
|
804
|
-
const data = {
|
|
805
|
-
messages: [
|
|
806
|
-
{
|
|
807
|
-
id: "u1",
|
|
808
|
-
role: "user",
|
|
809
|
-
parts: [{ type: "file", url: "data:image/png;base64,iVBORw0KGgo=" }],
|
|
810
|
-
},
|
|
811
|
-
],
|
|
812
|
-
} as unknown as EveMessageData;
|
|
813
|
-
|
|
814
|
-
const [message] = convertEveMessages(data);
|
|
815
|
-
|
|
816
|
-
expect(message?.content).toEqual([
|
|
817
|
-
{
|
|
818
|
-
type: "file",
|
|
819
|
-
data: "data:image/png;base64,iVBORw0KGgo=",
|
|
820
|
-
mimeType: "image/png",
|
|
821
|
-
},
|
|
822
|
-
]);
|
|
823
|
-
expect(message?.attachments?.map((a) => [a.type, a.contentType])).toEqual([
|
|
824
|
-
["image", "image/png"],
|
|
825
|
-
]);
|
|
826
|
-
});
|
|
827
|
-
|
|
828
955
|
it("prefers an explicit mediaType over the data URL declaration", () => {
|
|
829
956
|
const data = {
|
|
830
957
|
messages: [
|
|
@@ -840,13 +967,13 @@ describe("convertEveMessages", () => {
|
|
|
840
967
|
],
|
|
841
968
|
},
|
|
842
969
|
],
|
|
843
|
-
} satisfies
|
|
970
|
+
} satisfies MessageData;
|
|
844
971
|
|
|
845
972
|
const [message] = convertEveMessages(data);
|
|
846
973
|
|
|
847
|
-
expect(message?.
|
|
974
|
+
expect(message?.attachments?.[0]).toMatchObject({
|
|
848
975
|
type: "file",
|
|
849
|
-
|
|
976
|
+
contentType: "application/pdf",
|
|
850
977
|
});
|
|
851
978
|
});
|
|
852
979
|
|
|
@@ -867,7 +994,7 @@ describe("convertEveMessages", () => {
|
|
|
867
994
|
],
|
|
868
995
|
},
|
|
869
996
|
],
|
|
870
|
-
} satisfies
|
|
997
|
+
} satisfies MessageData;
|
|
871
998
|
|
|
872
999
|
const [message] = convertEveMessages(data);
|
|
873
1000
|
|
|
@@ -883,6 +1010,25 @@ describe("convertEveMessages", () => {
|
|
|
883
1010
|
]);
|
|
884
1011
|
});
|
|
885
1012
|
|
|
1013
|
+
it("drops a URL-less assistant file part while retaining text", () => {
|
|
1014
|
+
const data = {
|
|
1015
|
+
messages: [
|
|
1016
|
+
{
|
|
1017
|
+
id: "a1",
|
|
1018
|
+
role: "assistant",
|
|
1019
|
+
parts: [
|
|
1020
|
+
{ type: "file", mediaType: "application/pdf" },
|
|
1021
|
+
{ type: "text", text: "Done" },
|
|
1022
|
+
],
|
|
1023
|
+
},
|
|
1024
|
+
],
|
|
1025
|
+
} satisfies MessageData;
|
|
1026
|
+
|
|
1027
|
+
const [message] = convertEveMessages(data);
|
|
1028
|
+
|
|
1029
|
+
expect(message?.content).toEqual([{ type: "text", text: "Done" }]);
|
|
1030
|
+
});
|
|
1031
|
+
|
|
886
1032
|
it("drops non-convertible part types instead of throwing", () => {
|
|
887
1033
|
const data = {
|
|
888
1034
|
messages: [
|
|
@@ -890,14 +1036,12 @@ describe("convertEveMessages", () => {
|
|
|
890
1036
|
id: "a1",
|
|
891
1037
|
role: "assistant",
|
|
892
1038
|
parts: [
|
|
893
|
-
{ type: "step-start" },
|
|
894
1039
|
{ type: "future-part", payload: {} },
|
|
895
|
-
{ type: "file", mediaType: "application/pdf" },
|
|
896
1040
|
{ type: "text", text: "Done" },
|
|
897
1041
|
],
|
|
898
1042
|
},
|
|
899
1043
|
],
|
|
900
|
-
} as unknown as
|
|
1044
|
+
} as unknown as MessageData;
|
|
901
1045
|
|
|
902
1046
|
const [message] = convertEveMessages(data);
|
|
903
1047
|
|
|
@@ -911,26 +1055,22 @@ describe("convertEveMessages", () => {
|
|
|
911
1055
|
id: "a1",
|
|
912
1056
|
role: "assistant",
|
|
913
1057
|
parts: [
|
|
914
|
-
{
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
displayName: "GitHub",
|
|
920
|
-
stepIndex: 0,
|
|
921
|
-
turnId: "turn_1",
|
|
922
|
-
authorization: {
|
|
1058
|
+
signInPart({
|
|
1059
|
+
kind: "sign-in",
|
|
1060
|
+
prompt: "Sign in to GitHub",
|
|
1061
|
+
signIn: {
|
|
1062
|
+
name: "github",
|
|
923
1063
|
displayName: "GitHub",
|
|
924
1064
|
instructions: "Enter the code on the device page",
|
|
925
1065
|
url: "https://github.com/login/device",
|
|
926
1066
|
userCode: "ABCD-1234",
|
|
927
1067
|
expiresAt: "2026-01-01T00:00:00Z",
|
|
928
1068
|
},
|
|
929
|
-
},
|
|
1069
|
+
}),
|
|
930
1070
|
],
|
|
931
1071
|
},
|
|
932
1072
|
],
|
|
933
|
-
} satisfies
|
|
1073
|
+
} satisfies MessageData;
|
|
934
1074
|
|
|
935
1075
|
const [message] = convertEveMessages(data);
|
|
936
1076
|
|
|
@@ -959,15 +1099,15 @@ describe("convertEveMessages", () => {
|
|
|
959
1099
|
id: "a1",
|
|
960
1100
|
role: "assistant",
|
|
961
1101
|
parts: [
|
|
962
|
-
{
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
name: "github",
|
|
966
|
-
},
|
|
1102
|
+
signInPart({
|
|
1103
|
+
kind: "sign-in",
|
|
1104
|
+
prompt: "",
|
|
1105
|
+
signIn: { name: "github" },
|
|
1106
|
+
}),
|
|
967
1107
|
],
|
|
968
1108
|
},
|
|
969
1109
|
],
|
|
970
|
-
}
|
|
1110
|
+
} satisfies MessageData;
|
|
971
1111
|
|
|
972
1112
|
const [message] = convertEveMessages(data);
|
|
973
1113
|
|
|
@@ -975,7 +1115,7 @@ describe("convertEveMessages", () => {
|
|
|
975
1115
|
{
|
|
976
1116
|
type: "data",
|
|
977
1117
|
name: "authorization",
|
|
978
|
-
data: { state: "required", name: "github" },
|
|
1118
|
+
data: { state: "required", name: "github", description: "" },
|
|
979
1119
|
},
|
|
980
1120
|
]);
|
|
981
1121
|
});
|
|
@@ -987,19 +1127,19 @@ describe("convertEveMessages", () => {
|
|
|
987
1127
|
id: "a1",
|
|
988
1128
|
role: "assistant",
|
|
989
1129
|
parts: [
|
|
990
|
-
{
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
1130
|
+
signInPart({
|
|
1131
|
+
kind: "sign-in",
|
|
1132
|
+
prompt: "",
|
|
1133
|
+
signIn: {
|
|
1134
|
+
name: "github",
|
|
995
1135
|
url: "javascript:alert(1)",
|
|
996
1136
|
userCode: "ABCD-1234",
|
|
997
1137
|
},
|
|
998
|
-
},
|
|
1138
|
+
}),
|
|
999
1139
|
],
|
|
1000
1140
|
},
|
|
1001
1141
|
],
|
|
1002
|
-
}
|
|
1142
|
+
} satisfies MessageData;
|
|
1003
1143
|
|
|
1004
1144
|
const [message] = convertEveMessages(data);
|
|
1005
1145
|
|
|
@@ -1007,7 +1147,12 @@ describe("convertEveMessages", () => {
|
|
|
1007
1147
|
{
|
|
1008
1148
|
type: "data",
|
|
1009
1149
|
name: "authorization",
|
|
1010
|
-
data: {
|
|
1150
|
+
data: {
|
|
1151
|
+
state: "required",
|
|
1152
|
+
name: "github",
|
|
1153
|
+
description: "",
|
|
1154
|
+
userCode: "ABCD-1234",
|
|
1155
|
+
},
|
|
1011
1156
|
},
|
|
1012
1157
|
]);
|
|
1013
1158
|
});
|
|
@@ -1019,20 +1164,18 @@ describe("convertEveMessages", () => {
|
|
|
1019
1164
|
id: "a1",
|
|
1020
1165
|
role: "assistant",
|
|
1021
1166
|
parts: [
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
outcome: "authorized",
|
|
1031
|
-
},
|
|
1167
|
+
signInPart(
|
|
1168
|
+
{
|
|
1169
|
+
kind: "sign-in",
|
|
1170
|
+
prompt: "Sign in to GitHub",
|
|
1171
|
+
signIn: { name: "github", displayName: "GitHub" },
|
|
1172
|
+
},
|
|
1173
|
+
{ status: "settled", outcome: "accepted" },
|
|
1174
|
+
),
|
|
1032
1175
|
],
|
|
1033
1176
|
},
|
|
1034
1177
|
],
|
|
1035
|
-
} satisfies
|
|
1178
|
+
} satisfies MessageData;
|
|
1036
1179
|
|
|
1037
1180
|
const [message] = convertEveMessages(data);
|
|
1038
1181
|
|
|
@@ -1054,7 +1197,7 @@ describe("convertEveMessages", () => {
|
|
|
1054
1197
|
describe("assistant message status mapping", () => {
|
|
1055
1198
|
const withStatus = (
|
|
1056
1199
|
status: "streaming" | "complete" | "failed",
|
|
1057
|
-
):
|
|
1200
|
+
): MessageData => ({
|
|
1058
1201
|
messages: [
|
|
1059
1202
|
{
|
|
1060
1203
|
id: "a1",
|
|
@@ -1118,7 +1261,7 @@ describe("convertEveMessages", () => {
|
|
|
1118
1261
|
parts: [{ type: "text", text: "Try again" }],
|
|
1119
1262
|
},
|
|
1120
1263
|
],
|
|
1121
|
-
} satisfies
|
|
1264
|
+
} satisfies MessageData;
|
|
1122
1265
|
|
|
1123
1266
|
const [assistant] = convertEveMessages(data, {
|
|
1124
1267
|
isRunning: true,
|
|
@@ -1148,6 +1291,101 @@ describe("convertEveMessages", () => {
|
|
|
1148
1291
|
expect(message?.status).toEqual({ type: "incomplete", reason: "error" });
|
|
1149
1292
|
});
|
|
1150
1293
|
|
|
1294
|
+
it("keeps a failed turn incomplete when the event has no error detail", () => {
|
|
1295
|
+
const failure = {
|
|
1296
|
+
type: "turn.settled",
|
|
1297
|
+
data: { turnId: "turn_1", outcome: "failed" },
|
|
1298
|
+
meta: eventMeta(1),
|
|
1299
|
+
} as const satisfies SessionStreamEvent;
|
|
1300
|
+
const [message] = convertEveMessages(
|
|
1301
|
+
{
|
|
1302
|
+
messages: [
|
|
1303
|
+
{
|
|
1304
|
+
id: "a1",
|
|
1305
|
+
role: "assistant",
|
|
1306
|
+
metadata: { turnId: "turn_1", status: "complete" },
|
|
1307
|
+
parts: [{ type: "text", text: "Partial response" }],
|
|
1308
|
+
},
|
|
1309
|
+
],
|
|
1310
|
+
},
|
|
1311
|
+
{ events: [failure] },
|
|
1312
|
+
);
|
|
1313
|
+
|
|
1314
|
+
expect(message?.status).toEqual({ type: "incomplete", reason: "error" });
|
|
1315
|
+
});
|
|
1316
|
+
|
|
1317
|
+
it("scopes session failures to their turn and gives them precedence over turn cancellation", () => {
|
|
1318
|
+
const events = [
|
|
1319
|
+
{
|
|
1320
|
+
type: "turn.settled",
|
|
1321
|
+
data: { turnId: "turn_1", outcome: "cancelled" },
|
|
1322
|
+
meta: eventMeta(1),
|
|
1323
|
+
},
|
|
1324
|
+
{
|
|
1325
|
+
type: "session.ended",
|
|
1326
|
+
data: { outcome: "failed", cause: { turnId: "turn_1" } },
|
|
1327
|
+
meta: eventMeta(2),
|
|
1328
|
+
},
|
|
1329
|
+
] as const satisfies readonly SessionStreamEvent[];
|
|
1330
|
+
const messages = convertEveMessages(
|
|
1331
|
+
{
|
|
1332
|
+
messages: [
|
|
1333
|
+
{
|
|
1334
|
+
id: "a1",
|
|
1335
|
+
role: "assistant",
|
|
1336
|
+
metadata: { turnId: "turn_1" },
|
|
1337
|
+
parts: [{ type: "text", text: "First" }],
|
|
1338
|
+
},
|
|
1339
|
+
{
|
|
1340
|
+
id: "a2",
|
|
1341
|
+
role: "assistant",
|
|
1342
|
+
metadata: { turnId: "turn_2" },
|
|
1343
|
+
parts: [{ type: "text", text: "Second" }],
|
|
1344
|
+
},
|
|
1345
|
+
],
|
|
1346
|
+
},
|
|
1347
|
+
{ events, isRunning: false },
|
|
1348
|
+
);
|
|
1349
|
+
|
|
1350
|
+
expect(messages.map((message) => message.status)).toEqual([
|
|
1351
|
+
{ type: "incomplete", reason: "error" },
|
|
1352
|
+
{ type: "complete", reason: "stop" },
|
|
1353
|
+
]);
|
|
1354
|
+
});
|
|
1355
|
+
|
|
1356
|
+
it.each([
|
|
1357
|
+
["completed", { outcome: "completed" }],
|
|
1358
|
+
["failed without a cause", { outcome: "failed" }],
|
|
1359
|
+
[
|
|
1360
|
+
"failed outside a turn",
|
|
1361
|
+
{ outcome: "failed", cause: { deliveryId: "delivery_1" } },
|
|
1362
|
+
],
|
|
1363
|
+
] as const)(
|
|
1364
|
+
"does not assign a %s session ending to a turn",
|
|
1365
|
+
(_label, data) => {
|
|
1366
|
+
const event = {
|
|
1367
|
+
type: "session.ended",
|
|
1368
|
+
data,
|
|
1369
|
+
meta: eventMeta(1),
|
|
1370
|
+
} as const satisfies SessionStreamEvent;
|
|
1371
|
+
const [message] = convertEveMessages(
|
|
1372
|
+
{
|
|
1373
|
+
messages: [
|
|
1374
|
+
{
|
|
1375
|
+
id: "a1",
|
|
1376
|
+
role: "assistant",
|
|
1377
|
+
metadata: { turnId: "turn_1" },
|
|
1378
|
+
parts: [{ type: "text", text: "Done" }],
|
|
1379
|
+
},
|
|
1380
|
+
],
|
|
1381
|
+
},
|
|
1382
|
+
{ events: [event], isRunning: false },
|
|
1383
|
+
);
|
|
1384
|
+
|
|
1385
|
+
expect(message?.status).toEqual({ type: "complete", reason: "stop" });
|
|
1386
|
+
},
|
|
1387
|
+
);
|
|
1388
|
+
|
|
1151
1389
|
it("keeps requires-action for pending approvals when not running", () => {
|
|
1152
1390
|
const data = {
|
|
1153
1391
|
messages: [
|
|
@@ -1155,21 +1393,12 @@ describe("convertEveMessages", () => {
|
|
|
1155
1393
|
id: "a1",
|
|
1156
1394
|
role: "assistant",
|
|
1157
1395
|
metadata: { status: "streaming" },
|
|
1158
|
-
parts: [
|
|
1159
|
-
{
|
|
1160
|
-
type: "dynamic-tool",
|
|
1161
|
-
state: "approval-requested",
|
|
1162
|
-
toolCallId: "call_1",
|
|
1163
|
-
toolName: "send_email",
|
|
1164
|
-
input: {},
|
|
1165
|
-
approval: { id: "req_1" },
|
|
1166
|
-
},
|
|
1167
|
-
],
|
|
1396
|
+
parts: [toolPart({ interaction: approvalInteraction() })],
|
|
1168
1397
|
},
|
|
1169
1398
|
],
|
|
1170
|
-
} satisfies
|
|
1399
|
+
} satisfies MessageData;
|
|
1171
1400
|
|
|
1172
|
-
const
|
|
1401
|
+
const message = convertEveMessages(data, { isRunning: false }).at(-1);
|
|
1173
1402
|
|
|
1174
1403
|
expect(message?.status).toEqual({
|
|
1175
1404
|
type: "requires-action",
|
|
@@ -1185,19 +1414,15 @@ describe("convertEveMessages", () => {
|
|
|
1185
1414
|
role: "assistant",
|
|
1186
1415
|
metadata: { status: "streaming" },
|
|
1187
1416
|
parts: [
|
|
1188
|
-
{
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
name: "github",
|
|
1192
|
-
|
|
1193
|
-
displayName: "GitHub",
|
|
1194
|
-
stepIndex: 0,
|
|
1195
|
-
turnId: "turn_1",
|
|
1196
|
-
},
|
|
1417
|
+
signInPart({
|
|
1418
|
+
kind: "sign-in",
|
|
1419
|
+
prompt: "Sign in to GitHub",
|
|
1420
|
+
signIn: { name: "github", displayName: "GitHub" },
|
|
1421
|
+
}),
|
|
1197
1422
|
],
|
|
1198
1423
|
},
|
|
1199
1424
|
],
|
|
1200
|
-
} satisfies
|
|
1425
|
+
} satisfies MessageData;
|
|
1201
1426
|
|
|
1202
1427
|
const [message] = convertEveMessages(data, { isRunning: false });
|
|
1203
1428
|
|
|
@@ -1215,27 +1440,16 @@ describe("convertEveMessages", () => {
|
|
|
1215
1440
|
role: "assistant",
|
|
1216
1441
|
metadata: { status: "streaming" },
|
|
1217
1442
|
parts: [
|
|
1218
|
-
{
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
approval: { id: "req_1" },
|
|
1225
|
-
},
|
|
1226
|
-
{
|
|
1227
|
-
type: "authorization",
|
|
1228
|
-
state: "required",
|
|
1229
|
-
name: "github",
|
|
1230
|
-
description: "Sign in to GitHub",
|
|
1231
|
-
displayName: "GitHub",
|
|
1232
|
-
stepIndex: 0,
|
|
1233
|
-
turnId: "turn_1",
|
|
1234
|
-
},
|
|
1443
|
+
toolPart({ interaction: approvalInteraction() }),
|
|
1444
|
+
signInPart({
|
|
1445
|
+
kind: "sign-in",
|
|
1446
|
+
prompt: "Sign in to GitHub",
|
|
1447
|
+
signIn: { name: "github", displayName: "GitHub" },
|
|
1448
|
+
}),
|
|
1235
1449
|
],
|
|
1236
1450
|
},
|
|
1237
1451
|
],
|
|
1238
|
-
} satisfies
|
|
1452
|
+
} satisfies MessageData;
|
|
1239
1453
|
|
|
1240
1454
|
const [message] = convertEveMessages(data, { isRunning: false });
|
|
1241
1455
|
|
|
@@ -1246,52 +1460,26 @@ describe("convertEveMessages", () => {
|
|
|
1246
1460
|
});
|
|
1247
1461
|
|
|
1248
1462
|
it("settles a cancelled turn with a pending authorization", () => {
|
|
1249
|
-
const
|
|
1250
|
-
const
|
|
1251
|
-
{
|
|
1252
|
-
type: "authorization.required",
|
|
1253
|
-
meta: eventMeta(0),
|
|
1254
|
-
data: {
|
|
1255
|
-
turnId: "turn_1",
|
|
1256
|
-
stepIndex: 0,
|
|
1257
|
-
sequence: 0,
|
|
1258
|
-
name: "github",
|
|
1259
|
-
description: "Sign in to GitHub",
|
|
1260
|
-
},
|
|
1261
|
-
},
|
|
1262
|
-
{
|
|
1263
|
-
type: "turn.cancelled",
|
|
1264
|
-
meta: eventMeta(1),
|
|
1265
|
-
data: { turnId: "turn_1", sequence: 1 },
|
|
1266
|
-
},
|
|
1267
|
-
];
|
|
1268
|
-
const pendingData = events
|
|
1269
|
-
.slice(0, 1)
|
|
1270
|
-
.reduce(
|
|
1271
|
-
(state, event) => reducer.reduce(state, event),
|
|
1272
|
-
reducer.initial(),
|
|
1273
|
-
);
|
|
1463
|
+
const events = [...initialTurnEvents, signInOpened("github")];
|
|
1464
|
+
const pendingData = replay(events);
|
|
1274
1465
|
const pendingAssistant = pendingData.messages.find(
|
|
1275
1466
|
(message) => message.role === "assistant",
|
|
1276
1467
|
);
|
|
1277
1468
|
expect(pendingAssistant?.metadata?.status).toBe("streaming");
|
|
1278
1469
|
expect(
|
|
1279
1470
|
convertEveMessages(pendingData, { isRunning: false }).at(-1)?.status,
|
|
1280
|
-
).toEqual({
|
|
1281
|
-
type: "requires-action",
|
|
1282
|
-
reason: "interrupt",
|
|
1283
|
-
});
|
|
1284
|
-
|
|
1285
|
-
const data = events
|
|
1286
|
-
.slice(1)
|
|
1287
|
-
.reduce((state, event) => reducer.reduce(state, event), pendingData);
|
|
1288
|
-
|
|
1289
|
-
const [message] = convertEveMessages(data, { isRunning: false });
|
|
1471
|
+
).toEqual({ type: "requires-action", reason: "interrupt" });
|
|
1290
1472
|
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1473
|
+
const data = replay([
|
|
1474
|
+
...events,
|
|
1475
|
+
{
|
|
1476
|
+
type: "turn.settled",
|
|
1477
|
+
data: { turnId: "turn_1", outcome: "cancelled" },
|
|
1478
|
+
},
|
|
1479
|
+
]);
|
|
1480
|
+
const message = convertEveMessages(data, { isRunning: false }).at(-1);
|
|
1481
|
+
|
|
1482
|
+
expect(message?.status).toEqual({ type: "complete", reason: "stop" });
|
|
1295
1483
|
});
|
|
1296
1484
|
|
|
1297
1485
|
it("reports a failed turn with a pending authorization", () => {
|
|
@@ -1302,19 +1490,15 @@ describe("convertEveMessages", () => {
|
|
|
1302
1490
|
role: "assistant",
|
|
1303
1491
|
metadata: { status: "streaming" },
|
|
1304
1492
|
parts: [
|
|
1305
|
-
{
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
name: "github",
|
|
1309
|
-
|
|
1310
|
-
displayName: "GitHub",
|
|
1311
|
-
stepIndex: 0,
|
|
1312
|
-
turnId: "turn_1",
|
|
1313
|
-
},
|
|
1493
|
+
signInPart({
|
|
1494
|
+
kind: "sign-in",
|
|
1495
|
+
prompt: "Sign in to GitHub",
|
|
1496
|
+
signIn: { name: "github", displayName: "GitHub" },
|
|
1497
|
+
}),
|
|
1314
1498
|
],
|
|
1315
1499
|
},
|
|
1316
1500
|
],
|
|
1317
|
-
} satisfies
|
|
1501
|
+
} satisfies MessageData;
|
|
1318
1502
|
|
|
1319
1503
|
const error = new Error("authorization failed");
|
|
1320
1504
|
const [message] = convertEveMessages(data, {
|
|
@@ -1340,15 +1524,11 @@ describe("convertEveMessages", () => {
|
|
|
1340
1524
|
role: "assistant",
|
|
1341
1525
|
metadata: { status: "streaming" },
|
|
1342
1526
|
parts: [
|
|
1343
|
-
{
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
name: "github",
|
|
1347
|
-
|
|
1348
|
-
displayName: "GitHub",
|
|
1349
|
-
stepIndex: 0,
|
|
1350
|
-
turnId: "turn_1",
|
|
1351
|
-
},
|
|
1527
|
+
signInPart({
|
|
1528
|
+
kind: "sign-in",
|
|
1529
|
+
prompt: "Sign in to GitHub",
|
|
1530
|
+
signIn: { name: "github", displayName: "GitHub" },
|
|
1531
|
+
}),
|
|
1352
1532
|
],
|
|
1353
1533
|
},
|
|
1354
1534
|
{
|
|
@@ -1358,7 +1538,7 @@ describe("convertEveMessages", () => {
|
|
|
1358
1538
|
parts: [{ type: "text", text: "Later turn" }],
|
|
1359
1539
|
},
|
|
1360
1540
|
],
|
|
1361
|
-
} satisfies
|
|
1541
|
+
} satisfies MessageData;
|
|
1362
1542
|
|
|
1363
1543
|
const [authorization] = convertEveMessages(data, {
|
|
1364
1544
|
isRunning: false,
|
|
@@ -1379,93 +1559,70 @@ describe("convertEveMessages", () => {
|
|
|
1379
1559
|
role: "assistant",
|
|
1380
1560
|
metadata: { status: "complete" },
|
|
1381
1561
|
parts: [
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
outcome: "authorized",
|
|
1391
|
-
},
|
|
1562
|
+
signInPart(
|
|
1563
|
+
{
|
|
1564
|
+
kind: "sign-in",
|
|
1565
|
+
prompt: "Sign in to GitHub",
|
|
1566
|
+
signIn: { name: "github", displayName: "GitHub" },
|
|
1567
|
+
},
|
|
1568
|
+
{ status: "settled", outcome: "accepted" },
|
|
1569
|
+
),
|
|
1392
1570
|
{ type: "text", text: "Done" },
|
|
1393
1571
|
],
|
|
1394
1572
|
},
|
|
1395
1573
|
],
|
|
1396
|
-
} satisfies
|
|
1574
|
+
} satisfies MessageData;
|
|
1397
1575
|
|
|
1398
1576
|
const [message] = convertEveMessages(data, { isRunning: false });
|
|
1399
1577
|
|
|
1400
1578
|
expect(message?.status).toEqual({ type: "complete", reason: "stop" });
|
|
1401
1579
|
});
|
|
1402
1580
|
|
|
1403
|
-
describe("contract with eve's
|
|
1404
|
-
const
|
|
1405
|
-
|
|
1406
|
-
return events.reduce(
|
|
1407
|
-
(state, event) => reducer.reduce(state, event),
|
|
1408
|
-
reducer.initial(),
|
|
1409
|
-
);
|
|
1410
|
-
};
|
|
1411
|
-
|
|
1412
|
-
const midStreamEvents: readonly EveAgentReducerEvent[] = [
|
|
1413
|
-
{
|
|
1414
|
-
type: "client.message.submitted",
|
|
1415
|
-
data: { submissionId: "sub_1", message: "hi", createdAt: 0 },
|
|
1416
|
-
},
|
|
1417
|
-
{
|
|
1418
|
-
type: "turn.started",
|
|
1419
|
-
meta: eventMeta(0),
|
|
1420
|
-
data: { turnId: "turn_1", sequence: 0 },
|
|
1421
|
-
},
|
|
1422
|
-
{
|
|
1423
|
-
type: "step.started",
|
|
1424
|
-
meta: eventMeta(1),
|
|
1425
|
-
data: {
|
|
1426
|
-
turnId: "turn_1",
|
|
1427
|
-
stepIndex: 0,
|
|
1428
|
-
sequence: 1,
|
|
1429
|
-
modelId: "test-model",
|
|
1430
|
-
},
|
|
1431
|
-
},
|
|
1581
|
+
describe("contract with eve's conversation projection", () => {
|
|
1582
|
+
const midStreamEvents: readonly SessionEvent[] = [
|
|
1583
|
+
...initialTurnEvents,
|
|
1432
1584
|
{
|
|
1433
|
-
type: "
|
|
1434
|
-
|
|
1435
|
-
data: {
|
|
1436
|
-
turnId: "turn_1",
|
|
1437
|
-
stepIndex: 0,
|
|
1438
|
-
sequence: 2,
|
|
1439
|
-
messageDelta: "Let me th",
|
|
1440
|
-
},
|
|
1585
|
+
type: "content.delta",
|
|
1586
|
+
scope: { turnId: "turn_1", runId: "run_1" },
|
|
1587
|
+
data: { partId: "part_1", kind: "text", delta: "Let me th" },
|
|
1441
1588
|
},
|
|
1442
1589
|
];
|
|
1443
1590
|
|
|
1591
|
+
const completedContent = (
|
|
1592
|
+
kind: "text" | "reasoning",
|
|
1593
|
+
partId: string,
|
|
1594
|
+
value: string,
|
|
1595
|
+
runId = "run_1",
|
|
1596
|
+
): SessionEvent => ({
|
|
1597
|
+
type: "content.completed",
|
|
1598
|
+
scope: { turnId: "turn_1", runId },
|
|
1599
|
+
data: { partId, runId, kind, value, phase: "narration" },
|
|
1600
|
+
});
|
|
1601
|
+
|
|
1602
|
+
const callRequested = {
|
|
1603
|
+
type: "call.requested",
|
|
1604
|
+
scope: { turnId: "turn_1", runId: "run_1" },
|
|
1605
|
+
data: {
|
|
1606
|
+
callId: "call_1",
|
|
1607
|
+
owner: { runId: "run_1" },
|
|
1608
|
+
capability: { kind: "tool", name: "search" },
|
|
1609
|
+
input: {},
|
|
1610
|
+
},
|
|
1611
|
+
} as const satisfies SessionEvent;
|
|
1612
|
+
|
|
1444
1613
|
it("carries the challenge Eve projects onto an authorization part", () => {
|
|
1445
1614
|
const state = replay([
|
|
1446
1615
|
...midStreamEvents,
|
|
1447
|
-
{
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
name: "github",
|
|
1455
|
-
description: "Authorization required for github",
|
|
1456
|
-
authorization: {
|
|
1457
|
-
displayName: "GitHub",
|
|
1458
|
-
instructions: "Enter the code on the device page",
|
|
1459
|
-
url: "https://github.com/login/device",
|
|
1460
|
-
userCode: "ABCD-1234",
|
|
1461
|
-
expiresAt: "2026-01-01T00:00:00Z",
|
|
1462
|
-
},
|
|
1463
|
-
},
|
|
1464
|
-
},
|
|
1616
|
+
signInOpened("github", {
|
|
1617
|
+
displayName: "GitHub",
|
|
1618
|
+
instructions: "Enter the code on the device page",
|
|
1619
|
+
url: "https://github.com/login/device",
|
|
1620
|
+
userCode: "ABCD-1234",
|
|
1621
|
+
expiresAt: "2026-01-01T00:00:00Z",
|
|
1622
|
+
}),
|
|
1465
1623
|
]);
|
|
1466
1624
|
|
|
1467
1625
|
const converted = convertEveMessages(state, { isRunning: false });
|
|
1468
|
-
|
|
1469
1626
|
expect(converted.at(-1)?.content).toContainEqual({
|
|
1470
1627
|
type: "data",
|
|
1471
1628
|
name: "authorization",
|
|
@@ -1485,39 +1642,19 @@ describe("convertEveMessages", () => {
|
|
|
1485
1642
|
it("releases the hold when Eve settles the authorization part in place", () => {
|
|
1486
1643
|
const state = replay([
|
|
1487
1644
|
...midStreamEvents,
|
|
1645
|
+
signInOpened("github"),
|
|
1488
1646
|
{
|
|
1489
|
-
type: "
|
|
1490
|
-
|
|
1491
|
-
data: {
|
|
1492
|
-
turnId: "turn_1",
|
|
1493
|
-
stepIndex: 0,
|
|
1494
|
-
sequence: 3,
|
|
1495
|
-
name: "github",
|
|
1496
|
-
description: "Authorization required for github",
|
|
1497
|
-
},
|
|
1498
|
-
},
|
|
1499
|
-
{
|
|
1500
|
-
type: "authorization.completed",
|
|
1501
|
-
meta: eventMeta(4),
|
|
1502
|
-
data: {
|
|
1503
|
-
turnId: "turn_1",
|
|
1504
|
-
stepIndex: 0,
|
|
1505
|
-
sequence: 4,
|
|
1506
|
-
name: "github",
|
|
1507
|
-
outcome: "authorized",
|
|
1508
|
-
},
|
|
1647
|
+
type: "interaction.settled",
|
|
1648
|
+
data: { interactionId: "auth_github", outcome: "accepted" },
|
|
1509
1649
|
},
|
|
1510
1650
|
]);
|
|
1511
1651
|
|
|
1512
1652
|
const assistant = state.messages.find((m) => m.role === "assistant");
|
|
1513
1653
|
const authorizationParts = assistant?.parts.filter(
|
|
1514
|
-
(part) => part.type === "
|
|
1654
|
+
(part) => part.type === "interaction",
|
|
1515
1655
|
);
|
|
1516
1656
|
expect(authorizationParts).toHaveLength(1);
|
|
1517
|
-
expect(authorizationParts?.[0]).toMatchObject({
|
|
1518
|
-
|
|
1519
|
-
// Eve leaves the streaming marker set, so the released turn falls back
|
|
1520
|
-
// to the stale-marker mapping rather than to a terminal status.
|
|
1657
|
+
expect(authorizationParts?.[0]).toMatchObject({ status: "settled" });
|
|
1521
1658
|
expect(
|
|
1522
1659
|
convertEveMessages(state, { isRunning: false }).at(-1)?.status,
|
|
1523
1660
|
).toEqual({ type: "incomplete", reason: "cancelled" });
|
|
@@ -1526,38 +1663,11 @@ describe("convertEveMessages", () => {
|
|
|
1526
1663
|
it("keeps the hold while another connector is still required", () => {
|
|
1527
1664
|
const state = replay([
|
|
1528
1665
|
...midStreamEvents,
|
|
1666
|
+
signInOpened("github"),
|
|
1667
|
+
signInOpened("slack"),
|
|
1529
1668
|
{
|
|
1530
|
-
type: "
|
|
1531
|
-
|
|
1532
|
-
data: {
|
|
1533
|
-
turnId: "turn_1",
|
|
1534
|
-
stepIndex: 0,
|
|
1535
|
-
sequence: 3,
|
|
1536
|
-
name: "github",
|
|
1537
|
-
description: "Authorization required for github",
|
|
1538
|
-
},
|
|
1539
|
-
},
|
|
1540
|
-
{
|
|
1541
|
-
type: "authorization.required",
|
|
1542
|
-
meta: eventMeta(4),
|
|
1543
|
-
data: {
|
|
1544
|
-
turnId: "turn_1",
|
|
1545
|
-
stepIndex: 0,
|
|
1546
|
-
sequence: 4,
|
|
1547
|
-
name: "slack",
|
|
1548
|
-
description: "Authorization required for slack",
|
|
1549
|
-
},
|
|
1550
|
-
},
|
|
1551
|
-
{
|
|
1552
|
-
type: "authorization.completed",
|
|
1553
|
-
meta: eventMeta(5),
|
|
1554
|
-
data: {
|
|
1555
|
-
turnId: "turn_1",
|
|
1556
|
-
stepIndex: 0,
|
|
1557
|
-
sequence: 5,
|
|
1558
|
-
name: "github",
|
|
1559
|
-
outcome: "authorized",
|
|
1560
|
-
},
|
|
1669
|
+
type: "interaction.settled",
|
|
1670
|
+
data: { interactionId: "auth_github", outcome: "accepted" },
|
|
1561
1671
|
},
|
|
1562
1672
|
]);
|
|
1563
1673
|
|
|
@@ -1567,51 +1677,41 @@ describe("convertEveMessages", () => {
|
|
|
1567
1677
|
});
|
|
1568
1678
|
|
|
1569
1679
|
it("projects the input request onto the approval part it gates", () => {
|
|
1570
|
-
const events: readonly
|
|
1680
|
+
const events: readonly SessionEvent[] = [
|
|
1571
1681
|
...midStreamEvents,
|
|
1572
1682
|
{
|
|
1573
|
-
|
|
1683
|
+
...callRequested,
|
|
1574
1684
|
data: {
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
},
|
|
1591
|
-
],
|
|
1685
|
+
...callRequested.data,
|
|
1686
|
+
capability: { kind: "tool", name: "ask_question" },
|
|
1687
|
+
},
|
|
1688
|
+
},
|
|
1689
|
+
{
|
|
1690
|
+
type: "interaction.opened",
|
|
1691
|
+
scope: { turnId: "turn_1" },
|
|
1692
|
+
data: {
|
|
1693
|
+
interactionId: "req_1",
|
|
1694
|
+
subject: { callId: "call_1" },
|
|
1695
|
+
request: {
|
|
1696
|
+
kind: "question",
|
|
1697
|
+
prompt: "What should the subject line be?",
|
|
1698
|
+
display: "text",
|
|
1699
|
+
},
|
|
1592
1700
|
},
|
|
1593
|
-
meta: eventMeta(3),
|
|
1594
1701
|
},
|
|
1595
1702
|
];
|
|
1596
|
-
|
|
1597
|
-
// Rehydration replays the *stored* event log, which has been through
|
|
1598
|
-
// the wire and back, so the reloaded case is the serialized payload
|
|
1599
|
-
// rather than the same objects a second time.
|
|
1600
|
-
const rehydrated = JSON.parse(
|
|
1601
|
-
JSON.stringify(events),
|
|
1602
|
-
) as readonly EveAgentReducerEvent[];
|
|
1703
|
+
const rehydrated = JSON.parse(JSON.stringify(events)) as SessionEvent[];
|
|
1603
1704
|
|
|
1604
1705
|
for (const state of [replay(events), replay(rehydrated)]) {
|
|
1605
1706
|
const part = state.messages
|
|
1606
1707
|
.find((message) => message.role === "assistant")
|
|
1607
|
-
?.parts.find((candidate) => candidate.type === "
|
|
1708
|
+
?.parts.find((candidate) => candidate.type === "tool");
|
|
1608
1709
|
|
|
1609
1710
|
expect(part).toMatchObject({
|
|
1610
|
-
|
|
1611
|
-
|
|
1711
|
+
type: "tool",
|
|
1712
|
+
interaction: { interactionId: "req_1", status: "open" },
|
|
1612
1713
|
});
|
|
1613
1714
|
expect(findEveInputRequest(state, "req_1")).toMatchObject({
|
|
1614
|
-
requestId: "req_1",
|
|
1615
1715
|
prompt: "What should the subject line be?",
|
|
1616
1716
|
display: "text",
|
|
1617
1717
|
});
|
|
@@ -1620,188 +1720,110 @@ describe("convertEveMessages", () => {
|
|
|
1620
1720
|
|
|
1621
1721
|
it("leaves a reasoning part unsettled when a tool call follows it", () => {
|
|
1622
1722
|
const state = replay([
|
|
1623
|
-
...
|
|
1723
|
+
...initialTurnEvents,
|
|
1624
1724
|
{
|
|
1625
|
-
type: "
|
|
1626
|
-
|
|
1627
|
-
data: {
|
|
1628
|
-
turnId: "turn_1",
|
|
1629
|
-
stepIndex: 0,
|
|
1630
|
-
sequence: 2,
|
|
1631
|
-
reasoningDelta: "Think",
|
|
1632
|
-
},
|
|
1633
|
-
},
|
|
1634
|
-
{
|
|
1635
|
-
type: "actions.requested",
|
|
1636
|
-
meta: eventMeta(3),
|
|
1637
|
-
data: {
|
|
1638
|
-
turnId: "turn_1",
|
|
1639
|
-
stepIndex: 0,
|
|
1640
|
-
sequence: 3,
|
|
1641
|
-
actions: [
|
|
1642
|
-
{
|
|
1643
|
-
kind: "tool-call",
|
|
1644
|
-
callId: "call_1",
|
|
1645
|
-
toolName: "search",
|
|
1646
|
-
input: {},
|
|
1647
|
-
},
|
|
1648
|
-
],
|
|
1649
|
-
},
|
|
1725
|
+
type: "content.delta",
|
|
1726
|
+
scope: { turnId: "turn_1", runId: "run_1" },
|
|
1727
|
+
data: { partId: "reason_1", kind: "reasoning", delta: "Think" },
|
|
1650
1728
|
},
|
|
1729
|
+
callRequested,
|
|
1651
1730
|
]);
|
|
1652
1731
|
|
|
1653
1732
|
const message = state.messages.find((m) => m.role === "assistant");
|
|
1654
1733
|
expect(message?.parts).toEqual(
|
|
1655
1734
|
expect.arrayContaining([
|
|
1656
1735
|
expect.objectContaining({ type: "reasoning", state: "streaming" }),
|
|
1657
|
-
expect.objectContaining({ type: "
|
|
1736
|
+
expect.objectContaining({ type: "tool" }),
|
|
1658
1737
|
]),
|
|
1659
1738
|
);
|
|
1660
|
-
|
|
1661
1739
|
const content = convertEveMessages(state, { isRunning: true }).at(
|
|
1662
1740
|
-1,
|
|
1663
1741
|
)?.content;
|
|
1664
1742
|
expect(content).toEqual([
|
|
1665
|
-
expect.objectContaining({ type: "reasoning" }),
|
|
1666
1743
|
expect.objectContaining({ type: "tool-call" }),
|
|
1744
|
+
expect.objectContaining({ type: "reasoning" }),
|
|
1667
1745
|
]);
|
|
1668
|
-
expect(content?.[
|
|
1746
|
+
expect(content?.[1]).not.toHaveProperty("status");
|
|
1669
1747
|
});
|
|
1670
1748
|
|
|
1671
1749
|
it("settles leftover reasoning after the model stream completes a tool call", () => {
|
|
1672
1750
|
const state = replay([
|
|
1673
|
-
...
|
|
1674
|
-
{
|
|
1675
|
-
type: "reasoning.appended",
|
|
1676
|
-
meta: eventMeta(2),
|
|
1677
|
-
data: {
|
|
1678
|
-
turnId: "turn_1",
|
|
1679
|
-
stepIndex: 0,
|
|
1680
|
-
sequence: 2,
|
|
1681
|
-
reasoningDelta: "Think",
|
|
1682
|
-
},
|
|
1683
|
-
},
|
|
1684
|
-
{
|
|
1685
|
-
type: "actions.requested",
|
|
1686
|
-
meta: eventMeta(3),
|
|
1687
|
-
data: {
|
|
1688
|
-
turnId: "turn_1",
|
|
1689
|
-
stepIndex: 0,
|
|
1690
|
-
sequence: 3,
|
|
1691
|
-
actions: [
|
|
1692
|
-
{
|
|
1693
|
-
kind: "tool-call",
|
|
1694
|
-
callId: "call_1",
|
|
1695
|
-
toolName: "search",
|
|
1696
|
-
input: {},
|
|
1697
|
-
},
|
|
1698
|
-
],
|
|
1699
|
-
},
|
|
1700
|
-
},
|
|
1751
|
+
...initialTurnEvents,
|
|
1701
1752
|
{
|
|
1702
|
-
type: "
|
|
1703
|
-
|
|
1704
|
-
data: {
|
|
1705
|
-
turnId: "turn_1",
|
|
1706
|
-
stepIndex: 0,
|
|
1707
|
-
sequence: 4,
|
|
1708
|
-
reasoning: "Think",
|
|
1709
|
-
},
|
|
1753
|
+
type: "content.delta",
|
|
1754
|
+
scope: { turnId: "turn_1", runId: "run_1" },
|
|
1755
|
+
data: { partId: "reason_1", kind: "reasoning", delta: "Think" },
|
|
1710
1756
|
},
|
|
1757
|
+
callRequested,
|
|
1758
|
+
completedContent("reasoning", "reason_1", "Think"),
|
|
1711
1759
|
]);
|
|
1712
1760
|
|
|
1713
1761
|
const message = state.messages.find((m) => m.role === "assistant");
|
|
1714
1762
|
expect(message?.parts).toEqual(
|
|
1715
1763
|
expect.arrayContaining([
|
|
1716
1764
|
expect.objectContaining({ type: "reasoning", state: "done" }),
|
|
1717
|
-
expect.objectContaining({ type: "
|
|
1765
|
+
expect.objectContaining({ type: "tool" }),
|
|
1718
1766
|
]),
|
|
1719
1767
|
);
|
|
1720
|
-
|
|
1721
1768
|
const content = convertEveMessages(state, { isRunning: true }).at(
|
|
1722
1769
|
-1,
|
|
1723
1770
|
)?.content;
|
|
1724
1771
|
expect(content).toEqual([
|
|
1772
|
+
expect.objectContaining({ type: "tool-call" }),
|
|
1725
1773
|
expect.objectContaining({
|
|
1726
1774
|
type: "reasoning",
|
|
1727
1775
|
status: { type: "complete" },
|
|
1728
1776
|
}),
|
|
1729
|
-
expect.objectContaining({ type: "tool-call" }),
|
|
1730
1777
|
]);
|
|
1731
1778
|
});
|
|
1732
1779
|
|
|
1733
1780
|
it("keeps a later step's live text part unsettled after an earlier step completed", () => {
|
|
1734
1781
|
const state = replay([
|
|
1735
|
-
...
|
|
1782
|
+
...initialTurnEvents,
|
|
1736
1783
|
{
|
|
1737
|
-
type: "
|
|
1738
|
-
|
|
1739
|
-
data: {
|
|
1740
|
-
turnId: "turn_1",
|
|
1741
|
-
stepIndex: 0,
|
|
1742
|
-
sequence: 2,
|
|
1743
|
-
messageDelta: "First",
|
|
1744
|
-
},
|
|
1784
|
+
type: "content.delta",
|
|
1785
|
+
scope: { turnId: "turn_1", runId: "run_1" },
|
|
1786
|
+
data: { partId: "part_1", kind: "text", delta: "First" },
|
|
1745
1787
|
},
|
|
1788
|
+
completedContent("text", "part_1", "First"),
|
|
1746
1789
|
{
|
|
1747
|
-
type: "
|
|
1748
|
-
meta: eventMeta(3),
|
|
1790
|
+
type: "model.settled",
|
|
1749
1791
|
data: {
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
sequence: 3,
|
|
1792
|
+
runId: "run_1",
|
|
1793
|
+
outcome: "completed",
|
|
1753
1794
|
finishReason: "tool-calls",
|
|
1754
|
-
message: "First",
|
|
1755
1795
|
},
|
|
1756
1796
|
},
|
|
1757
1797
|
{
|
|
1758
|
-
type: "
|
|
1759
|
-
|
|
1760
|
-
data: {
|
|
1761
|
-
turnId: "turn_1",
|
|
1762
|
-
stepIndex: 0,
|
|
1763
|
-
sequence: 4,
|
|
1764
|
-
finishReason: "tool-calls",
|
|
1765
|
-
},
|
|
1798
|
+
type: "model.requested",
|
|
1799
|
+
scope: { turnId: "turn_1" },
|
|
1800
|
+
data: { runId: "run_2", owner: { turnId: "turn_1" } },
|
|
1766
1801
|
},
|
|
1767
1802
|
{
|
|
1768
|
-
type: "
|
|
1769
|
-
|
|
1770
|
-
data: {
|
|
1771
|
-
turnId: "turn_1",
|
|
1772
|
-
stepIndex: 1,
|
|
1773
|
-
sequence: 5,
|
|
1774
|
-
modelId: "test-model",
|
|
1775
|
-
},
|
|
1803
|
+
type: "model.started",
|
|
1804
|
+
scope: { turnId: "turn_1", runId: "run_2" },
|
|
1805
|
+
data: { runId: "run_2", modelId: "test-model", attempt: 0 },
|
|
1776
1806
|
},
|
|
1777
1807
|
{
|
|
1778
|
-
type: "
|
|
1779
|
-
|
|
1780
|
-
data: {
|
|
1781
|
-
turnId: "turn_1",
|
|
1782
|
-
stepIndex: 1,
|
|
1783
|
-
sequence: 6,
|
|
1784
|
-
messageDelta: "Sec",
|
|
1785
|
-
},
|
|
1808
|
+
type: "content.delta",
|
|
1809
|
+
scope: { turnId: "turn_1", runId: "run_2" },
|
|
1810
|
+
data: { partId: "part_2", kind: "text", delta: "Sec" },
|
|
1786
1811
|
},
|
|
1787
1812
|
]);
|
|
1788
1813
|
|
|
1789
1814
|
const assistant = state.messages.find((m) => m.role === "assistant");
|
|
1790
1815
|
expect(assistant?.parts).toEqual([
|
|
1791
|
-
expect.objectContaining({ type: "step-start" }),
|
|
1792
1816
|
expect.objectContaining({
|
|
1793
1817
|
type: "text",
|
|
1794
1818
|
text: "First",
|
|
1795
1819
|
state: "done",
|
|
1796
1820
|
}),
|
|
1797
|
-
expect.objectContaining({ type: "step-start" }),
|
|
1798
1821
|
expect.objectContaining({
|
|
1799
1822
|
type: "text",
|
|
1800
1823
|
text: "Sec",
|
|
1801
1824
|
state: "streaming",
|
|
1802
1825
|
}),
|
|
1803
1826
|
]);
|
|
1804
|
-
|
|
1805
1827
|
const content = convertEveMessages(state, { isRunning: true }).at(
|
|
1806
1828
|
-1,
|
|
1807
1829
|
)?.content;
|
|
@@ -1818,59 +1840,68 @@ describe("convertEveMessages", () => {
|
|
|
1818
1840
|
|
|
1819
1841
|
it("a locally aborted turn keeps its streaming marker and converts to cancelled", () => {
|
|
1820
1842
|
const state = replay(midStreamEvents);
|
|
1821
|
-
|
|
1822
1843
|
const assistant = state.messages.find((m) => m.role === "assistant");
|
|
1823
1844
|
expect(assistant?.metadata?.status).toBe("streaming");
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1845
|
+
expect(
|
|
1846
|
+
convertEveMessages(state, { isRunning: false }).at(-1)?.status,
|
|
1847
|
+
).toEqual({
|
|
1827
1848
|
type: "incomplete",
|
|
1828
1849
|
reason: "cancelled",
|
|
1829
1850
|
});
|
|
1830
1851
|
});
|
|
1831
1852
|
|
|
1832
1853
|
it("a failed session keeps its streaming marker and converts to error", () => {
|
|
1833
|
-
const
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1854
|
+
const sessionFailure = {
|
|
1855
|
+
type: "session.ended",
|
|
1856
|
+
meta: eventMeta(midStreamEvents.length),
|
|
1857
|
+
data: {
|
|
1858
|
+
outcome: "failed",
|
|
1859
|
+
cause: { turnId: "turn_1" },
|
|
1860
|
+
error: { code: "internal", message: "boom" },
|
|
1839
1861
|
},
|
|
1840
|
-
|
|
1841
|
-
|
|
1862
|
+
} as const satisfies SessionStreamEvent;
|
|
1863
|
+
const state = replay([...midStreamEvents, sessionFailure]);
|
|
1842
1864
|
const assistant = state.messages.find((m) => m.role === "assistant");
|
|
1843
1865
|
expect(assistant?.metadata?.status).toBe("streaming");
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1866
|
+
expect(
|
|
1867
|
+
convertEveMessages(state, {
|
|
1868
|
+
isRunning: false,
|
|
1869
|
+
error: new Error("boom"),
|
|
1870
|
+
}).at(-1)?.status,
|
|
1871
|
+
).toEqual({
|
|
1850
1872
|
type: "incomplete",
|
|
1851
1873
|
reason: "error",
|
|
1852
1874
|
error: { code: "unknown", message: "boom" },
|
|
1853
1875
|
});
|
|
1876
|
+
expect(
|
|
1877
|
+
convertEveMessages(state, {
|
|
1878
|
+
isRunning: false,
|
|
1879
|
+
events: [sessionFailure],
|
|
1880
|
+
}).at(-1)?.status,
|
|
1881
|
+
).toEqual({
|
|
1882
|
+
type: "incomplete",
|
|
1883
|
+
reason: "error",
|
|
1884
|
+
error: { code: "internal", message: "boom" },
|
|
1885
|
+
});
|
|
1854
1886
|
});
|
|
1855
1887
|
|
|
1856
1888
|
it("a failed turn stays incomplete after Eve settles its message", () => {
|
|
1857
1889
|
const failureEvent = {
|
|
1858
|
-
type: "turn.
|
|
1859
|
-
meta: eventMeta(
|
|
1890
|
+
type: "turn.settled",
|
|
1891
|
+
meta: eventMeta(8),
|
|
1860
1892
|
data: {
|
|
1861
1893
|
turnId: "turn_1",
|
|
1862
|
-
|
|
1863
|
-
code: "internal",
|
|
1864
|
-
message: "boom",
|
|
1894
|
+
outcome: "failed",
|
|
1895
|
+
error: { code: "internal", message: "boom" },
|
|
1865
1896
|
},
|
|
1866
|
-
} as const satisfies
|
|
1897
|
+
} as const satisfies SessionStreamEvent;
|
|
1867
1898
|
const state = replay([...midStreamEvents, failureEvent]);
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1899
|
+
expect(
|
|
1900
|
+
convertEveMessages(state, {
|
|
1901
|
+
isRunning: false,
|
|
1902
|
+
events: [failureEvent],
|
|
1903
|
+
}).at(-1)?.status,
|
|
1904
|
+
).toEqual({
|
|
1874
1905
|
type: "incomplete",
|
|
1875
1906
|
reason: "error",
|
|
1876
1907
|
error: { code: "internal", message: "boom" },
|
|
@@ -1879,44 +1910,45 @@ describe("convertEveMessages", () => {
|
|
|
1879
1910
|
|
|
1880
1911
|
it("a cancelled turn stays cancelled after Eve settles its message", () => {
|
|
1881
1912
|
const cancelEvent = {
|
|
1882
|
-
type: "turn.
|
|
1883
|
-
meta: eventMeta(
|
|
1884
|
-
data: { turnId: "turn_1",
|
|
1885
|
-
} as const satisfies
|
|
1913
|
+
type: "turn.settled",
|
|
1914
|
+
meta: eventMeta(8),
|
|
1915
|
+
data: { turnId: "turn_1", outcome: "cancelled" },
|
|
1916
|
+
} as const satisfies SessionStreamEvent;
|
|
1886
1917
|
const state = replay([...midStreamEvents, cancelEvent]);
|
|
1887
|
-
|
|
1888
1918
|
expect(
|
|
1889
1919
|
convertEveMessages(state, { isRunning: false }).at(-1)?.status,
|
|
1890
|
-
).toEqual({
|
|
1920
|
+
).toEqual({
|
|
1921
|
+
type: "complete",
|
|
1922
|
+
reason: "stop",
|
|
1923
|
+
});
|
|
1891
1924
|
expect(
|
|
1892
1925
|
convertEveMessages(state, {
|
|
1893
1926
|
isRunning: false,
|
|
1894
1927
|
events: [cancelEvent],
|
|
1895
1928
|
}).at(-1)?.status,
|
|
1896
|
-
).toEqual({
|
|
1929
|
+
).toEqual({
|
|
1930
|
+
type: "incomplete",
|
|
1931
|
+
reason: "cancelled",
|
|
1932
|
+
});
|
|
1897
1933
|
});
|
|
1898
1934
|
|
|
1899
1935
|
it("a completed turn terminalizes the streaming marker and converts to complete", () => {
|
|
1900
1936
|
const state = replay([
|
|
1901
1937
|
...midStreamEvents,
|
|
1938
|
+
completedContent("text", "part_1", "Let me think"),
|
|
1902
1939
|
{
|
|
1903
|
-
type: "
|
|
1904
|
-
meta: eventMeta(3),
|
|
1940
|
+
type: "model.settled",
|
|
1905
1941
|
data: {
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
sequence: 3,
|
|
1942
|
+
runId: "run_1",
|
|
1943
|
+
outcome: "completed",
|
|
1909
1944
|
finishReason: "stop",
|
|
1910
|
-
message: "Let me think",
|
|
1911
1945
|
},
|
|
1912
1946
|
},
|
|
1913
1947
|
{
|
|
1914
|
-
type: "turn.
|
|
1915
|
-
|
|
1916
|
-
data: { turnId: "turn_1", sequence: 4 },
|
|
1948
|
+
type: "turn.settled",
|
|
1949
|
+
data: { turnId: "turn_1", outcome: "completed", reply: ["part_1"] },
|
|
1917
1950
|
},
|
|
1918
1951
|
]);
|
|
1919
|
-
|
|
1920
1952
|
const assistant = state.messages.find((m) => m.role === "assistant");
|
|
1921
1953
|
expect(assistant?.metadata?.status).toBe("complete");
|
|
1922
1954
|
expect(assistant?.parts).toEqual(
|
|
@@ -1924,7 +1956,6 @@ describe("convertEveMessages", () => {
|
|
|
1924
1956
|
expect.objectContaining({ type: "text", state: "done" }),
|
|
1925
1957
|
]),
|
|
1926
1958
|
);
|
|
1927
|
-
|
|
1928
1959
|
const converted = convertEveMessages(state, { isRunning: false });
|
|
1929
1960
|
expect(converted.at(-1)?.status).toEqual({
|
|
1930
1961
|
type: "complete",
|
|
@@ -1952,7 +1983,7 @@ describe("convertEveMessages", () => {
|
|
|
1952
1983
|
parts: [{ type: "text", text: "Hello" }],
|
|
1953
1984
|
},
|
|
1954
1985
|
],
|
|
1955
|
-
} satisfies
|
|
1986
|
+
} satisfies MessageData;
|
|
1956
1987
|
|
|
1957
1988
|
const [message] = convertEveMessages(data, {
|
|
1958
1989
|
getCreatedAt: () => createdAt,
|
|
@@ -1982,11 +2013,42 @@ describe("getEveMessageContent", () => {
|
|
|
1982
2013
|
expect(getEveMessageContent(message)).toBe("Hello");
|
|
1983
2014
|
});
|
|
1984
2015
|
|
|
2016
|
+
it("preserves multiple text parts without attachments", () => {
|
|
2017
|
+
const message = {
|
|
2018
|
+
role: "user",
|
|
2019
|
+
createdAt: new Date(),
|
|
2020
|
+
parentId: null,
|
|
2021
|
+
sourceId: null,
|
|
2022
|
+
runConfig: undefined,
|
|
2023
|
+
metadata: { custom: {} },
|
|
2024
|
+
content: [
|
|
2025
|
+
{ type: "text", text: "First" },
|
|
2026
|
+
{ type: "text", text: "Second" },
|
|
2027
|
+
],
|
|
2028
|
+
} satisfies AppendMessage;
|
|
2029
|
+
|
|
2030
|
+
expect(getEveMessageContent(message)).toEqual([
|
|
2031
|
+
{ type: "text", text: "First" },
|
|
2032
|
+
{ type: "text", text: "Second" },
|
|
2033
|
+
]);
|
|
2034
|
+
});
|
|
2035
|
+
|
|
2036
|
+
it("rejects an unsupported append part instead of sending it to Eve", () => {
|
|
2037
|
+
const message = {
|
|
2038
|
+
...baseAppendMessage,
|
|
2039
|
+
content: [{ type: "reasoning", text: "Private reasoning" }],
|
|
2040
|
+
} as AppendMessage;
|
|
2041
|
+
|
|
2042
|
+
expect(() => getEveMessageContent(message)).toThrow(
|
|
2043
|
+
"Unsupported eve append message part type: reasoning",
|
|
2044
|
+
);
|
|
2045
|
+
});
|
|
2046
|
+
|
|
1985
2047
|
it("declares the data URL subtype of an image part", () => {
|
|
1986
2048
|
const message = {
|
|
1987
2049
|
...baseAppendMessage,
|
|
1988
2050
|
content: [{ type: "image", image: "data:image/jpeg;base64,/9j/4AAQ" }],
|
|
1989
|
-
} as
|
|
2051
|
+
} as AppendMessage;
|
|
1990
2052
|
|
|
1991
2053
|
expect(getEveMessageContent(message)).toEqual([
|
|
1992
2054
|
{
|
|
@@ -2007,7 +2069,7 @@ describe("getEveMessageContent", () => {
|
|
|
2007
2069
|
"data:application/octet-stream;base64,iVBORw0KGgoAAAANSUhEUgAAAAE=",
|
|
2008
2070
|
},
|
|
2009
2071
|
],
|
|
2010
|
-
} as
|
|
2072
|
+
} as AppendMessage;
|
|
2011
2073
|
|
|
2012
2074
|
expect(getEveMessageContent(message)).toEqual([
|
|
2013
2075
|
{
|
|
@@ -2028,7 +2090,7 @@ describe("getEveMessageContent", () => {
|
|
|
2028
2090
|
filename: "photo",
|
|
2029
2091
|
},
|
|
2030
2092
|
],
|
|
2031
|
-
} as
|
|
2093
|
+
} as AppendMessage;
|
|
2032
2094
|
|
|
2033
2095
|
expect(getEveMessageContent(message)).toEqual([
|
|
2034
2096
|
{
|
|
@@ -2054,7 +2116,7 @@ describe("getEveMessageContent", () => {
|
|
|
2054
2116
|
status: { type: "complete" },
|
|
2055
2117
|
},
|
|
2056
2118
|
],
|
|
2057
|
-
} as
|
|
2119
|
+
} as AppendMessage;
|
|
2058
2120
|
|
|
2059
2121
|
expect(getEveMessageContent(message)).toEqual([
|
|
2060
2122
|
{
|
|
@@ -2154,7 +2216,7 @@ describe("getEveMessageContent", () => {
|
|
|
2154
2216
|
const message = {
|
|
2155
2217
|
...baseAppendMessage,
|
|
2156
2218
|
content: [{ type: "audio", audio: { data: "QUJD", format: "mp3" } }],
|
|
2157
|
-
} as
|
|
2219
|
+
} as AppendMessage;
|
|
2158
2220
|
|
|
2159
2221
|
expect(getEveMessageContent(message)).toEqual([
|
|
2160
2222
|
{
|
|
@@ -2169,7 +2231,7 @@ describe("getEveMessageContent", () => {
|
|
|
2169
2231
|
const message = {
|
|
2170
2232
|
...baseAppendMessage,
|
|
2171
2233
|
content: [{ type: "audio", audio: { data: "QUJD", format: "wav" } }],
|
|
2172
|
-
} as
|
|
2234
|
+
} as AppendMessage;
|
|
2173
2235
|
|
|
2174
2236
|
expect(getEveMessageContent(message)).toEqual([
|
|
2175
2237
|
{
|
|
@@ -2189,7 +2251,7 @@ describe("getEveMessageContent", () => {
|
|
|
2189
2251
|
audio: { data: "data:audio/mpeg;base64,QUJD", format: "mp3" },
|
|
2190
2252
|
},
|
|
2191
2253
|
],
|
|
2192
|
-
} as
|
|
2254
|
+
} as AppendMessage;
|
|
2193
2255
|
|
|
2194
2256
|
expect(getEveMessageContent(message)).toEqual([
|
|
2195
2257
|
{
|
|
@@ -2209,7 +2271,7 @@ describe("getEveMessageContent", () => {
|
|
|
2209
2271
|
audio: { data: "data:;base64,QUJD", format: "mp3" },
|
|
2210
2272
|
},
|
|
2211
2273
|
],
|
|
2212
|
-
} as
|
|
2274
|
+
} as AppendMessage;
|
|
2213
2275
|
|
|
2214
2276
|
expect(getEveMessageContent(message)).toEqual([
|
|
2215
2277
|
{
|
|
@@ -2229,7 +2291,7 @@ describe("getEveMessageContent", () => {
|
|
|
2229
2291
|
audio: { data: "https://cdn.example.com/memo.mp3", format: "mp3" },
|
|
2230
2292
|
},
|
|
2231
2293
|
],
|
|
2232
|
-
} as
|
|
2294
|
+
} as AppendMessage;
|
|
2233
2295
|
|
|
2234
2296
|
expect(getEveMessageContent(message)).toEqual([
|
|
2235
2297
|
{
|
|
@@ -2260,7 +2322,7 @@ describe("getEveMessageContent", () => {
|
|
|
2260
2322
|
status: { type: "complete" },
|
|
2261
2323
|
},
|
|
2262
2324
|
],
|
|
2263
|
-
} as
|
|
2325
|
+
} as AppendMessage;
|
|
2264
2326
|
|
|
2265
2327
|
expect(getEveMessageContent(message)).toEqual([
|
|
2266
2328
|
{
|
|
@@ -2287,16 +2349,19 @@ describe("getEveMessageContent", () => {
|
|
|
2287
2349
|
],
|
|
2288
2350
|
},
|
|
2289
2351
|
],
|
|
2290
|
-
} satisfies
|
|
2352
|
+
} satisfies MessageData);
|
|
2291
2353
|
|
|
2292
|
-
expect(echoed?.content).toEqual([
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2354
|
+
expect(echoed?.content).toEqual([]);
|
|
2355
|
+
expect(echoed?.attachments?.map((a) => a.content)).toEqual([
|
|
2356
|
+
[
|
|
2357
|
+
{
|
|
2358
|
+
type: "file",
|
|
2359
|
+
data: "https://example.com/report.pdf",
|
|
2360
|
+
mimeType: "application/pdf",
|
|
2361
|
+
filename: "report.pdf",
|
|
2362
|
+
sourceType: "url",
|
|
2363
|
+
},
|
|
2364
|
+
],
|
|
2300
2365
|
]);
|
|
2301
2366
|
});
|
|
2302
2367
|
|
|
@@ -2307,7 +2372,7 @@ describe("getEveMessageContent", () => {
|
|
|
2307
2372
|
{ type: "text", text: "hi" },
|
|
2308
2373
|
{ type: "data", name: "chart", data: { x: 1 } },
|
|
2309
2374
|
],
|
|
2310
|
-
} as
|
|
2375
|
+
} as AppendMessage;
|
|
2311
2376
|
|
|
2312
2377
|
expect(getEveMessageContent(message)).toBe("hi");
|
|
2313
2378
|
});
|
|
@@ -2315,7 +2380,7 @@ describe("getEveMessageContent", () => {
|
|
|
2315
2380
|
|
|
2316
2381
|
const approveCancel = [
|
|
2317
2382
|
{ id: "approve", label: "Approve" },
|
|
2318
|
-
{ id: "
|
|
2383
|
+
{ id: "deny", label: "Cancel" },
|
|
2319
2384
|
];
|
|
2320
2385
|
const environments = [
|
|
2321
2386
|
{ id: "staging", label: "Staging" },
|
|
@@ -2323,9 +2388,8 @@ const environments = [
|
|
|
2323
2388
|
];
|
|
2324
2389
|
|
|
2325
2390
|
const withInputRequest = (
|
|
2326
|
-
overrides: Partial<
|
|
2327
|
-
):
|
|
2328
|
-
requestId: "req_1",
|
|
2391
|
+
overrides: Partial<InputRequest> = {},
|
|
2392
|
+
): InputRequest => ({
|
|
2329
2393
|
prompt: "Send the email?",
|
|
2330
2394
|
kind: "question",
|
|
2331
2395
|
...overrides,
|
|
@@ -2343,8 +2407,8 @@ describe("toEveInputResponse", () => {
|
|
|
2343
2407
|
withInputRequest({ display: "confirmation", options: approveCancel }),
|
|
2344
2408
|
),
|
|
2345
2409
|
).toEqual({
|
|
2346
|
-
|
|
2347
|
-
optionId: "
|
|
2410
|
+
interactionId: "req_1",
|
|
2411
|
+
optionId: "deny",
|
|
2348
2412
|
text: "Not yet",
|
|
2349
2413
|
});
|
|
2350
2414
|
});
|
|
@@ -2352,7 +2416,7 @@ describe("toEveInputResponse", () => {
|
|
|
2352
2416
|
it("keeps the shipped one-argument mapping when no request is available", () => {
|
|
2353
2417
|
expect(toEveInputResponse({ approvalId: "req_1", approved: true })).toEqual(
|
|
2354
2418
|
{
|
|
2355
|
-
|
|
2419
|
+
interactionId: "req_1",
|
|
2356
2420
|
optionId: "approve",
|
|
2357
2421
|
},
|
|
2358
2422
|
);
|
|
@@ -2363,7 +2427,7 @@ describe("toEveInputResponse", () => {
|
|
|
2363
2427
|
approved: false,
|
|
2364
2428
|
reason: "Not yet",
|
|
2365
2429
|
}),
|
|
2366
|
-
).toEqual({
|
|
2430
|
+
).toEqual({ interactionId: "req_1", optionId: "deny", text: "Not yet" });
|
|
2367
2431
|
|
|
2368
2432
|
expect(
|
|
2369
2433
|
toEveInputResponse({
|
|
@@ -2371,7 +2435,7 @@ describe("toEveInputResponse", () => {
|
|
|
2371
2435
|
approved: true,
|
|
2372
2436
|
optionId: "staging",
|
|
2373
2437
|
}),
|
|
2374
|
-
).toEqual({
|
|
2438
|
+
).toEqual({ interactionId: "req_1", optionId: "staging" });
|
|
2375
2439
|
});
|
|
2376
2440
|
|
|
2377
2441
|
it("keeps the confirmation mapping when the request carries approve/cancel options", () => {
|
|
@@ -2380,7 +2444,7 @@ describe("toEveInputResponse", () => {
|
|
|
2380
2444
|
{ approvalId: "req_1", approved: true },
|
|
2381
2445
|
withInputRequest({ display: "confirmation", options: approveCancel }),
|
|
2382
2446
|
),
|
|
2383
|
-
).toEqual({
|
|
2447
|
+
).toEqual({ interactionId: "req_1", optionId: "approve" });
|
|
2384
2448
|
});
|
|
2385
2449
|
|
|
2386
2450
|
it("maps a select response to the chosen option id", () => {
|
|
@@ -2389,7 +2453,36 @@ describe("toEveInputResponse", () => {
|
|
|
2389
2453
|
{ approvalId: "req_1", approved: true, optionId: "staging" },
|
|
2390
2454
|
withInputRequest({ display: "select", options: environments }),
|
|
2391
2455
|
),
|
|
2392
|
-
).toEqual({
|
|
2456
|
+
).toEqual({ interactionId: "req_1", optionId: "staging" });
|
|
2457
|
+
});
|
|
2458
|
+
|
|
2459
|
+
it("preserves explanatory text with an explicitly selected option", () => {
|
|
2460
|
+
expect(
|
|
2461
|
+
toEveInputResponse(
|
|
2462
|
+
{
|
|
2463
|
+
approvalId: "req_1",
|
|
2464
|
+
approved: true,
|
|
2465
|
+
optionId: "staging",
|
|
2466
|
+
text: "Use the safe environment",
|
|
2467
|
+
},
|
|
2468
|
+
withInputRequest({ display: "select", options: environments }),
|
|
2469
|
+
),
|
|
2470
|
+
).toEqual({
|
|
2471
|
+
interactionId: "req_1",
|
|
2472
|
+
optionId: "staging",
|
|
2473
|
+
text: "Use the safe environment",
|
|
2474
|
+
});
|
|
2475
|
+
});
|
|
2476
|
+
|
|
2477
|
+
it("explains that an optionless request cannot accept an explicit option", () => {
|
|
2478
|
+
expect(() =>
|
|
2479
|
+
toEveInputResponse(
|
|
2480
|
+
{ approvalId: "req_1", approved: true, optionId: "staging" },
|
|
2481
|
+
withInputRequest({ display: "text" }),
|
|
2482
|
+
),
|
|
2483
|
+
).toThrow(
|
|
2484
|
+
'Eve interaction "req_1" has no option with id "staging"; respond with one of: (the request carries no options)',
|
|
2485
|
+
);
|
|
2393
2486
|
});
|
|
2394
2487
|
|
|
2395
2488
|
it.each([
|
|
@@ -2400,7 +2493,7 @@ describe("toEveInputResponse", () => {
|
|
|
2400
2493
|
"a select display that allows freeform",
|
|
2401
2494
|
{ display: "select", allowFreeform: true, options: environments },
|
|
2402
2495
|
],
|
|
2403
|
-
] satisfies [string, Partial<
|
|
2496
|
+
] satisfies [string, Partial<InputRequest>][])(
|
|
2404
2497
|
"answers a request with %s as free-form text, not a fabricated option id",
|
|
2405
2498
|
(_label, overrides) => {
|
|
2406
2499
|
const response = toEveInputResponse(
|
|
@@ -2409,7 +2502,7 @@ describe("toEveInputResponse", () => {
|
|
|
2409
2502
|
);
|
|
2410
2503
|
|
|
2411
2504
|
expect(response).toEqual({
|
|
2412
|
-
|
|
2505
|
+
interactionId: "req_1",
|
|
2413
2506
|
text: "Quarterly results",
|
|
2414
2507
|
});
|
|
2415
2508
|
expect(response).not.toHaveProperty("optionId");
|
|
@@ -2431,7 +2524,7 @@ describe("toEveInputResponse", () => {
|
|
|
2431
2524
|
{ approvalId: "req_1", approved: true, text: "staging" },
|
|
2432
2525
|
withInputRequest({ display: "text" }),
|
|
2433
2526
|
),
|
|
2434
|
-
).toEqual({
|
|
2527
|
+
).toEqual({ interactionId: "req_1", text: "staging" });
|
|
2435
2528
|
});
|
|
2436
2529
|
|
|
2437
2530
|
it("prefers the response text over the reason it used to be smuggled in", () => {
|
|
@@ -2445,7 +2538,7 @@ describe("toEveInputResponse", () => {
|
|
|
2445
2538
|
},
|
|
2446
2539
|
withInputRequest({ display: "text" }),
|
|
2447
2540
|
),
|
|
2448
|
-
).toEqual({
|
|
2541
|
+
).toEqual({ interactionId: "req_1", text: "staging" });
|
|
2449
2542
|
});
|
|
2450
2543
|
|
|
2451
2544
|
it("never fabricates approve for a text-display request without an answer", () => {
|
|
@@ -2474,9 +2567,6 @@ describe("toEveInputResponse", () => {
|
|
|
2474
2567
|
),
|
|
2475
2568
|
).toThrow(/no option with id "sandbox".*staging, production/s);
|
|
2476
2569
|
|
|
2477
|
-
// A named option the request does not carry outranks the decision, even
|
|
2478
|
-
// when the decision names an option the request does carry: substituting
|
|
2479
|
-
// the literal deny here would discard the choice the caller made.
|
|
2480
2570
|
expect(() =>
|
|
2481
2571
|
toEveInputResponse(
|
|
2482
2572
|
{ approvalId: "req_1", approved: false, optionId: "schedule-later" },
|
|
@@ -2493,7 +2583,7 @@ describe("toEveInputResponse", () => {
|
|
|
2493
2583
|
|
|
2494
2584
|
expect(
|
|
2495
2585
|
toEveInputResponse({ approvalId: "req_1", approved: true }, inputRequest),
|
|
2496
|
-
).toEqual({
|
|
2586
|
+
).toEqual({ interactionId: "req_1", optionId: "approve" });
|
|
2497
2587
|
|
|
2498
2588
|
expect(
|
|
2499
2589
|
toEveInputResponse(
|
|
@@ -2501,7 +2591,7 @@ describe("toEveInputResponse", () => {
|
|
|
2501
2591
|
inputRequest,
|
|
2502
2592
|
),
|
|
2503
2593
|
).toEqual({
|
|
2504
|
-
|
|
2594
|
+
interactionId: "req_1",
|
|
2505
2595
|
optionId: "approve",
|
|
2506
2596
|
text: "Quarterly results",
|
|
2507
2597
|
});
|
|
@@ -2509,25 +2599,25 @@ describe("toEveInputResponse", () => {
|
|
|
2509
2599
|
|
|
2510
2600
|
it("answers a tool-approval by kind even when its options are missing", () => {
|
|
2511
2601
|
const inputRequest = withInputRequest({
|
|
2512
|
-
kind: "
|
|
2602
|
+
kind: "approval",
|
|
2513
2603
|
display: "confirmation",
|
|
2514
2604
|
});
|
|
2515
2605
|
|
|
2516
2606
|
expect(
|
|
2517
2607
|
toEveInputResponse({ approvalId: "req_1", approved: true }, inputRequest),
|
|
2518
|
-
).toEqual({
|
|
2608
|
+
).toEqual({ interactionId: "req_1", optionId: "approve" });
|
|
2519
2609
|
|
|
2520
2610
|
expect(
|
|
2521
2611
|
toEveInputResponse(
|
|
2522
2612
|
{ approvalId: "req_1", approved: false, reason: "not now" },
|
|
2523
2613
|
inputRequest,
|
|
2524
2614
|
),
|
|
2525
|
-
).toEqual({
|
|
2615
|
+
).toEqual({ interactionId: "req_1", optionId: "deny", text: "not now" });
|
|
2526
2616
|
});
|
|
2527
2617
|
|
|
2528
2618
|
it("still requires a declared option when a tool-approval carries its own", () => {
|
|
2529
2619
|
const inputRequest = withInputRequest({
|
|
2530
|
-
kind: "
|
|
2620
|
+
kind: "approval",
|
|
2531
2621
|
display: "confirmation",
|
|
2532
2622
|
options: [{ id: "schedule-later", label: "Schedule later" }],
|
|
2533
2623
|
});
|
|
@@ -2541,7 +2631,7 @@ describe("toEveInputResponse", () => {
|
|
|
2541
2631
|
{ approvalId: "req_1", approved: true, optionId: "schedule-later" },
|
|
2542
2632
|
inputRequest,
|
|
2543
2633
|
),
|
|
2544
|
-
).toEqual({
|
|
2634
|
+
).toEqual({ interactionId: "req_1", optionId: "schedule-later" });
|
|
2545
2635
|
});
|
|
2546
2636
|
|
|
2547
2637
|
it("never answers an optionless confirmation as free-form text", () => {
|
|
@@ -2560,41 +2650,25 @@ describe("toEveInputResponse", () => {
|
|
|
2560
2650
|
});
|
|
2561
2651
|
|
|
2562
2652
|
describe("findEveInputRequest", () => {
|
|
2653
|
+
const request = {
|
|
2654
|
+
kind: "question",
|
|
2655
|
+
prompt: "What should the subject line be?",
|
|
2656
|
+
display: "text",
|
|
2657
|
+
allowFreeform: true,
|
|
2658
|
+
} as const satisfies InputRequest;
|
|
2563
2659
|
const data = {
|
|
2564
2660
|
messages: [
|
|
2565
2661
|
{
|
|
2566
2662
|
id: "a1",
|
|
2567
2663
|
role: "assistant",
|
|
2568
|
-
parts: [
|
|
2569
|
-
{
|
|
2570
|
-
type: "dynamic-tool",
|
|
2571
|
-
state: "approval-requested",
|
|
2572
|
-
toolCallId: "call_1",
|
|
2573
|
-
toolName: "ask_question",
|
|
2574
|
-
input: {},
|
|
2575
|
-
approval: { id: "req_1" },
|
|
2576
|
-
toolMetadata: {
|
|
2577
|
-
eve: {
|
|
2578
|
-
kind: "tool-call",
|
|
2579
|
-
name: "ask_question",
|
|
2580
|
-
inputRequest: {
|
|
2581
|
-
requestId: "req_1",
|
|
2582
|
-
prompt: "What should the subject line be?",
|
|
2583
|
-
kind: "question",
|
|
2584
|
-
display: "text",
|
|
2585
|
-
allowFreeform: true,
|
|
2586
|
-
},
|
|
2587
|
-
},
|
|
2588
|
-
},
|
|
2589
|
-
},
|
|
2590
|
-
],
|
|
2664
|
+
parts: [toolPart({ interaction: approvalInteraction(request) })],
|
|
2591
2665
|
},
|
|
2592
2666
|
],
|
|
2593
|
-
} satisfies
|
|
2667
|
+
} satisfies MessageData;
|
|
2594
2668
|
|
|
2595
2669
|
it("finds the input request by approval id", () => {
|
|
2596
2670
|
expect(findEveInputRequest(data, "req_1")).toMatchObject({
|
|
2597
|
-
|
|
2671
|
+
prompt: "What should the subject line be?",
|
|
2598
2672
|
display: "text",
|
|
2599
2673
|
});
|
|
2600
2674
|
});
|
|
@@ -2603,16 +2677,24 @@ describe("findEveInputRequest", () => {
|
|
|
2603
2677
|
expect(findEveInputRequest(data, "req_404")).toBeUndefined();
|
|
2604
2678
|
});
|
|
2605
2679
|
|
|
2606
|
-
it
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2680
|
+
it("returns undefined when the matching tool has no interaction", () => {
|
|
2681
|
+
const bare = {
|
|
2682
|
+
messages: [{ id: "a1", role: "assistant", parts: [toolPart()] }],
|
|
2683
|
+
} satisfies MessageData;
|
|
2684
|
+
expect(findEveInputRequest(bare, "req_1")).toBeUndefined();
|
|
2685
|
+
});
|
|
2612
2686
|
|
|
2687
|
+
it("finds a standalone interaction by its approval id", () => {
|
|
2613
2688
|
expect(
|
|
2614
|
-
findEveInputRequest(
|
|
2615
|
-
|
|
2689
|
+
findEveInputRequest(
|
|
2690
|
+
{
|
|
2691
|
+
messages: [
|
|
2692
|
+
{ id: "a1", role: "assistant", parts: [signInPart(request)] },
|
|
2693
|
+
],
|
|
2694
|
+
},
|
|
2695
|
+
"auth_1",
|
|
2696
|
+
),
|
|
2697
|
+
).toEqual(request);
|
|
2616
2698
|
});
|
|
2617
2699
|
});
|
|
2618
2700
|
|
|
@@ -2621,20 +2703,78 @@ describe("collectInterruptedTurnEvents", () => {
|
|
|
2621
2703
|
({
|
|
2622
2704
|
type: "turn.started",
|
|
2623
2705
|
meta: eventMeta(sequence),
|
|
2624
|
-
data: {
|
|
2625
|
-
|
|
2706
|
+
data: {
|
|
2707
|
+
turnId,
|
|
2708
|
+
cause: { deliveryId: `delivery_${turnId}` },
|
|
2709
|
+
follows: null,
|
|
2710
|
+
},
|
|
2711
|
+
}) as const satisfies SessionStreamEvent;
|
|
2626
2712
|
const failed = (turnId: string, sequence: number) =>
|
|
2627
2713
|
({
|
|
2628
|
-
type: "turn.
|
|
2714
|
+
type: "turn.settled",
|
|
2629
2715
|
meta: eventMeta(sequence),
|
|
2630
|
-
data: {
|
|
2631
|
-
|
|
2716
|
+
data: {
|
|
2717
|
+
turnId,
|
|
2718
|
+
outcome: "failed",
|
|
2719
|
+
error: { code: "internal", message: "boom" },
|
|
2720
|
+
},
|
|
2721
|
+
}) as const satisfies SessionStreamEvent;
|
|
2632
2722
|
const cancelled = (turnId: string, sequence: number) =>
|
|
2633
2723
|
({
|
|
2634
|
-
type: "turn.
|
|
2724
|
+
type: "turn.settled",
|
|
2635
2725
|
meta: eventMeta(sequence),
|
|
2636
|
-
data: { turnId,
|
|
2637
|
-
}) as const satisfies
|
|
2726
|
+
data: { turnId, outcome: "cancelled" },
|
|
2727
|
+
}) as const satisfies SessionStreamEvent;
|
|
2728
|
+
|
|
2729
|
+
it.each([
|
|
2730
|
+
[0, 128],
|
|
2731
|
+
[0, 512],
|
|
2732
|
+
[4, 128],
|
|
2733
|
+
[4, 512],
|
|
2734
|
+
])(
|
|
2735
|
+
"copies %i cached interruptions once when collecting %i more",
|
|
2736
|
+
(cachedCount, appendedCount) => {
|
|
2737
|
+
const previousEvents = Array.from({ length: cachedCount }, (_, index) =>
|
|
2738
|
+
failed(`previous_${index}`, index),
|
|
2739
|
+
);
|
|
2740
|
+
const previousInterruptions = [...previousEvents];
|
|
2741
|
+
const cache: InterruptedTurnEventCache = {
|
|
2742
|
+
lastEvents: previousEvents,
|
|
2743
|
+
interruptions: previousInterruptions,
|
|
2744
|
+
};
|
|
2745
|
+
const events = [
|
|
2746
|
+
...previousEvents,
|
|
2747
|
+
...Array.from({ length: appendedCount }, (_, index) =>
|
|
2748
|
+
cancelled(`next_${index}`, cachedCount + index),
|
|
2749
|
+
),
|
|
2750
|
+
];
|
|
2751
|
+
const iterator = Array.prototype[Symbol.iterator];
|
|
2752
|
+
let copiedEntries = 0;
|
|
2753
|
+
let interruptions: readonly SessionStreamEvent[];
|
|
2754
|
+
try {
|
|
2755
|
+
// A different copying primitive needs its own work counter.
|
|
2756
|
+
Array.prototype[Symbol.iterator] = function* (this: unknown[]) {
|
|
2757
|
+
for (const entry of iterator.call(this)) {
|
|
2758
|
+
copiedEntries++;
|
|
2759
|
+
yield entry;
|
|
2760
|
+
}
|
|
2761
|
+
return undefined;
|
|
2762
|
+
};
|
|
2763
|
+
interruptions = collectInterruptedTurnEvents(events, cache);
|
|
2764
|
+
} finally {
|
|
2765
|
+
Array.prototype[Symbol.iterator] = iterator;
|
|
2766
|
+
}
|
|
2767
|
+
|
|
2768
|
+
expect(interruptions).toEqual(events);
|
|
2769
|
+
expect(
|
|
2770
|
+
interruptions.every((event, index) => event === events[index]),
|
|
2771
|
+
).toBe(true);
|
|
2772
|
+
expect(previousInterruptions).toEqual(previousEvents);
|
|
2773
|
+
expect(cache.interruptions).toBe(interruptions);
|
|
2774
|
+
expect(cache.lastEvents).toBe(events);
|
|
2775
|
+
expect(copiedEntries).toBe(cachedCount);
|
|
2776
|
+
},
|
|
2777
|
+
);
|
|
2638
2778
|
|
|
2639
2779
|
it("scans only appended events and keeps the array until an interruption arrives", () => {
|
|
2640
2780
|
const cache: InterruptedTurnEventCache = {
|
|
@@ -2660,4 +2800,29 @@ describe("collectInterruptedTurnEvents", () => {
|
|
|
2660
2800
|
collectInterruptedTurnEvents([started("t1", 0), failed("t1", 1)], cache);
|
|
2661
2801
|
expect(collectInterruptedTurnEvents([started("t2", 0)], cache)).toEqual([]);
|
|
2662
2802
|
});
|
|
2803
|
+
|
|
2804
|
+
it("records failed session endings and ignores completed endings", () => {
|
|
2805
|
+
const cache: InterruptedTurnEventCache = {
|
|
2806
|
+
lastEvents: [],
|
|
2807
|
+
interruptions: [],
|
|
2808
|
+
};
|
|
2809
|
+
const completed = {
|
|
2810
|
+
type: "session.ended",
|
|
2811
|
+
data: { outcome: "completed" },
|
|
2812
|
+
meta: eventMeta(1),
|
|
2813
|
+
} as const satisfies SessionStreamEvent;
|
|
2814
|
+
const failedSession = {
|
|
2815
|
+
type: "session.ended",
|
|
2816
|
+
data: { outcome: "failed", cause: { turnId: "t1" } },
|
|
2817
|
+
meta: eventMeta(2),
|
|
2818
|
+
} as const satisfies SessionStreamEvent;
|
|
2819
|
+
const events = [completed, failedSession];
|
|
2820
|
+
|
|
2821
|
+
expect(collectInterruptedTurnEvents(events, cache)).toEqual([
|
|
2822
|
+
failedSession,
|
|
2823
|
+
]);
|
|
2824
|
+
expect(collectInterruptedTurnEvents(events, cache)).toBe(
|
|
2825
|
+
cache.interruptions,
|
|
2826
|
+
);
|
|
2827
|
+
});
|
|
2663
2828
|
});
|