@assistant-ui/eve 0.0.24 → 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 +146 -117
- 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 +925 -729
- package/src/convertEveMessages.ts +282 -227
- 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,7 +792,7 @@ describe("convertEveMessages", () => {
|
|
|
579
792
|
],
|
|
580
793
|
},
|
|
581
794
|
],
|
|
582
|
-
} satisfies
|
|
795
|
+
} satisfies MessageData;
|
|
583
796
|
|
|
584
797
|
const [message] = convertEveMessages(data);
|
|
585
798
|
|
|
@@ -604,7 +817,7 @@ describe("convertEveMessages", () => {
|
|
|
604
817
|
],
|
|
605
818
|
},
|
|
606
819
|
],
|
|
607
|
-
} satisfies
|
|
820
|
+
} satisfies MessageData;
|
|
608
821
|
|
|
609
822
|
const [message] = convertEveMessages(data);
|
|
610
823
|
|
|
@@ -647,7 +860,7 @@ describe("convertEveMessages", () => {
|
|
|
647
860
|
],
|
|
648
861
|
},
|
|
649
862
|
],
|
|
650
|
-
} satisfies
|
|
863
|
+
} satisfies MessageData;
|
|
651
864
|
|
|
652
865
|
const [message] = convertEveMessages(data);
|
|
653
866
|
|
|
@@ -685,7 +898,7 @@ describe("convertEveMessages", () => {
|
|
|
685
898
|
],
|
|
686
899
|
},
|
|
687
900
|
],
|
|
688
|
-
} satisfies
|
|
901
|
+
} satisfies MessageData;
|
|
689
902
|
|
|
690
903
|
const [message] = convertEveMessages(data);
|
|
691
904
|
|
|
@@ -729,7 +942,7 @@ describe("convertEveMessages", () => {
|
|
|
729
942
|
],
|
|
730
943
|
},
|
|
731
944
|
],
|
|
732
|
-
} satisfies
|
|
945
|
+
} satisfies MessageData;
|
|
733
946
|
|
|
734
947
|
const [message] = convertEveMessages(data);
|
|
735
948
|
|
|
@@ -739,58 +952,6 @@ describe("convertEveMessages", () => {
|
|
|
739
952
|
]);
|
|
740
953
|
});
|
|
741
954
|
|
|
742
|
-
it("defaults a file part with a missing mediaType to application/octet-stream", () => {
|
|
743
|
-
const data = {
|
|
744
|
-
messages: [
|
|
745
|
-
{
|
|
746
|
-
id: "u1",
|
|
747
|
-
role: "user",
|
|
748
|
-
parts: [{ type: "file", url: "https://example.com/blob" }],
|
|
749
|
-
},
|
|
750
|
-
],
|
|
751
|
-
} as unknown as EveMessageData;
|
|
752
|
-
|
|
753
|
-
const [message] = convertEveMessages(data);
|
|
754
|
-
|
|
755
|
-
expect(message?.content).toEqual([]);
|
|
756
|
-
expect(message?.attachments).toEqual([
|
|
757
|
-
{
|
|
758
|
-
id: "0",
|
|
759
|
-
type: "file",
|
|
760
|
-
name: "file",
|
|
761
|
-
content: [
|
|
762
|
-
{
|
|
763
|
-
type: "file",
|
|
764
|
-
data: "https://example.com/blob",
|
|
765
|
-
mimeType: "application/octet-stream",
|
|
766
|
-
sourceType: "url",
|
|
767
|
-
},
|
|
768
|
-
],
|
|
769
|
-
contentType: "application/octet-stream",
|
|
770
|
-
status: { type: "complete" },
|
|
771
|
-
},
|
|
772
|
-
]);
|
|
773
|
-
});
|
|
774
|
-
|
|
775
|
-
it("reads the media type from a data URL when eve omits mediaType", () => {
|
|
776
|
-
const data = {
|
|
777
|
-
messages: [
|
|
778
|
-
{
|
|
779
|
-
id: "u1",
|
|
780
|
-
role: "user",
|
|
781
|
-
parts: [{ type: "file", url: "data:image/png;base64,iVBORw0KGgo=" }],
|
|
782
|
-
},
|
|
783
|
-
],
|
|
784
|
-
} as unknown as EveMessageData;
|
|
785
|
-
|
|
786
|
-
const [message] = convertEveMessages(data);
|
|
787
|
-
|
|
788
|
-
expect(message?.content).toEqual([]);
|
|
789
|
-
expect(message?.attachments?.map((a) => [a.type, a.contentType])).toEqual([
|
|
790
|
-
["image", "image/png"],
|
|
791
|
-
]);
|
|
792
|
-
});
|
|
793
|
-
|
|
794
955
|
it("prefers an explicit mediaType over the data URL declaration", () => {
|
|
795
956
|
const data = {
|
|
796
957
|
messages: [
|
|
@@ -806,7 +967,7 @@ describe("convertEveMessages", () => {
|
|
|
806
967
|
],
|
|
807
968
|
},
|
|
808
969
|
],
|
|
809
|
-
} satisfies
|
|
970
|
+
} satisfies MessageData;
|
|
810
971
|
|
|
811
972
|
const [message] = convertEveMessages(data);
|
|
812
973
|
|
|
@@ -833,7 +994,7 @@ describe("convertEveMessages", () => {
|
|
|
833
994
|
],
|
|
834
995
|
},
|
|
835
996
|
],
|
|
836
|
-
} satisfies
|
|
997
|
+
} satisfies MessageData;
|
|
837
998
|
|
|
838
999
|
const [message] = convertEveMessages(data);
|
|
839
1000
|
|
|
@@ -849,6 +1010,25 @@ describe("convertEveMessages", () => {
|
|
|
849
1010
|
]);
|
|
850
1011
|
});
|
|
851
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
|
+
|
|
852
1032
|
it("drops non-convertible part types instead of throwing", () => {
|
|
853
1033
|
const data = {
|
|
854
1034
|
messages: [
|
|
@@ -856,14 +1036,12 @@ describe("convertEveMessages", () => {
|
|
|
856
1036
|
id: "a1",
|
|
857
1037
|
role: "assistant",
|
|
858
1038
|
parts: [
|
|
859
|
-
{ type: "step-start" },
|
|
860
1039
|
{ type: "future-part", payload: {} },
|
|
861
|
-
{ type: "file", mediaType: "application/pdf" },
|
|
862
1040
|
{ type: "text", text: "Done" },
|
|
863
1041
|
],
|
|
864
1042
|
},
|
|
865
1043
|
],
|
|
866
|
-
} as unknown as
|
|
1044
|
+
} as unknown as MessageData;
|
|
867
1045
|
|
|
868
1046
|
const [message] = convertEveMessages(data);
|
|
869
1047
|
|
|
@@ -877,26 +1055,22 @@ describe("convertEveMessages", () => {
|
|
|
877
1055
|
id: "a1",
|
|
878
1056
|
role: "assistant",
|
|
879
1057
|
parts: [
|
|
880
|
-
{
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
displayName: "GitHub",
|
|
886
|
-
stepIndex: 0,
|
|
887
|
-
turnId: "turn_1",
|
|
888
|
-
authorization: {
|
|
1058
|
+
signInPart({
|
|
1059
|
+
kind: "sign-in",
|
|
1060
|
+
prompt: "Sign in to GitHub",
|
|
1061
|
+
signIn: {
|
|
1062
|
+
name: "github",
|
|
889
1063
|
displayName: "GitHub",
|
|
890
1064
|
instructions: "Enter the code on the device page",
|
|
891
1065
|
url: "https://github.com/login/device",
|
|
892
1066
|
userCode: "ABCD-1234",
|
|
893
1067
|
expiresAt: "2026-01-01T00:00:00Z",
|
|
894
1068
|
},
|
|
895
|
-
},
|
|
1069
|
+
}),
|
|
896
1070
|
],
|
|
897
1071
|
},
|
|
898
1072
|
],
|
|
899
|
-
} satisfies
|
|
1073
|
+
} satisfies MessageData;
|
|
900
1074
|
|
|
901
1075
|
const [message] = convertEveMessages(data);
|
|
902
1076
|
|
|
@@ -925,15 +1099,15 @@ describe("convertEveMessages", () => {
|
|
|
925
1099
|
id: "a1",
|
|
926
1100
|
role: "assistant",
|
|
927
1101
|
parts: [
|
|
928
|
-
{
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
name: "github",
|
|
932
|
-
},
|
|
1102
|
+
signInPart({
|
|
1103
|
+
kind: "sign-in",
|
|
1104
|
+
prompt: "",
|
|
1105
|
+
signIn: { name: "github" },
|
|
1106
|
+
}),
|
|
933
1107
|
],
|
|
934
1108
|
},
|
|
935
1109
|
],
|
|
936
|
-
}
|
|
1110
|
+
} satisfies MessageData;
|
|
937
1111
|
|
|
938
1112
|
const [message] = convertEveMessages(data);
|
|
939
1113
|
|
|
@@ -941,7 +1115,7 @@ describe("convertEveMessages", () => {
|
|
|
941
1115
|
{
|
|
942
1116
|
type: "data",
|
|
943
1117
|
name: "authorization",
|
|
944
|
-
data: { state: "required", name: "github" },
|
|
1118
|
+
data: { state: "required", name: "github", description: "" },
|
|
945
1119
|
},
|
|
946
1120
|
]);
|
|
947
1121
|
});
|
|
@@ -953,19 +1127,19 @@ describe("convertEveMessages", () => {
|
|
|
953
1127
|
id: "a1",
|
|
954
1128
|
role: "assistant",
|
|
955
1129
|
parts: [
|
|
956
|
-
{
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
1130
|
+
signInPart({
|
|
1131
|
+
kind: "sign-in",
|
|
1132
|
+
prompt: "",
|
|
1133
|
+
signIn: {
|
|
1134
|
+
name: "github",
|
|
961
1135
|
url: "javascript:alert(1)",
|
|
962
1136
|
userCode: "ABCD-1234",
|
|
963
1137
|
},
|
|
964
|
-
},
|
|
1138
|
+
}),
|
|
965
1139
|
],
|
|
966
1140
|
},
|
|
967
1141
|
],
|
|
968
|
-
}
|
|
1142
|
+
} satisfies MessageData;
|
|
969
1143
|
|
|
970
1144
|
const [message] = convertEveMessages(data);
|
|
971
1145
|
|
|
@@ -973,7 +1147,12 @@ describe("convertEveMessages", () => {
|
|
|
973
1147
|
{
|
|
974
1148
|
type: "data",
|
|
975
1149
|
name: "authorization",
|
|
976
|
-
data: {
|
|
1150
|
+
data: {
|
|
1151
|
+
state: "required",
|
|
1152
|
+
name: "github",
|
|
1153
|
+
description: "",
|
|
1154
|
+
userCode: "ABCD-1234",
|
|
1155
|
+
},
|
|
977
1156
|
},
|
|
978
1157
|
]);
|
|
979
1158
|
});
|
|
@@ -985,20 +1164,18 @@ describe("convertEveMessages", () => {
|
|
|
985
1164
|
id: "a1",
|
|
986
1165
|
role: "assistant",
|
|
987
1166
|
parts: [
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
outcome: "authorized",
|
|
997
|
-
},
|
|
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
|
+
),
|
|
998
1175
|
],
|
|
999
1176
|
},
|
|
1000
1177
|
],
|
|
1001
|
-
} satisfies
|
|
1178
|
+
} satisfies MessageData;
|
|
1002
1179
|
|
|
1003
1180
|
const [message] = convertEveMessages(data);
|
|
1004
1181
|
|
|
@@ -1020,7 +1197,7 @@ describe("convertEveMessages", () => {
|
|
|
1020
1197
|
describe("assistant message status mapping", () => {
|
|
1021
1198
|
const withStatus = (
|
|
1022
1199
|
status: "streaming" | "complete" | "failed",
|
|
1023
|
-
):
|
|
1200
|
+
): MessageData => ({
|
|
1024
1201
|
messages: [
|
|
1025
1202
|
{
|
|
1026
1203
|
id: "a1",
|
|
@@ -1084,7 +1261,7 @@ describe("convertEveMessages", () => {
|
|
|
1084
1261
|
parts: [{ type: "text", text: "Try again" }],
|
|
1085
1262
|
},
|
|
1086
1263
|
],
|
|
1087
|
-
} satisfies
|
|
1264
|
+
} satisfies MessageData;
|
|
1088
1265
|
|
|
1089
1266
|
const [assistant] = convertEveMessages(data, {
|
|
1090
1267
|
isRunning: true,
|
|
@@ -1114,6 +1291,101 @@ describe("convertEveMessages", () => {
|
|
|
1114
1291
|
expect(message?.status).toEqual({ type: "incomplete", reason: "error" });
|
|
1115
1292
|
});
|
|
1116
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
|
+
|
|
1117
1389
|
it("keeps requires-action for pending approvals when not running", () => {
|
|
1118
1390
|
const data = {
|
|
1119
1391
|
messages: [
|
|
@@ -1121,21 +1393,12 @@ describe("convertEveMessages", () => {
|
|
|
1121
1393
|
id: "a1",
|
|
1122
1394
|
role: "assistant",
|
|
1123
1395
|
metadata: { status: "streaming" },
|
|
1124
|
-
parts: [
|
|
1125
|
-
{
|
|
1126
|
-
type: "dynamic-tool",
|
|
1127
|
-
state: "approval-requested",
|
|
1128
|
-
toolCallId: "call_1",
|
|
1129
|
-
toolName: "send_email",
|
|
1130
|
-
input: {},
|
|
1131
|
-
approval: { id: "req_1" },
|
|
1132
|
-
},
|
|
1133
|
-
],
|
|
1396
|
+
parts: [toolPart({ interaction: approvalInteraction() })],
|
|
1134
1397
|
},
|
|
1135
1398
|
],
|
|
1136
|
-
} satisfies
|
|
1399
|
+
} satisfies MessageData;
|
|
1137
1400
|
|
|
1138
|
-
const
|
|
1401
|
+
const message = convertEveMessages(data, { isRunning: false }).at(-1);
|
|
1139
1402
|
|
|
1140
1403
|
expect(message?.status).toEqual({
|
|
1141
1404
|
type: "requires-action",
|
|
@@ -1151,19 +1414,15 @@ describe("convertEveMessages", () => {
|
|
|
1151
1414
|
role: "assistant",
|
|
1152
1415
|
metadata: { status: "streaming" },
|
|
1153
1416
|
parts: [
|
|
1154
|
-
{
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
name: "github",
|
|
1158
|
-
|
|
1159
|
-
displayName: "GitHub",
|
|
1160
|
-
stepIndex: 0,
|
|
1161
|
-
turnId: "turn_1",
|
|
1162
|
-
},
|
|
1417
|
+
signInPart({
|
|
1418
|
+
kind: "sign-in",
|
|
1419
|
+
prompt: "Sign in to GitHub",
|
|
1420
|
+
signIn: { name: "github", displayName: "GitHub" },
|
|
1421
|
+
}),
|
|
1163
1422
|
],
|
|
1164
1423
|
},
|
|
1165
1424
|
],
|
|
1166
|
-
} satisfies
|
|
1425
|
+
} satisfies MessageData;
|
|
1167
1426
|
|
|
1168
1427
|
const [message] = convertEveMessages(data, { isRunning: false });
|
|
1169
1428
|
|
|
@@ -1181,27 +1440,16 @@ describe("convertEveMessages", () => {
|
|
|
1181
1440
|
role: "assistant",
|
|
1182
1441
|
metadata: { status: "streaming" },
|
|
1183
1442
|
parts: [
|
|
1184
|
-
{
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
approval: { id: "req_1" },
|
|
1191
|
-
},
|
|
1192
|
-
{
|
|
1193
|
-
type: "authorization",
|
|
1194
|
-
state: "required",
|
|
1195
|
-
name: "github",
|
|
1196
|
-
description: "Sign in to GitHub",
|
|
1197
|
-
displayName: "GitHub",
|
|
1198
|
-
stepIndex: 0,
|
|
1199
|
-
turnId: "turn_1",
|
|
1200
|
-
},
|
|
1443
|
+
toolPart({ interaction: approvalInteraction() }),
|
|
1444
|
+
signInPart({
|
|
1445
|
+
kind: "sign-in",
|
|
1446
|
+
prompt: "Sign in to GitHub",
|
|
1447
|
+
signIn: { name: "github", displayName: "GitHub" },
|
|
1448
|
+
}),
|
|
1201
1449
|
],
|
|
1202
1450
|
},
|
|
1203
1451
|
],
|
|
1204
|
-
} satisfies
|
|
1452
|
+
} satisfies MessageData;
|
|
1205
1453
|
|
|
1206
1454
|
const [message] = convertEveMessages(data, { isRunning: false });
|
|
1207
1455
|
|
|
@@ -1210,54 +1458,28 @@ describe("convertEveMessages", () => {
|
|
|
1210
1458
|
reason: "tool-calls",
|
|
1211
1459
|
});
|
|
1212
1460
|
});
|
|
1213
|
-
|
|
1214
|
-
it("settles a cancelled turn with a pending authorization", () => {
|
|
1215
|
-
const
|
|
1216
|
-
const
|
|
1217
|
-
{
|
|
1218
|
-
type: "authorization.required",
|
|
1219
|
-
meta: eventMeta(0),
|
|
1220
|
-
data: {
|
|
1221
|
-
turnId: "turn_1",
|
|
1222
|
-
stepIndex: 0,
|
|
1223
|
-
sequence: 0,
|
|
1224
|
-
name: "github",
|
|
1225
|
-
description: "Sign in to GitHub",
|
|
1226
|
-
},
|
|
1227
|
-
},
|
|
1228
|
-
{
|
|
1229
|
-
type: "turn.cancelled",
|
|
1230
|
-
meta: eventMeta(1),
|
|
1231
|
-
data: { turnId: "turn_1", sequence: 1 },
|
|
1232
|
-
},
|
|
1233
|
-
];
|
|
1234
|
-
const pendingData = events
|
|
1235
|
-
.slice(0, 1)
|
|
1236
|
-
.reduce(
|
|
1237
|
-
(state, event) => reducer.reduce(state, event),
|
|
1238
|
-
reducer.initial(),
|
|
1239
|
-
);
|
|
1461
|
+
|
|
1462
|
+
it("settles a cancelled turn with a pending authorization", () => {
|
|
1463
|
+
const events = [...initialTurnEvents, signInOpened("github")];
|
|
1464
|
+
const pendingData = replay(events);
|
|
1240
1465
|
const pendingAssistant = pendingData.messages.find(
|
|
1241
1466
|
(message) => message.role === "assistant",
|
|
1242
1467
|
);
|
|
1243
1468
|
expect(pendingAssistant?.metadata?.status).toBe("streaming");
|
|
1244
1469
|
expect(
|
|
1245
1470
|
convertEveMessages(pendingData, { isRunning: false }).at(-1)?.status,
|
|
1246
|
-
).toEqual({
|
|
1247
|
-
type: "requires-action",
|
|
1248
|
-
reason: "interrupt",
|
|
1249
|
-
});
|
|
1250
|
-
|
|
1251
|
-
const data = events
|
|
1252
|
-
.slice(1)
|
|
1253
|
-
.reduce((state, event) => reducer.reduce(state, event), pendingData);
|
|
1471
|
+
).toEqual({ type: "requires-action", reason: "interrupt" });
|
|
1254
1472
|
|
|
1255
|
-
const
|
|
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);
|
|
1256
1481
|
|
|
1257
|
-
expect(message?.status).toEqual({
|
|
1258
|
-
type: "complete",
|
|
1259
|
-
reason: "stop",
|
|
1260
|
-
});
|
|
1482
|
+
expect(message?.status).toEqual({ type: "complete", reason: "stop" });
|
|
1261
1483
|
});
|
|
1262
1484
|
|
|
1263
1485
|
it("reports a failed turn with a pending authorization", () => {
|
|
@@ -1268,19 +1490,15 @@ describe("convertEveMessages", () => {
|
|
|
1268
1490
|
role: "assistant",
|
|
1269
1491
|
metadata: { status: "streaming" },
|
|
1270
1492
|
parts: [
|
|
1271
|
-
{
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
name: "github",
|
|
1275
|
-
|
|
1276
|
-
displayName: "GitHub",
|
|
1277
|
-
stepIndex: 0,
|
|
1278
|
-
turnId: "turn_1",
|
|
1279
|
-
},
|
|
1493
|
+
signInPart({
|
|
1494
|
+
kind: "sign-in",
|
|
1495
|
+
prompt: "Sign in to GitHub",
|
|
1496
|
+
signIn: { name: "github", displayName: "GitHub" },
|
|
1497
|
+
}),
|
|
1280
1498
|
],
|
|
1281
1499
|
},
|
|
1282
1500
|
],
|
|
1283
|
-
} satisfies
|
|
1501
|
+
} satisfies MessageData;
|
|
1284
1502
|
|
|
1285
1503
|
const error = new Error("authorization failed");
|
|
1286
1504
|
const [message] = convertEveMessages(data, {
|
|
@@ -1306,15 +1524,11 @@ describe("convertEveMessages", () => {
|
|
|
1306
1524
|
role: "assistant",
|
|
1307
1525
|
metadata: { status: "streaming" },
|
|
1308
1526
|
parts: [
|
|
1309
|
-
{
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
name: "github",
|
|
1313
|
-
|
|
1314
|
-
displayName: "GitHub",
|
|
1315
|
-
stepIndex: 0,
|
|
1316
|
-
turnId: "turn_1",
|
|
1317
|
-
},
|
|
1527
|
+
signInPart({
|
|
1528
|
+
kind: "sign-in",
|
|
1529
|
+
prompt: "Sign in to GitHub",
|
|
1530
|
+
signIn: { name: "github", displayName: "GitHub" },
|
|
1531
|
+
}),
|
|
1318
1532
|
],
|
|
1319
1533
|
},
|
|
1320
1534
|
{
|
|
@@ -1324,7 +1538,7 @@ describe("convertEveMessages", () => {
|
|
|
1324
1538
|
parts: [{ type: "text", text: "Later turn" }],
|
|
1325
1539
|
},
|
|
1326
1540
|
],
|
|
1327
|
-
} satisfies
|
|
1541
|
+
} satisfies MessageData;
|
|
1328
1542
|
|
|
1329
1543
|
const [authorization] = convertEveMessages(data, {
|
|
1330
1544
|
isRunning: false,
|
|
@@ -1345,93 +1559,70 @@ describe("convertEveMessages", () => {
|
|
|
1345
1559
|
role: "assistant",
|
|
1346
1560
|
metadata: { status: "complete" },
|
|
1347
1561
|
parts: [
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
outcome: "authorized",
|
|
1357
|
-
},
|
|
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
|
+
),
|
|
1358
1570
|
{ type: "text", text: "Done" },
|
|
1359
1571
|
],
|
|
1360
1572
|
},
|
|
1361
1573
|
],
|
|
1362
|
-
} satisfies
|
|
1574
|
+
} satisfies MessageData;
|
|
1363
1575
|
|
|
1364
1576
|
const [message] = convertEveMessages(data, { isRunning: false });
|
|
1365
1577
|
|
|
1366
1578
|
expect(message?.status).toEqual({ type: "complete", reason: "stop" });
|
|
1367
1579
|
});
|
|
1368
1580
|
|
|
1369
|
-
describe("contract with eve's
|
|
1370
|
-
const
|
|
1371
|
-
|
|
1372
|
-
return events.reduce(
|
|
1373
|
-
(state, event) => reducer.reduce(state, event),
|
|
1374
|
-
reducer.initial(),
|
|
1375
|
-
);
|
|
1376
|
-
};
|
|
1377
|
-
|
|
1378
|
-
const midStreamEvents: readonly EveAgentReducerEvent[] = [
|
|
1379
|
-
{
|
|
1380
|
-
type: "client.message.submitted",
|
|
1381
|
-
data: { submissionId: "sub_1", message: "hi", createdAt: 0 },
|
|
1382
|
-
},
|
|
1581
|
+
describe("contract with eve's conversation projection", () => {
|
|
1582
|
+
const midStreamEvents: readonly SessionEvent[] = [
|
|
1583
|
+
...initialTurnEvents,
|
|
1383
1584
|
{
|
|
1384
|
-
type: "
|
|
1385
|
-
|
|
1386
|
-
data: {
|
|
1387
|
-
},
|
|
1388
|
-
{
|
|
1389
|
-
type: "step.started",
|
|
1390
|
-
meta: eventMeta(1),
|
|
1391
|
-
data: {
|
|
1392
|
-
turnId: "turn_1",
|
|
1393
|
-
stepIndex: 0,
|
|
1394
|
-
sequence: 1,
|
|
1395
|
-
modelId: "test-model",
|
|
1396
|
-
},
|
|
1397
|
-
},
|
|
1398
|
-
{
|
|
1399
|
-
type: "message.appended",
|
|
1400
|
-
meta: eventMeta(2),
|
|
1401
|
-
data: {
|
|
1402
|
-
turnId: "turn_1",
|
|
1403
|
-
stepIndex: 0,
|
|
1404
|
-
sequence: 2,
|
|
1405
|
-
messageDelta: "Let me th",
|
|
1406
|
-
},
|
|
1585
|
+
type: "content.delta",
|
|
1586
|
+
scope: { turnId: "turn_1", runId: "run_1" },
|
|
1587
|
+
data: { partId: "part_1", kind: "text", delta: "Let me th" },
|
|
1407
1588
|
},
|
|
1408
1589
|
];
|
|
1409
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
|
+
|
|
1410
1613
|
it("carries the challenge Eve projects onto an authorization part", () => {
|
|
1411
1614
|
const state = replay([
|
|
1412
1615
|
...midStreamEvents,
|
|
1413
|
-
{
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
name: "github",
|
|
1421
|
-
description: "Authorization required for github",
|
|
1422
|
-
authorization: {
|
|
1423
|
-
displayName: "GitHub",
|
|
1424
|
-
instructions: "Enter the code on the device page",
|
|
1425
|
-
url: "https://github.com/login/device",
|
|
1426
|
-
userCode: "ABCD-1234",
|
|
1427
|
-
expiresAt: "2026-01-01T00:00:00Z",
|
|
1428
|
-
},
|
|
1429
|
-
},
|
|
1430
|
-
},
|
|
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
|
+
}),
|
|
1431
1623
|
]);
|
|
1432
1624
|
|
|
1433
1625
|
const converted = convertEveMessages(state, { isRunning: false });
|
|
1434
|
-
|
|
1435
1626
|
expect(converted.at(-1)?.content).toContainEqual({
|
|
1436
1627
|
type: "data",
|
|
1437
1628
|
name: "authorization",
|
|
@@ -1451,39 +1642,19 @@ describe("convertEveMessages", () => {
|
|
|
1451
1642
|
it("releases the hold when Eve settles the authorization part in place", () => {
|
|
1452
1643
|
const state = replay([
|
|
1453
1644
|
...midStreamEvents,
|
|
1645
|
+
signInOpened("github"),
|
|
1454
1646
|
{
|
|
1455
|
-
type: "
|
|
1456
|
-
|
|
1457
|
-
data: {
|
|
1458
|
-
turnId: "turn_1",
|
|
1459
|
-
stepIndex: 0,
|
|
1460
|
-
sequence: 3,
|
|
1461
|
-
name: "github",
|
|
1462
|
-
description: "Authorization required for github",
|
|
1463
|
-
},
|
|
1464
|
-
},
|
|
1465
|
-
{
|
|
1466
|
-
type: "authorization.completed",
|
|
1467
|
-
meta: eventMeta(4),
|
|
1468
|
-
data: {
|
|
1469
|
-
turnId: "turn_1",
|
|
1470
|
-
stepIndex: 0,
|
|
1471
|
-
sequence: 4,
|
|
1472
|
-
name: "github",
|
|
1473
|
-
outcome: "authorized",
|
|
1474
|
-
},
|
|
1647
|
+
type: "interaction.settled",
|
|
1648
|
+
data: { interactionId: "auth_github", outcome: "accepted" },
|
|
1475
1649
|
},
|
|
1476
1650
|
]);
|
|
1477
1651
|
|
|
1478
1652
|
const assistant = state.messages.find((m) => m.role === "assistant");
|
|
1479
1653
|
const authorizationParts = assistant?.parts.filter(
|
|
1480
|
-
(part) => part.type === "
|
|
1654
|
+
(part) => part.type === "interaction",
|
|
1481
1655
|
);
|
|
1482
1656
|
expect(authorizationParts).toHaveLength(1);
|
|
1483
|
-
expect(authorizationParts?.[0]).toMatchObject({
|
|
1484
|
-
|
|
1485
|
-
// Eve leaves the streaming marker set, so the released turn falls back
|
|
1486
|
-
// to the stale-marker mapping rather than to a terminal status.
|
|
1657
|
+
expect(authorizationParts?.[0]).toMatchObject({ status: "settled" });
|
|
1487
1658
|
expect(
|
|
1488
1659
|
convertEveMessages(state, { isRunning: false }).at(-1)?.status,
|
|
1489
1660
|
).toEqual({ type: "incomplete", reason: "cancelled" });
|
|
@@ -1492,38 +1663,11 @@ describe("convertEveMessages", () => {
|
|
|
1492
1663
|
it("keeps the hold while another connector is still required", () => {
|
|
1493
1664
|
const state = replay([
|
|
1494
1665
|
...midStreamEvents,
|
|
1666
|
+
signInOpened("github"),
|
|
1667
|
+
signInOpened("slack"),
|
|
1495
1668
|
{
|
|
1496
|
-
type: "
|
|
1497
|
-
|
|
1498
|
-
data: {
|
|
1499
|
-
turnId: "turn_1",
|
|
1500
|
-
stepIndex: 0,
|
|
1501
|
-
sequence: 3,
|
|
1502
|
-
name: "github",
|
|
1503
|
-
description: "Authorization required for github",
|
|
1504
|
-
},
|
|
1505
|
-
},
|
|
1506
|
-
{
|
|
1507
|
-
type: "authorization.required",
|
|
1508
|
-
meta: eventMeta(4),
|
|
1509
|
-
data: {
|
|
1510
|
-
turnId: "turn_1",
|
|
1511
|
-
stepIndex: 0,
|
|
1512
|
-
sequence: 4,
|
|
1513
|
-
name: "slack",
|
|
1514
|
-
description: "Authorization required for slack",
|
|
1515
|
-
},
|
|
1516
|
-
},
|
|
1517
|
-
{
|
|
1518
|
-
type: "authorization.completed",
|
|
1519
|
-
meta: eventMeta(5),
|
|
1520
|
-
data: {
|
|
1521
|
-
turnId: "turn_1",
|
|
1522
|
-
stepIndex: 0,
|
|
1523
|
-
sequence: 5,
|
|
1524
|
-
name: "github",
|
|
1525
|
-
outcome: "authorized",
|
|
1526
|
-
},
|
|
1669
|
+
type: "interaction.settled",
|
|
1670
|
+
data: { interactionId: "auth_github", outcome: "accepted" },
|
|
1527
1671
|
},
|
|
1528
1672
|
]);
|
|
1529
1673
|
|
|
@@ -1533,51 +1677,41 @@ describe("convertEveMessages", () => {
|
|
|
1533
1677
|
});
|
|
1534
1678
|
|
|
1535
1679
|
it("projects the input request onto the approval part it gates", () => {
|
|
1536
|
-
const events: readonly
|
|
1680
|
+
const events: readonly SessionEvent[] = [
|
|
1537
1681
|
...midStreamEvents,
|
|
1538
1682
|
{
|
|
1539
|
-
|
|
1683
|
+
...callRequested,
|
|
1540
1684
|
data: {
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
},
|
|
1557
|
-
],
|
|
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
|
+
},
|
|
1558
1700
|
},
|
|
1559
|
-
meta: eventMeta(3),
|
|
1560
1701
|
},
|
|
1561
1702
|
];
|
|
1562
|
-
|
|
1563
|
-
// Rehydration replays the *stored* event log, which has been through
|
|
1564
|
-
// the wire and back, so the reloaded case is the serialized payload
|
|
1565
|
-
// rather than the same objects a second time.
|
|
1566
|
-
const rehydrated = JSON.parse(
|
|
1567
|
-
JSON.stringify(events),
|
|
1568
|
-
) as readonly EveAgentReducerEvent[];
|
|
1703
|
+
const rehydrated = JSON.parse(JSON.stringify(events)) as SessionEvent[];
|
|
1569
1704
|
|
|
1570
1705
|
for (const state of [replay(events), replay(rehydrated)]) {
|
|
1571
1706
|
const part = state.messages
|
|
1572
1707
|
.find((message) => message.role === "assistant")
|
|
1573
|
-
?.parts.find((candidate) => candidate.type === "
|
|
1708
|
+
?.parts.find((candidate) => candidate.type === "tool");
|
|
1574
1709
|
|
|
1575
1710
|
expect(part).toMatchObject({
|
|
1576
|
-
|
|
1577
|
-
|
|
1711
|
+
type: "tool",
|
|
1712
|
+
interaction: { interactionId: "req_1", status: "open" },
|
|
1578
1713
|
});
|
|
1579
1714
|
expect(findEveInputRequest(state, "req_1")).toMatchObject({
|
|
1580
|
-
requestId: "req_1",
|
|
1581
1715
|
prompt: "What should the subject line be?",
|
|
1582
1716
|
display: "text",
|
|
1583
1717
|
});
|
|
@@ -1586,188 +1720,110 @@ describe("convertEveMessages", () => {
|
|
|
1586
1720
|
|
|
1587
1721
|
it("leaves a reasoning part unsettled when a tool call follows it", () => {
|
|
1588
1722
|
const state = replay([
|
|
1589
|
-
...
|
|
1590
|
-
{
|
|
1591
|
-
type: "reasoning.appended",
|
|
1592
|
-
meta: eventMeta(2),
|
|
1593
|
-
data: {
|
|
1594
|
-
turnId: "turn_1",
|
|
1595
|
-
stepIndex: 0,
|
|
1596
|
-
sequence: 2,
|
|
1597
|
-
reasoningDelta: "Think",
|
|
1598
|
-
},
|
|
1599
|
-
},
|
|
1723
|
+
...initialTurnEvents,
|
|
1600
1724
|
{
|
|
1601
|
-
type: "
|
|
1602
|
-
|
|
1603
|
-
data: {
|
|
1604
|
-
turnId: "turn_1",
|
|
1605
|
-
stepIndex: 0,
|
|
1606
|
-
sequence: 3,
|
|
1607
|
-
actions: [
|
|
1608
|
-
{
|
|
1609
|
-
kind: "tool-call",
|
|
1610
|
-
callId: "call_1",
|
|
1611
|
-
toolName: "search",
|
|
1612
|
-
input: {},
|
|
1613
|
-
},
|
|
1614
|
-
],
|
|
1615
|
-
},
|
|
1725
|
+
type: "content.delta",
|
|
1726
|
+
scope: { turnId: "turn_1", runId: "run_1" },
|
|
1727
|
+
data: { partId: "reason_1", kind: "reasoning", delta: "Think" },
|
|
1616
1728
|
},
|
|
1729
|
+
callRequested,
|
|
1617
1730
|
]);
|
|
1618
1731
|
|
|
1619
1732
|
const message = state.messages.find((m) => m.role === "assistant");
|
|
1620
1733
|
expect(message?.parts).toEqual(
|
|
1621
1734
|
expect.arrayContaining([
|
|
1622
1735
|
expect.objectContaining({ type: "reasoning", state: "streaming" }),
|
|
1623
|
-
expect.objectContaining({ type: "
|
|
1736
|
+
expect.objectContaining({ type: "tool" }),
|
|
1624
1737
|
]),
|
|
1625
1738
|
);
|
|
1626
|
-
|
|
1627
1739
|
const content = convertEveMessages(state, { isRunning: true }).at(
|
|
1628
1740
|
-1,
|
|
1629
1741
|
)?.content;
|
|
1630
1742
|
expect(content).toEqual([
|
|
1631
|
-
expect.objectContaining({ type: "reasoning" }),
|
|
1632
1743
|
expect.objectContaining({ type: "tool-call" }),
|
|
1744
|
+
expect.objectContaining({ type: "reasoning" }),
|
|
1633
1745
|
]);
|
|
1634
|
-
expect(content?.[
|
|
1746
|
+
expect(content?.[1]).not.toHaveProperty("status");
|
|
1635
1747
|
});
|
|
1636
1748
|
|
|
1637
1749
|
it("settles leftover reasoning after the model stream completes a tool call", () => {
|
|
1638
1750
|
const state = replay([
|
|
1639
|
-
...
|
|
1640
|
-
{
|
|
1641
|
-
type: "reasoning.appended",
|
|
1642
|
-
meta: eventMeta(2),
|
|
1643
|
-
data: {
|
|
1644
|
-
turnId: "turn_1",
|
|
1645
|
-
stepIndex: 0,
|
|
1646
|
-
sequence: 2,
|
|
1647
|
-
reasoningDelta: "Think",
|
|
1648
|
-
},
|
|
1649
|
-
},
|
|
1650
|
-
{
|
|
1651
|
-
type: "actions.requested",
|
|
1652
|
-
meta: eventMeta(3),
|
|
1653
|
-
data: {
|
|
1654
|
-
turnId: "turn_1",
|
|
1655
|
-
stepIndex: 0,
|
|
1656
|
-
sequence: 3,
|
|
1657
|
-
actions: [
|
|
1658
|
-
{
|
|
1659
|
-
kind: "tool-call",
|
|
1660
|
-
callId: "call_1",
|
|
1661
|
-
toolName: "search",
|
|
1662
|
-
input: {},
|
|
1663
|
-
},
|
|
1664
|
-
],
|
|
1665
|
-
},
|
|
1666
|
-
},
|
|
1751
|
+
...initialTurnEvents,
|
|
1667
1752
|
{
|
|
1668
|
-
type: "
|
|
1669
|
-
|
|
1670
|
-
data: {
|
|
1671
|
-
turnId: "turn_1",
|
|
1672
|
-
stepIndex: 0,
|
|
1673
|
-
sequence: 4,
|
|
1674
|
-
reasoning: "Think",
|
|
1675
|
-
},
|
|
1753
|
+
type: "content.delta",
|
|
1754
|
+
scope: { turnId: "turn_1", runId: "run_1" },
|
|
1755
|
+
data: { partId: "reason_1", kind: "reasoning", delta: "Think" },
|
|
1676
1756
|
},
|
|
1757
|
+
callRequested,
|
|
1758
|
+
completedContent("reasoning", "reason_1", "Think"),
|
|
1677
1759
|
]);
|
|
1678
1760
|
|
|
1679
1761
|
const message = state.messages.find((m) => m.role === "assistant");
|
|
1680
1762
|
expect(message?.parts).toEqual(
|
|
1681
1763
|
expect.arrayContaining([
|
|
1682
1764
|
expect.objectContaining({ type: "reasoning", state: "done" }),
|
|
1683
|
-
expect.objectContaining({ type: "
|
|
1765
|
+
expect.objectContaining({ type: "tool" }),
|
|
1684
1766
|
]),
|
|
1685
1767
|
);
|
|
1686
|
-
|
|
1687
1768
|
const content = convertEveMessages(state, { isRunning: true }).at(
|
|
1688
1769
|
-1,
|
|
1689
1770
|
)?.content;
|
|
1690
1771
|
expect(content).toEqual([
|
|
1772
|
+
expect.objectContaining({ type: "tool-call" }),
|
|
1691
1773
|
expect.objectContaining({
|
|
1692
1774
|
type: "reasoning",
|
|
1693
1775
|
status: { type: "complete" },
|
|
1694
1776
|
}),
|
|
1695
|
-
expect.objectContaining({ type: "tool-call" }),
|
|
1696
1777
|
]);
|
|
1697
1778
|
});
|
|
1698
1779
|
|
|
1699
1780
|
it("keeps a later step's live text part unsettled after an earlier step completed", () => {
|
|
1700
1781
|
const state = replay([
|
|
1701
|
-
...
|
|
1782
|
+
...initialTurnEvents,
|
|
1702
1783
|
{
|
|
1703
|
-
type: "
|
|
1704
|
-
|
|
1705
|
-
data: {
|
|
1706
|
-
turnId: "turn_1",
|
|
1707
|
-
stepIndex: 0,
|
|
1708
|
-
sequence: 2,
|
|
1709
|
-
messageDelta: "First",
|
|
1710
|
-
},
|
|
1784
|
+
type: "content.delta",
|
|
1785
|
+
scope: { turnId: "turn_1", runId: "run_1" },
|
|
1786
|
+
data: { partId: "part_1", kind: "text", delta: "First" },
|
|
1711
1787
|
},
|
|
1788
|
+
completedContent("text", "part_1", "First"),
|
|
1712
1789
|
{
|
|
1713
|
-
type: "
|
|
1714
|
-
meta: eventMeta(3),
|
|
1790
|
+
type: "model.settled",
|
|
1715
1791
|
data: {
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
sequence: 3,
|
|
1792
|
+
runId: "run_1",
|
|
1793
|
+
outcome: "completed",
|
|
1719
1794
|
finishReason: "tool-calls",
|
|
1720
|
-
message: "First",
|
|
1721
1795
|
},
|
|
1722
1796
|
},
|
|
1723
1797
|
{
|
|
1724
|
-
type: "
|
|
1725
|
-
|
|
1726
|
-
data: {
|
|
1727
|
-
turnId: "turn_1",
|
|
1728
|
-
stepIndex: 0,
|
|
1729
|
-
sequence: 4,
|
|
1730
|
-
finishReason: "tool-calls",
|
|
1731
|
-
},
|
|
1798
|
+
type: "model.requested",
|
|
1799
|
+
scope: { turnId: "turn_1" },
|
|
1800
|
+
data: { runId: "run_2", owner: { turnId: "turn_1" } },
|
|
1732
1801
|
},
|
|
1733
1802
|
{
|
|
1734
|
-
type: "
|
|
1735
|
-
|
|
1736
|
-
data: {
|
|
1737
|
-
turnId: "turn_1",
|
|
1738
|
-
stepIndex: 1,
|
|
1739
|
-
sequence: 5,
|
|
1740
|
-
modelId: "test-model",
|
|
1741
|
-
},
|
|
1803
|
+
type: "model.started",
|
|
1804
|
+
scope: { turnId: "turn_1", runId: "run_2" },
|
|
1805
|
+
data: { runId: "run_2", modelId: "test-model", attempt: 0 },
|
|
1742
1806
|
},
|
|
1743
1807
|
{
|
|
1744
|
-
type: "
|
|
1745
|
-
|
|
1746
|
-
data: {
|
|
1747
|
-
turnId: "turn_1",
|
|
1748
|
-
stepIndex: 1,
|
|
1749
|
-
sequence: 6,
|
|
1750
|
-
messageDelta: "Sec",
|
|
1751
|
-
},
|
|
1808
|
+
type: "content.delta",
|
|
1809
|
+
scope: { turnId: "turn_1", runId: "run_2" },
|
|
1810
|
+
data: { partId: "part_2", kind: "text", delta: "Sec" },
|
|
1752
1811
|
},
|
|
1753
1812
|
]);
|
|
1754
1813
|
|
|
1755
1814
|
const assistant = state.messages.find((m) => m.role === "assistant");
|
|
1756
1815
|
expect(assistant?.parts).toEqual([
|
|
1757
|
-
expect.objectContaining({ type: "step-start" }),
|
|
1758
1816
|
expect.objectContaining({
|
|
1759
1817
|
type: "text",
|
|
1760
1818
|
text: "First",
|
|
1761
1819
|
state: "done",
|
|
1762
1820
|
}),
|
|
1763
|
-
expect.objectContaining({ type: "step-start" }),
|
|
1764
1821
|
expect.objectContaining({
|
|
1765
1822
|
type: "text",
|
|
1766
1823
|
text: "Sec",
|
|
1767
1824
|
state: "streaming",
|
|
1768
1825
|
}),
|
|
1769
1826
|
]);
|
|
1770
|
-
|
|
1771
1827
|
const content = convertEveMessages(state, { isRunning: true }).at(
|
|
1772
1828
|
-1,
|
|
1773
1829
|
)?.content;
|
|
@@ -1784,59 +1840,68 @@ describe("convertEveMessages", () => {
|
|
|
1784
1840
|
|
|
1785
1841
|
it("a locally aborted turn keeps its streaming marker and converts to cancelled", () => {
|
|
1786
1842
|
const state = replay(midStreamEvents);
|
|
1787
|
-
|
|
1788
1843
|
const assistant = state.messages.find((m) => m.role === "assistant");
|
|
1789
1844
|
expect(assistant?.metadata?.status).toBe("streaming");
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1845
|
+
expect(
|
|
1846
|
+
convertEveMessages(state, { isRunning: false }).at(-1)?.status,
|
|
1847
|
+
).toEqual({
|
|
1793
1848
|
type: "incomplete",
|
|
1794
1849
|
reason: "cancelled",
|
|
1795
1850
|
});
|
|
1796
1851
|
});
|
|
1797
1852
|
|
|
1798
1853
|
it("a failed session keeps its streaming marker and converts to error", () => {
|
|
1799
|
-
const
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
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" },
|
|
1805
1861
|
},
|
|
1806
|
-
|
|
1807
|
-
|
|
1862
|
+
} as const satisfies SessionStreamEvent;
|
|
1863
|
+
const state = replay([...midStreamEvents, sessionFailure]);
|
|
1808
1864
|
const assistant = state.messages.find((m) => m.role === "assistant");
|
|
1809
1865
|
expect(assistant?.metadata?.status).toBe("streaming");
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1866
|
+
expect(
|
|
1867
|
+
convertEveMessages(state, {
|
|
1868
|
+
isRunning: false,
|
|
1869
|
+
error: new Error("boom"),
|
|
1870
|
+
}).at(-1)?.status,
|
|
1871
|
+
).toEqual({
|
|
1816
1872
|
type: "incomplete",
|
|
1817
1873
|
reason: "error",
|
|
1818
1874
|
error: { code: "unknown", message: "boom" },
|
|
1819
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
|
+
});
|
|
1820
1886
|
});
|
|
1821
1887
|
|
|
1822
1888
|
it("a failed turn stays incomplete after Eve settles its message", () => {
|
|
1823
1889
|
const failureEvent = {
|
|
1824
|
-
type: "turn.
|
|
1825
|
-
meta: eventMeta(
|
|
1890
|
+
type: "turn.settled",
|
|
1891
|
+
meta: eventMeta(8),
|
|
1826
1892
|
data: {
|
|
1827
1893
|
turnId: "turn_1",
|
|
1828
|
-
|
|
1829
|
-
code: "internal",
|
|
1830
|
-
message: "boom",
|
|
1894
|
+
outcome: "failed",
|
|
1895
|
+
error: { code: "internal", message: "boom" },
|
|
1831
1896
|
},
|
|
1832
|
-
} as const satisfies
|
|
1897
|
+
} as const satisfies SessionStreamEvent;
|
|
1833
1898
|
const state = replay([...midStreamEvents, failureEvent]);
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1899
|
+
expect(
|
|
1900
|
+
convertEveMessages(state, {
|
|
1901
|
+
isRunning: false,
|
|
1902
|
+
events: [failureEvent],
|
|
1903
|
+
}).at(-1)?.status,
|
|
1904
|
+
).toEqual({
|
|
1840
1905
|
type: "incomplete",
|
|
1841
1906
|
reason: "error",
|
|
1842
1907
|
error: { code: "internal", message: "boom" },
|
|
@@ -1845,44 +1910,45 @@ describe("convertEveMessages", () => {
|
|
|
1845
1910
|
|
|
1846
1911
|
it("a cancelled turn stays cancelled after Eve settles its message", () => {
|
|
1847
1912
|
const cancelEvent = {
|
|
1848
|
-
type: "turn.
|
|
1849
|
-
meta: eventMeta(
|
|
1850
|
-
data: { turnId: "turn_1",
|
|
1851
|
-
} as const satisfies
|
|
1913
|
+
type: "turn.settled",
|
|
1914
|
+
meta: eventMeta(8),
|
|
1915
|
+
data: { turnId: "turn_1", outcome: "cancelled" },
|
|
1916
|
+
} as const satisfies SessionStreamEvent;
|
|
1852
1917
|
const state = replay([...midStreamEvents, cancelEvent]);
|
|
1853
|
-
|
|
1854
1918
|
expect(
|
|
1855
1919
|
convertEveMessages(state, { isRunning: false }).at(-1)?.status,
|
|
1856
|
-
).toEqual({
|
|
1920
|
+
).toEqual({
|
|
1921
|
+
type: "complete",
|
|
1922
|
+
reason: "stop",
|
|
1923
|
+
});
|
|
1857
1924
|
expect(
|
|
1858
1925
|
convertEveMessages(state, {
|
|
1859
1926
|
isRunning: false,
|
|
1860
1927
|
events: [cancelEvent],
|
|
1861
1928
|
}).at(-1)?.status,
|
|
1862
|
-
).toEqual({
|
|
1929
|
+
).toEqual({
|
|
1930
|
+
type: "incomplete",
|
|
1931
|
+
reason: "cancelled",
|
|
1932
|
+
});
|
|
1863
1933
|
});
|
|
1864
1934
|
|
|
1865
1935
|
it("a completed turn terminalizes the streaming marker and converts to complete", () => {
|
|
1866
1936
|
const state = replay([
|
|
1867
1937
|
...midStreamEvents,
|
|
1938
|
+
completedContent("text", "part_1", "Let me think"),
|
|
1868
1939
|
{
|
|
1869
|
-
type: "
|
|
1870
|
-
meta: eventMeta(3),
|
|
1940
|
+
type: "model.settled",
|
|
1871
1941
|
data: {
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
sequence: 3,
|
|
1942
|
+
runId: "run_1",
|
|
1943
|
+
outcome: "completed",
|
|
1875
1944
|
finishReason: "stop",
|
|
1876
|
-
message: "Let me think",
|
|
1877
1945
|
},
|
|
1878
1946
|
},
|
|
1879
1947
|
{
|
|
1880
|
-
type: "turn.
|
|
1881
|
-
|
|
1882
|
-
data: { turnId: "turn_1", sequence: 4 },
|
|
1948
|
+
type: "turn.settled",
|
|
1949
|
+
data: { turnId: "turn_1", outcome: "completed", reply: ["part_1"] },
|
|
1883
1950
|
},
|
|
1884
1951
|
]);
|
|
1885
|
-
|
|
1886
1952
|
const assistant = state.messages.find((m) => m.role === "assistant");
|
|
1887
1953
|
expect(assistant?.metadata?.status).toBe("complete");
|
|
1888
1954
|
expect(assistant?.parts).toEqual(
|
|
@@ -1890,7 +1956,6 @@ describe("convertEveMessages", () => {
|
|
|
1890
1956
|
expect.objectContaining({ type: "text", state: "done" }),
|
|
1891
1957
|
]),
|
|
1892
1958
|
);
|
|
1893
|
-
|
|
1894
1959
|
const converted = convertEveMessages(state, { isRunning: false });
|
|
1895
1960
|
expect(converted.at(-1)?.status).toEqual({
|
|
1896
1961
|
type: "complete",
|
|
@@ -1918,7 +1983,7 @@ describe("convertEveMessages", () => {
|
|
|
1918
1983
|
parts: [{ type: "text", text: "Hello" }],
|
|
1919
1984
|
},
|
|
1920
1985
|
],
|
|
1921
|
-
} satisfies
|
|
1986
|
+
} satisfies MessageData;
|
|
1922
1987
|
|
|
1923
1988
|
const [message] = convertEveMessages(data, {
|
|
1924
1989
|
getCreatedAt: () => createdAt,
|
|
@@ -1948,11 +2013,42 @@ describe("getEveMessageContent", () => {
|
|
|
1948
2013
|
expect(getEveMessageContent(message)).toBe("Hello");
|
|
1949
2014
|
});
|
|
1950
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
|
+
|
|
1951
2047
|
it("declares the data URL subtype of an image part", () => {
|
|
1952
2048
|
const message = {
|
|
1953
2049
|
...baseAppendMessage,
|
|
1954
2050
|
content: [{ type: "image", image: "data:image/jpeg;base64,/9j/4AAQ" }],
|
|
1955
|
-
} as
|
|
2051
|
+
} as AppendMessage;
|
|
1956
2052
|
|
|
1957
2053
|
expect(getEveMessageContent(message)).toEqual([
|
|
1958
2054
|
{
|
|
@@ -1973,7 +2069,7 @@ describe("getEveMessageContent", () => {
|
|
|
1973
2069
|
"data:application/octet-stream;base64,iVBORw0KGgoAAAANSUhEUgAAAAE=",
|
|
1974
2070
|
},
|
|
1975
2071
|
],
|
|
1976
|
-
} as
|
|
2072
|
+
} as AppendMessage;
|
|
1977
2073
|
|
|
1978
2074
|
expect(getEveMessageContent(message)).toEqual([
|
|
1979
2075
|
{
|
|
@@ -1994,7 +2090,7 @@ describe("getEveMessageContent", () => {
|
|
|
1994
2090
|
filename: "photo",
|
|
1995
2091
|
},
|
|
1996
2092
|
],
|
|
1997
|
-
} as
|
|
2093
|
+
} as AppendMessage;
|
|
1998
2094
|
|
|
1999
2095
|
expect(getEveMessageContent(message)).toEqual([
|
|
2000
2096
|
{
|
|
@@ -2020,7 +2116,7 @@ describe("getEveMessageContent", () => {
|
|
|
2020
2116
|
status: { type: "complete" },
|
|
2021
2117
|
},
|
|
2022
2118
|
],
|
|
2023
|
-
} as
|
|
2119
|
+
} as AppendMessage;
|
|
2024
2120
|
|
|
2025
2121
|
expect(getEveMessageContent(message)).toEqual([
|
|
2026
2122
|
{
|
|
@@ -2120,7 +2216,7 @@ describe("getEveMessageContent", () => {
|
|
|
2120
2216
|
const message = {
|
|
2121
2217
|
...baseAppendMessage,
|
|
2122
2218
|
content: [{ type: "audio", audio: { data: "QUJD", format: "mp3" } }],
|
|
2123
|
-
} as
|
|
2219
|
+
} as AppendMessage;
|
|
2124
2220
|
|
|
2125
2221
|
expect(getEveMessageContent(message)).toEqual([
|
|
2126
2222
|
{
|
|
@@ -2135,7 +2231,7 @@ describe("getEveMessageContent", () => {
|
|
|
2135
2231
|
const message = {
|
|
2136
2232
|
...baseAppendMessage,
|
|
2137
2233
|
content: [{ type: "audio", audio: { data: "QUJD", format: "wav" } }],
|
|
2138
|
-
} as
|
|
2234
|
+
} as AppendMessage;
|
|
2139
2235
|
|
|
2140
2236
|
expect(getEveMessageContent(message)).toEqual([
|
|
2141
2237
|
{
|
|
@@ -2155,7 +2251,7 @@ describe("getEveMessageContent", () => {
|
|
|
2155
2251
|
audio: { data: "data:audio/mpeg;base64,QUJD", format: "mp3" },
|
|
2156
2252
|
},
|
|
2157
2253
|
],
|
|
2158
|
-
} as
|
|
2254
|
+
} as AppendMessage;
|
|
2159
2255
|
|
|
2160
2256
|
expect(getEveMessageContent(message)).toEqual([
|
|
2161
2257
|
{
|
|
@@ -2175,7 +2271,7 @@ describe("getEveMessageContent", () => {
|
|
|
2175
2271
|
audio: { data: "data:;base64,QUJD", format: "mp3" },
|
|
2176
2272
|
},
|
|
2177
2273
|
],
|
|
2178
|
-
} as
|
|
2274
|
+
} as AppendMessage;
|
|
2179
2275
|
|
|
2180
2276
|
expect(getEveMessageContent(message)).toEqual([
|
|
2181
2277
|
{
|
|
@@ -2195,7 +2291,7 @@ describe("getEveMessageContent", () => {
|
|
|
2195
2291
|
audio: { data: "https://cdn.example.com/memo.mp3", format: "mp3" },
|
|
2196
2292
|
},
|
|
2197
2293
|
],
|
|
2198
|
-
} as
|
|
2294
|
+
} as AppendMessage;
|
|
2199
2295
|
|
|
2200
2296
|
expect(getEveMessageContent(message)).toEqual([
|
|
2201
2297
|
{
|
|
@@ -2226,7 +2322,7 @@ describe("getEveMessageContent", () => {
|
|
|
2226
2322
|
status: { type: "complete" },
|
|
2227
2323
|
},
|
|
2228
2324
|
],
|
|
2229
|
-
} as
|
|
2325
|
+
} as AppendMessage;
|
|
2230
2326
|
|
|
2231
2327
|
expect(getEveMessageContent(message)).toEqual([
|
|
2232
2328
|
{
|
|
@@ -2253,7 +2349,7 @@ describe("getEveMessageContent", () => {
|
|
|
2253
2349
|
],
|
|
2254
2350
|
},
|
|
2255
2351
|
],
|
|
2256
|
-
} satisfies
|
|
2352
|
+
} satisfies MessageData);
|
|
2257
2353
|
|
|
2258
2354
|
expect(echoed?.content).toEqual([]);
|
|
2259
2355
|
expect(echoed?.attachments?.map((a) => a.content)).toEqual([
|
|
@@ -2276,7 +2372,7 @@ describe("getEveMessageContent", () => {
|
|
|
2276
2372
|
{ type: "text", text: "hi" },
|
|
2277
2373
|
{ type: "data", name: "chart", data: { x: 1 } },
|
|
2278
2374
|
],
|
|
2279
|
-
} as
|
|
2375
|
+
} as AppendMessage;
|
|
2280
2376
|
|
|
2281
2377
|
expect(getEveMessageContent(message)).toBe("hi");
|
|
2282
2378
|
});
|
|
@@ -2284,7 +2380,7 @@ describe("getEveMessageContent", () => {
|
|
|
2284
2380
|
|
|
2285
2381
|
const approveCancel = [
|
|
2286
2382
|
{ id: "approve", label: "Approve" },
|
|
2287
|
-
{ id: "
|
|
2383
|
+
{ id: "deny", label: "Cancel" },
|
|
2288
2384
|
];
|
|
2289
2385
|
const environments = [
|
|
2290
2386
|
{ id: "staging", label: "Staging" },
|
|
@@ -2292,9 +2388,8 @@ const environments = [
|
|
|
2292
2388
|
];
|
|
2293
2389
|
|
|
2294
2390
|
const withInputRequest = (
|
|
2295
|
-
overrides: Partial<
|
|
2296
|
-
):
|
|
2297
|
-
requestId: "req_1",
|
|
2391
|
+
overrides: Partial<InputRequest> = {},
|
|
2392
|
+
): InputRequest => ({
|
|
2298
2393
|
prompt: "Send the email?",
|
|
2299
2394
|
kind: "question",
|
|
2300
2395
|
...overrides,
|
|
@@ -2312,8 +2407,8 @@ describe("toEveInputResponse", () => {
|
|
|
2312
2407
|
withInputRequest({ display: "confirmation", options: approveCancel }),
|
|
2313
2408
|
),
|
|
2314
2409
|
).toEqual({
|
|
2315
|
-
|
|
2316
|
-
optionId: "
|
|
2410
|
+
interactionId: "req_1",
|
|
2411
|
+
optionId: "deny",
|
|
2317
2412
|
text: "Not yet",
|
|
2318
2413
|
});
|
|
2319
2414
|
});
|
|
@@ -2321,7 +2416,7 @@ describe("toEveInputResponse", () => {
|
|
|
2321
2416
|
it("keeps the shipped one-argument mapping when no request is available", () => {
|
|
2322
2417
|
expect(toEveInputResponse({ approvalId: "req_1", approved: true })).toEqual(
|
|
2323
2418
|
{
|
|
2324
|
-
|
|
2419
|
+
interactionId: "req_1",
|
|
2325
2420
|
optionId: "approve",
|
|
2326
2421
|
},
|
|
2327
2422
|
);
|
|
@@ -2332,7 +2427,7 @@ describe("toEveInputResponse", () => {
|
|
|
2332
2427
|
approved: false,
|
|
2333
2428
|
reason: "Not yet",
|
|
2334
2429
|
}),
|
|
2335
|
-
).toEqual({
|
|
2430
|
+
).toEqual({ interactionId: "req_1", optionId: "deny", text: "Not yet" });
|
|
2336
2431
|
|
|
2337
2432
|
expect(
|
|
2338
2433
|
toEveInputResponse({
|
|
@@ -2340,7 +2435,7 @@ describe("toEveInputResponse", () => {
|
|
|
2340
2435
|
approved: true,
|
|
2341
2436
|
optionId: "staging",
|
|
2342
2437
|
}),
|
|
2343
|
-
).toEqual({
|
|
2438
|
+
).toEqual({ interactionId: "req_1", optionId: "staging" });
|
|
2344
2439
|
});
|
|
2345
2440
|
|
|
2346
2441
|
it("keeps the confirmation mapping when the request carries approve/cancel options", () => {
|
|
@@ -2349,7 +2444,7 @@ describe("toEveInputResponse", () => {
|
|
|
2349
2444
|
{ approvalId: "req_1", approved: true },
|
|
2350
2445
|
withInputRequest({ display: "confirmation", options: approveCancel }),
|
|
2351
2446
|
),
|
|
2352
|
-
).toEqual({
|
|
2447
|
+
).toEqual({ interactionId: "req_1", optionId: "approve" });
|
|
2353
2448
|
});
|
|
2354
2449
|
|
|
2355
2450
|
it("maps a select response to the chosen option id", () => {
|
|
@@ -2358,7 +2453,36 @@ describe("toEveInputResponse", () => {
|
|
|
2358
2453
|
{ approvalId: "req_1", approved: true, optionId: "staging" },
|
|
2359
2454
|
withInputRequest({ display: "select", options: environments }),
|
|
2360
2455
|
),
|
|
2361
|
-
).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
|
+
);
|
|
2362
2486
|
});
|
|
2363
2487
|
|
|
2364
2488
|
it.each([
|
|
@@ -2369,7 +2493,7 @@ describe("toEveInputResponse", () => {
|
|
|
2369
2493
|
"a select display that allows freeform",
|
|
2370
2494
|
{ display: "select", allowFreeform: true, options: environments },
|
|
2371
2495
|
],
|
|
2372
|
-
] satisfies [string, Partial<
|
|
2496
|
+
] satisfies [string, Partial<InputRequest>][])(
|
|
2373
2497
|
"answers a request with %s as free-form text, not a fabricated option id",
|
|
2374
2498
|
(_label, overrides) => {
|
|
2375
2499
|
const response = toEveInputResponse(
|
|
@@ -2378,7 +2502,7 @@ describe("toEveInputResponse", () => {
|
|
|
2378
2502
|
);
|
|
2379
2503
|
|
|
2380
2504
|
expect(response).toEqual({
|
|
2381
|
-
|
|
2505
|
+
interactionId: "req_1",
|
|
2382
2506
|
text: "Quarterly results",
|
|
2383
2507
|
});
|
|
2384
2508
|
expect(response).not.toHaveProperty("optionId");
|
|
@@ -2400,7 +2524,7 @@ describe("toEveInputResponse", () => {
|
|
|
2400
2524
|
{ approvalId: "req_1", approved: true, text: "staging" },
|
|
2401
2525
|
withInputRequest({ display: "text" }),
|
|
2402
2526
|
),
|
|
2403
|
-
).toEqual({
|
|
2527
|
+
).toEqual({ interactionId: "req_1", text: "staging" });
|
|
2404
2528
|
});
|
|
2405
2529
|
|
|
2406
2530
|
it("prefers the response text over the reason it used to be smuggled in", () => {
|
|
@@ -2414,7 +2538,7 @@ describe("toEveInputResponse", () => {
|
|
|
2414
2538
|
},
|
|
2415
2539
|
withInputRequest({ display: "text" }),
|
|
2416
2540
|
),
|
|
2417
|
-
).toEqual({
|
|
2541
|
+
).toEqual({ interactionId: "req_1", text: "staging" });
|
|
2418
2542
|
});
|
|
2419
2543
|
|
|
2420
2544
|
it("never fabricates approve for a text-display request without an answer", () => {
|
|
@@ -2443,9 +2567,6 @@ describe("toEveInputResponse", () => {
|
|
|
2443
2567
|
),
|
|
2444
2568
|
).toThrow(/no option with id "sandbox".*staging, production/s);
|
|
2445
2569
|
|
|
2446
|
-
// A named option the request does not carry outranks the decision, even
|
|
2447
|
-
// when the decision names an option the request does carry: substituting
|
|
2448
|
-
// the literal deny here would discard the choice the caller made.
|
|
2449
2570
|
expect(() =>
|
|
2450
2571
|
toEveInputResponse(
|
|
2451
2572
|
{ approvalId: "req_1", approved: false, optionId: "schedule-later" },
|
|
@@ -2462,7 +2583,7 @@ describe("toEveInputResponse", () => {
|
|
|
2462
2583
|
|
|
2463
2584
|
expect(
|
|
2464
2585
|
toEveInputResponse({ approvalId: "req_1", approved: true }, inputRequest),
|
|
2465
|
-
).toEqual({
|
|
2586
|
+
).toEqual({ interactionId: "req_1", optionId: "approve" });
|
|
2466
2587
|
|
|
2467
2588
|
expect(
|
|
2468
2589
|
toEveInputResponse(
|
|
@@ -2470,7 +2591,7 @@ describe("toEveInputResponse", () => {
|
|
|
2470
2591
|
inputRequest,
|
|
2471
2592
|
),
|
|
2472
2593
|
).toEqual({
|
|
2473
|
-
|
|
2594
|
+
interactionId: "req_1",
|
|
2474
2595
|
optionId: "approve",
|
|
2475
2596
|
text: "Quarterly results",
|
|
2476
2597
|
});
|
|
@@ -2478,25 +2599,25 @@ describe("toEveInputResponse", () => {
|
|
|
2478
2599
|
|
|
2479
2600
|
it("answers a tool-approval by kind even when its options are missing", () => {
|
|
2480
2601
|
const inputRequest = withInputRequest({
|
|
2481
|
-
kind: "
|
|
2602
|
+
kind: "approval",
|
|
2482
2603
|
display: "confirmation",
|
|
2483
2604
|
});
|
|
2484
2605
|
|
|
2485
2606
|
expect(
|
|
2486
2607
|
toEveInputResponse({ approvalId: "req_1", approved: true }, inputRequest),
|
|
2487
|
-
).toEqual({
|
|
2608
|
+
).toEqual({ interactionId: "req_1", optionId: "approve" });
|
|
2488
2609
|
|
|
2489
2610
|
expect(
|
|
2490
2611
|
toEveInputResponse(
|
|
2491
2612
|
{ approvalId: "req_1", approved: false, reason: "not now" },
|
|
2492
2613
|
inputRequest,
|
|
2493
2614
|
),
|
|
2494
|
-
).toEqual({
|
|
2615
|
+
).toEqual({ interactionId: "req_1", optionId: "deny", text: "not now" });
|
|
2495
2616
|
});
|
|
2496
2617
|
|
|
2497
2618
|
it("still requires a declared option when a tool-approval carries its own", () => {
|
|
2498
2619
|
const inputRequest = withInputRequest({
|
|
2499
|
-
kind: "
|
|
2620
|
+
kind: "approval",
|
|
2500
2621
|
display: "confirmation",
|
|
2501
2622
|
options: [{ id: "schedule-later", label: "Schedule later" }],
|
|
2502
2623
|
});
|
|
@@ -2510,7 +2631,7 @@ describe("toEveInputResponse", () => {
|
|
|
2510
2631
|
{ approvalId: "req_1", approved: true, optionId: "schedule-later" },
|
|
2511
2632
|
inputRequest,
|
|
2512
2633
|
),
|
|
2513
|
-
).toEqual({
|
|
2634
|
+
).toEqual({ interactionId: "req_1", optionId: "schedule-later" });
|
|
2514
2635
|
});
|
|
2515
2636
|
|
|
2516
2637
|
it("never answers an optionless confirmation as free-form text", () => {
|
|
@@ -2529,41 +2650,25 @@ describe("toEveInputResponse", () => {
|
|
|
2529
2650
|
});
|
|
2530
2651
|
|
|
2531
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;
|
|
2532
2659
|
const data = {
|
|
2533
2660
|
messages: [
|
|
2534
2661
|
{
|
|
2535
2662
|
id: "a1",
|
|
2536
2663
|
role: "assistant",
|
|
2537
|
-
parts: [
|
|
2538
|
-
{
|
|
2539
|
-
type: "dynamic-tool",
|
|
2540
|
-
state: "approval-requested",
|
|
2541
|
-
toolCallId: "call_1",
|
|
2542
|
-
toolName: "ask_question",
|
|
2543
|
-
input: {},
|
|
2544
|
-
approval: { id: "req_1" },
|
|
2545
|
-
toolMetadata: {
|
|
2546
|
-
eve: {
|
|
2547
|
-
kind: "tool-call",
|
|
2548
|
-
name: "ask_question",
|
|
2549
|
-
inputRequest: {
|
|
2550
|
-
requestId: "req_1",
|
|
2551
|
-
prompt: "What should the subject line be?",
|
|
2552
|
-
kind: "question",
|
|
2553
|
-
display: "text",
|
|
2554
|
-
allowFreeform: true,
|
|
2555
|
-
},
|
|
2556
|
-
},
|
|
2557
|
-
},
|
|
2558
|
-
},
|
|
2559
|
-
],
|
|
2664
|
+
parts: [toolPart({ interaction: approvalInteraction(request) })],
|
|
2560
2665
|
},
|
|
2561
2666
|
],
|
|
2562
|
-
} satisfies
|
|
2667
|
+
} satisfies MessageData;
|
|
2563
2668
|
|
|
2564
2669
|
it("finds the input request by approval id", () => {
|
|
2565
2670
|
expect(findEveInputRequest(data, "req_1")).toMatchObject({
|
|
2566
|
-
|
|
2671
|
+
prompt: "What should the subject line be?",
|
|
2567
2672
|
display: "text",
|
|
2568
2673
|
});
|
|
2569
2674
|
});
|
|
@@ -2572,16 +2677,24 @@ describe("findEveInputRequest", () => {
|
|
|
2572
2677
|
expect(findEveInputRequest(data, "req_404")).toBeUndefined();
|
|
2573
2678
|
});
|
|
2574
2679
|
|
|
2575
|
-
it
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
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
|
+
});
|
|
2581
2686
|
|
|
2687
|
+
it("finds a standalone interaction by its approval id", () => {
|
|
2582
2688
|
expect(
|
|
2583
|
-
findEveInputRequest(
|
|
2584
|
-
|
|
2689
|
+
findEveInputRequest(
|
|
2690
|
+
{
|
|
2691
|
+
messages: [
|
|
2692
|
+
{ id: "a1", role: "assistant", parts: [signInPart(request)] },
|
|
2693
|
+
],
|
|
2694
|
+
},
|
|
2695
|
+
"auth_1",
|
|
2696
|
+
),
|
|
2697
|
+
).toEqual(request);
|
|
2585
2698
|
});
|
|
2586
2699
|
});
|
|
2587
2700
|
|
|
@@ -2590,20 +2703,78 @@ describe("collectInterruptedTurnEvents", () => {
|
|
|
2590
2703
|
({
|
|
2591
2704
|
type: "turn.started",
|
|
2592
2705
|
meta: eventMeta(sequence),
|
|
2593
|
-
data: {
|
|
2594
|
-
|
|
2706
|
+
data: {
|
|
2707
|
+
turnId,
|
|
2708
|
+
cause: { deliveryId: `delivery_${turnId}` },
|
|
2709
|
+
follows: null,
|
|
2710
|
+
},
|
|
2711
|
+
}) as const satisfies SessionStreamEvent;
|
|
2595
2712
|
const failed = (turnId: string, sequence: number) =>
|
|
2596
2713
|
({
|
|
2597
|
-
type: "turn.
|
|
2714
|
+
type: "turn.settled",
|
|
2598
2715
|
meta: eventMeta(sequence),
|
|
2599
|
-
data: {
|
|
2600
|
-
|
|
2716
|
+
data: {
|
|
2717
|
+
turnId,
|
|
2718
|
+
outcome: "failed",
|
|
2719
|
+
error: { code: "internal", message: "boom" },
|
|
2720
|
+
},
|
|
2721
|
+
}) as const satisfies SessionStreamEvent;
|
|
2601
2722
|
const cancelled = (turnId: string, sequence: number) =>
|
|
2602
2723
|
({
|
|
2603
|
-
type: "turn.
|
|
2724
|
+
type: "turn.settled",
|
|
2604
2725
|
meta: eventMeta(sequence),
|
|
2605
|
-
data: { turnId,
|
|
2606
|
-
}) 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
|
+
);
|
|
2607
2778
|
|
|
2608
2779
|
it("scans only appended events and keeps the array until an interruption arrives", () => {
|
|
2609
2780
|
const cache: InterruptedTurnEventCache = {
|
|
@@ -2629,4 +2800,29 @@ describe("collectInterruptedTurnEvents", () => {
|
|
|
2629
2800
|
collectInterruptedTurnEvents([started("t1", 0), failed("t1", 1)], cache);
|
|
2630
2801
|
expect(collectInterruptedTurnEvents([started("t2", 0)], cache)).toEqual([]);
|
|
2631
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
|
+
});
|
|
2632
2828
|
});
|