@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,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
|
-
import type {
|
|
2
|
+
import type { SessionStreamEvent } from "eve/client";
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
5
|
collectTurnTimestamps,
|
|
@@ -11,59 +11,53 @@ const TURN = "turn_1";
|
|
|
11
11
|
const turnStarted = (at: string, turnId = TURN) =>
|
|
12
12
|
({
|
|
13
13
|
type: "turn.started",
|
|
14
|
-
data: {
|
|
15
|
-
meta: { at,
|
|
16
|
-
}) as const satisfies
|
|
14
|
+
data: { turnId, cause: { deliveryId: "delivery_1" }, follows: null },
|
|
15
|
+
meta: { at, position: { line: 1, index: 0 } },
|
|
16
|
+
}) as const satisfies SessionStreamEvent;
|
|
17
17
|
|
|
18
|
-
const
|
|
18
|
+
const deliveryConsumed = (at: string, turnId = TURN) =>
|
|
19
19
|
({
|
|
20
|
-
type: "
|
|
21
|
-
data: { message: "hi", sequence: 2, turnId },
|
|
22
|
-
meta: { at, id: `evt_${at}` },
|
|
23
|
-
}) as const satisfies MessageStreamEvent;
|
|
24
|
-
|
|
25
|
-
const compactionRequested = (at: string, turnId = TURN) =>
|
|
26
|
-
({
|
|
27
|
-
type: "compaction.requested",
|
|
20
|
+
type: "delivery.consumed",
|
|
28
21
|
data: {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
sessionId: "s",
|
|
32
|
-
stepIndex: 0,
|
|
22
|
+
deliveryId: "delivery_1",
|
|
23
|
+
parts: [{ kind: "text", text: "hi" }],
|
|
33
24
|
turnId,
|
|
34
|
-
usageInputTokens: null,
|
|
35
25
|
},
|
|
36
|
-
meta: { at,
|
|
37
|
-
}) as const satisfies
|
|
26
|
+
meta: { at, position: { line: 2, index: 0 } },
|
|
27
|
+
}) as const satisfies SessionStreamEvent;
|
|
38
28
|
|
|
39
|
-
const
|
|
29
|
+
const contextStarted = (at: string, turnId = TURN) =>
|
|
40
30
|
({
|
|
41
|
-
type: "
|
|
42
|
-
data: {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
stepIndex: 0,
|
|
46
|
-
turnId,
|
|
47
|
-
},
|
|
48
|
-
meta: { at, id: `evt_${at}` },
|
|
49
|
-
}) as const satisfies MessageStreamEvent;
|
|
31
|
+
type: "context.started",
|
|
32
|
+
data: { changeId: "change_1", kind: "compaction", turnId },
|
|
33
|
+
meta: { at, position: { line: 3, index: 0 } },
|
|
34
|
+
}) as const satisfies SessionStreamEvent;
|
|
50
35
|
|
|
51
|
-
const
|
|
36
|
+
const contentDelta = (at: string, turnId = TURN) =>
|
|
52
37
|
({
|
|
53
|
-
type: "
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
38
|
+
type: "content.delta",
|
|
39
|
+
scope: { turnId },
|
|
40
|
+
data: { partId: "part_1", kind: "text", delta: "he" },
|
|
41
|
+
meta: { at, position: { line: 4, index: 0 } },
|
|
42
|
+
}) as const satisfies SessionStreamEvent;
|
|
43
|
+
|
|
44
|
+
const modelStarted = (at: string, turnId = TURN) =>
|
|
45
|
+
({
|
|
46
|
+
type: "model.started",
|
|
47
|
+
scope: { turnId },
|
|
48
|
+
data: { runId: "run_1", modelId: "m", attempt: 0 },
|
|
49
|
+
meta: { at, position: { line: 4, index: 0 } },
|
|
50
|
+
}) as const satisfies SessionStreamEvent;
|
|
57
51
|
|
|
58
52
|
describe("collectTurnTimestamps", () => {
|
|
59
|
-
it("stamps the user at
|
|
53
|
+
it("stamps the user at delivery.consumed and the assistant at model.started, skipping the turn's other events", () => {
|
|
60
54
|
const timestamps = collectTurnTimestamps(
|
|
61
55
|
[
|
|
62
56
|
turnStarted("2026-01-02T10:00:00.000Z"),
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
57
|
+
deliveryConsumed("2026-01-02T10:00:01.000Z"),
|
|
58
|
+
contextStarted("2026-01-02T10:00:02.000Z"),
|
|
59
|
+
modelStarted("2026-01-02T10:02:00.000Z"),
|
|
60
|
+
modelStarted("2026-01-02T10:03:00.000Z"),
|
|
67
61
|
],
|
|
68
62
|
createTurnTimestampCache(),
|
|
69
63
|
);
|
|
@@ -81,7 +75,7 @@ describe("collectTurnTimestamps", () => {
|
|
|
81
75
|
const cache = createTurnTimestampCache();
|
|
82
76
|
const events = [
|
|
83
77
|
turnStarted("2026-01-02T10:00:00.000Z"),
|
|
84
|
-
|
|
78
|
+
deliveryConsumed("2026-01-02T10:00:01.000Z"),
|
|
85
79
|
];
|
|
86
80
|
const first = collectTurnTimestamps(events, cache);
|
|
87
81
|
expect(first.get(TURN)?.assistant).toBe(undefined);
|
|
@@ -93,7 +87,7 @@ describe("collectTurnTimestamps", () => {
|
|
|
93
87
|
expect(collectTurnTimestamps(quiet, cache)).toBe(first);
|
|
94
88
|
|
|
95
89
|
const timestamps = collectTurnTimestamps(
|
|
96
|
-
[...quiet,
|
|
90
|
+
[...quiet, modelStarted("2026-01-02T10:02:00.000Z")],
|
|
97
91
|
cache,
|
|
98
92
|
);
|
|
99
93
|
expect(timestamps.get(TURN)?.user).toEqual(
|
|
@@ -104,31 +98,13 @@ describe("collectTurnTimestamps", () => {
|
|
|
104
98
|
);
|
|
105
99
|
});
|
|
106
100
|
|
|
107
|
-
it("stamps
|
|
108
|
-
const timestamps = collectTurnTimestamps(
|
|
109
|
-
[
|
|
110
|
-
messageReceived("2026-01-02T10:00:01.000Z"),
|
|
111
|
-
{
|
|
112
|
-
type: "turn.cancelled",
|
|
113
|
-
data: { sequence: 3, turnId: TURN },
|
|
114
|
-
meta: { at: "2026-01-02T10:00:05.000Z", id: "evt_cancel" },
|
|
115
|
-
},
|
|
116
|
-
],
|
|
117
|
-
createTurnTimestampCache(),
|
|
118
|
-
);
|
|
119
|
-
|
|
120
|
-
expect(timestamps.get(TURN)?.assistant).toEqual(
|
|
121
|
-
new Date("2026-01-02T10:00:05.000Z"),
|
|
122
|
-
);
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it("stamps the assistant from the turn's first message-creating event when no step.started precedes it", () => {
|
|
101
|
+
it("stamps the assistant from the turn's first content event when no model.started precedes it", () => {
|
|
126
102
|
const timestamps = collectTurnTimestamps(
|
|
127
103
|
[
|
|
128
104
|
turnStarted("2026-01-02T10:00:00.000Z"),
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
105
|
+
deliveryConsumed("2026-01-02T10:00:01.000Z"),
|
|
106
|
+
contextStarted("2026-01-02T10:00:02.000Z"),
|
|
107
|
+
contentDelta("2026-01-02T10:02:00.000Z"),
|
|
132
108
|
],
|
|
133
109
|
createTurnTimestampCache(),
|
|
134
110
|
);
|
|
@@ -139,22 +115,11 @@ describe("collectTurnTimestamps", () => {
|
|
|
139
115
|
});
|
|
140
116
|
|
|
141
117
|
it("skips malformed events without throwing", () => {
|
|
142
|
-
const events = [
|
|
143
|
-
|
|
144
|
-
{
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
data: { turnId: 42 },
|
|
148
|
-
meta: { at: "x", id: "e3" },
|
|
149
|
-
},
|
|
150
|
-
{
|
|
151
|
-
type: "message.received",
|
|
152
|
-
data: { message: "hi", sequence: 2, turnId: TURN },
|
|
153
|
-
meta: { at: "not a date", id: "e4" },
|
|
154
|
-
},
|
|
155
|
-
{ type: "step.started", data: { stepIndex: 0, turnId: TURN } },
|
|
156
|
-
stepStarted("2026-01-02T10:02:00.000Z"),
|
|
157
|
-
] as never as readonly MessageStreamEvent[];
|
|
118
|
+
const events: readonly SessionStreamEvent[] = [
|
|
119
|
+
deliveryConsumed("not a date"),
|
|
120
|
+
{ ...modelStarted("2026-01-02T10:01:00.000Z"), scope: {} },
|
|
121
|
+
modelStarted("2026-01-02T10:02:00.000Z"),
|
|
122
|
+
];
|
|
158
123
|
|
|
159
124
|
const timestamps = collectTurnTimestamps(
|
|
160
125
|
events,
|
|
@@ -171,14 +136,14 @@ describe("collectTurnTimestamps", () => {
|
|
|
171
136
|
const cache = createTurnTimestampCache();
|
|
172
137
|
collectTurnTimestamps(
|
|
173
138
|
[
|
|
174
|
-
|
|
175
|
-
|
|
139
|
+
deliveryConsumed("2026-01-02T10:00:01.000Z"),
|
|
140
|
+
modelStarted("2026-01-02T10:02:00.000Z"),
|
|
176
141
|
],
|
|
177
142
|
cache,
|
|
178
143
|
);
|
|
179
144
|
|
|
180
145
|
const timestamps = collectTurnTimestamps(
|
|
181
|
-
[
|
|
146
|
+
[deliveryConsumed("2026-03-01T09:00:01.000Z")],
|
|
182
147
|
cache,
|
|
183
148
|
);
|
|
184
149
|
|
|
@@ -187,11 +152,45 @@ describe("collectTurnTimestamps", () => {
|
|
|
187
152
|
);
|
|
188
153
|
expect(timestamps.get(TURN)?.assistant).toBe(undefined);
|
|
189
154
|
});
|
|
155
|
+
|
|
156
|
+
it("ignores empty deliveries and stamps the first later delivery with parts", () => {
|
|
157
|
+
const empty = {
|
|
158
|
+
...deliveryConsumed("2026-01-02T10:00:01.000Z"),
|
|
159
|
+
data: { deliveryId: "delivery_1", turnId: TURN, parts: [] },
|
|
160
|
+
} satisfies SessionStreamEvent;
|
|
161
|
+
const timestamps = collectTurnTimestamps(
|
|
162
|
+
[empty, deliveryConsumed("2026-01-02T10:00:02.000Z")],
|
|
163
|
+
createTurnTimestampCache(),
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
expect(timestamps.get(TURN)?.user).toEqual(
|
|
167
|
+
new Date("2026-01-02T10:00:02.000Z"),
|
|
168
|
+
);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("resumes after an event without a timestamp and keeps the first valid assistant time", () => {
|
|
172
|
+
const cache = createTurnTimestampCache();
|
|
173
|
+
const missingTime = {
|
|
174
|
+
...contentDelta("2026-01-02T10:01:00.000Z"),
|
|
175
|
+
meta: { position: { line: 4, index: 0 } },
|
|
176
|
+
} as unknown as SessionStreamEvent;
|
|
177
|
+
const firstEvents = [missingTime];
|
|
178
|
+
const first = collectTurnTimestamps(firstEvents, cache);
|
|
179
|
+
|
|
180
|
+
expect(first.get(TURN)).toBeUndefined();
|
|
181
|
+
expect(collectTurnTimestamps(firstEvents, cache)).toBe(first);
|
|
182
|
+
|
|
183
|
+
const appended = [...firstEvents, modelStarted("2026-01-02T10:02:00.000Z")];
|
|
184
|
+
const second = collectTurnTimestamps(appended, cache);
|
|
185
|
+
|
|
186
|
+
expect(second.get(TURN)?.assistant).toEqual(
|
|
187
|
+
new Date("2026-01-02T10:02:00.000Z"),
|
|
188
|
+
);
|
|
189
|
+
expect(collectTurnTimestamps(appended, cache)).toBe(second);
|
|
190
|
+
});
|
|
190
191
|
});
|
|
191
192
|
|
|
192
193
|
describe("collectTurnTimestamps prototype-inherited event types", () => {
|
|
193
|
-
// `type` arrives off the wire, so a lookup on a plain object literal can
|
|
194
|
-
// resolve an inherited member instead of missing.
|
|
195
194
|
it.each(["__proto__", "constructor", "toString"])(
|
|
196
195
|
"ignores an event whose type is %s",
|
|
197
196
|
(type) => {
|
|
@@ -199,10 +198,13 @@ describe("collectTurnTimestamps prototype-inherited event types", () => {
|
|
|
199
198
|
const events = [
|
|
200
199
|
{
|
|
201
200
|
type,
|
|
202
|
-
data: {
|
|
203
|
-
meta: {
|
|
201
|
+
data: {},
|
|
202
|
+
meta: {
|
|
203
|
+
at: "2026-01-01T00:00:00.000Z",
|
|
204
|
+
position: { line: 1, index: 0 },
|
|
205
|
+
},
|
|
204
206
|
},
|
|
205
|
-
] as
|
|
207
|
+
] as SessionStreamEvent[];
|
|
206
208
|
|
|
207
209
|
const timestamps = collectTurnTimestamps(events, cache);
|
|
208
210
|
|
package/src/deriveCreatedAt.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { SessionStreamEvent } from "eve/client";
|
|
2
2
|
|
|
3
3
|
export type TurnTimestamps = {
|
|
4
4
|
readonly user?: Date;
|
|
@@ -6,38 +6,31 @@ export type TurnTimestamps = {
|
|
|
6
6
|
};
|
|
7
7
|
|
|
8
8
|
export type TurnTimestampCache = {
|
|
9
|
-
lastEvents: readonly
|
|
9
|
+
lastEvents: readonly SessionStreamEvent[];
|
|
10
10
|
timestamps: ReadonlyMap<string, TurnTimestamps>;
|
|
11
11
|
};
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
* The
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* before the model runs. An event eve adds to the creating set later is absent
|
|
20
|
-
* here and falls back to the wall clock instead of stamping a wrong time.
|
|
14
|
+
* The facts eve projects into a message: a `delivery.consumed` carrying parts
|
|
15
|
+
* opens the turn's user message, and the model, content, call, and interaction
|
|
16
|
+
* facts open its assistant message. A fact eve adds to the creating set later
|
|
17
|
+
* is absent here and falls back to the wall clock instead of stamping a wrong
|
|
18
|
+
* time.
|
|
21
19
|
*/
|
|
22
20
|
const ROLE_BY_EVENT_TYPE: Partial<
|
|
23
|
-
Record<
|
|
21
|
+
Record<SessionStreamEvent["type"], keyof TurnTimestamps>
|
|
24
22
|
> = {
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"message.appended": "assistant",
|
|
37
|
-
"message.completed": "assistant",
|
|
38
|
-
"result.completed": "assistant",
|
|
39
|
-
"turn.completed": "assistant",
|
|
40
|
-
"turn.cancelled": "assistant",
|
|
23
|
+
"delivery.consumed": "user",
|
|
24
|
+
"model.requested": "assistant",
|
|
25
|
+
"model.started": "assistant",
|
|
26
|
+
"content.delta": "assistant",
|
|
27
|
+
"content.completed": "assistant",
|
|
28
|
+
"call.input": "assistant",
|
|
29
|
+
"call.requested": "assistant",
|
|
30
|
+
"call.started": "assistant",
|
|
31
|
+
"call.settled": "assistant",
|
|
32
|
+
"call.progress": "assistant",
|
|
33
|
+
"interaction.opened": "assistant",
|
|
41
34
|
};
|
|
42
35
|
|
|
43
36
|
export const createTurnTimestampCache = (): TurnTimestampCache => ({
|
|
@@ -46,15 +39,15 @@ export const createTurnTimestampCache = (): TurnTimestampCache => ({
|
|
|
46
39
|
});
|
|
47
40
|
|
|
48
41
|
/**
|
|
49
|
-
* Eve appends to its log
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
42
|
+
* Eve appends to its log, so a snapshot that keeps the previous scan's last
|
|
43
|
+
* element by identity shares the whole prefix and the scan resumes there. Any
|
|
44
|
+
* other snapshot is re-derived into a fresh map: turn ids are per-session
|
|
45
|
+
* sequence numbers that recur after `newSession()`, so entries must not
|
|
46
|
+
* outlive the log that produced them. Re-derivation is also what makes the
|
|
47
|
+
* render-phase cache writes safe when React discards a render.
|
|
55
48
|
*/
|
|
56
49
|
export const collectTurnTimestamps = (
|
|
57
|
-
events: readonly
|
|
50
|
+
events: readonly SessionStreamEvent[],
|
|
58
51
|
cache: TurnTimestampCache,
|
|
59
52
|
): ReadonlyMap<string, TurnTimestamps> => {
|
|
60
53
|
if (events === cache.lastEvents) return cache.timestamps;
|
|
@@ -79,9 +72,12 @@ export const collectTurnTimestamps = (
|
|
|
79
72
|
continue;
|
|
80
73
|
const role = ROLE_BY_EVENT_TYPE[event.type];
|
|
81
74
|
if (role === undefined) continue;
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
75
|
+
const turnId =
|
|
76
|
+
event.type === "delivery.consumed"
|
|
77
|
+
? event.data.turnId
|
|
78
|
+
: event.scope?.turnId;
|
|
79
|
+
if (event.type === "delivery.consumed" && event.data.parts.length === 0)
|
|
80
|
+
continue;
|
|
85
81
|
if (typeof turnId !== "string") continue;
|
|
86
82
|
const known = timestamps.get(turnId);
|
|
87
83
|
if (known?.[role] !== undefined) continue;
|
package/src/eveExtras.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { createRuntimeExtras } from "@assistant-ui/core/react";
|
|
2
|
-
import type {
|
|
2
|
+
import type { UseEveAgentReturn } from "eve/react";
|
|
3
3
|
|
|
4
4
|
export type EveRuntimeExtras = Pick<
|
|
5
|
-
|
|
6
|
-
"error" | "events" | "session"
|
|
7
|
-
|
|
5
|
+
UseEveAgentReturn,
|
|
6
|
+
"error" | "events" | "session"
|
|
7
|
+
> & { readonly reset: UseEveAgentReturn["newSession"] };
|
|
8
8
|
|
|
9
9
|
export const eveExtras =
|
|
10
10
|
createRuntimeExtras<EveRuntimeExtras>("useEveAgentRuntime");
|
package/src/hooks.ts
CHANGED
|
@@ -27,9 +27,10 @@ export const useEveEvents = (): EveRuntimeExtras["events"] =>
|
|
|
27
27
|
eveExtras.use((e) => e.events, EMPTY_EVENTS);
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
|
-
* Returns a function that
|
|
31
|
-
*
|
|
32
|
-
*
|
|
30
|
+
* Returns a function that starts a new Eve session: it stops reading the
|
|
31
|
+
* current session and clears its events and messages, without cancelling a
|
|
32
|
+
* turn still running on the server. Pending staged messages and tool execution
|
|
33
|
+
* state are discarded. Safe to render
|
|
33
34
|
* outside an Eve runtime; invoking the returned function there throws.
|
|
34
35
|
*
|
|
35
36
|
* With `cloud`, it starts a new thread instead, leaving the current thread and its session in the list.
|
package/src/testUtils.ts
CHANGED
|
@@ -1,25 +1,206 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Client, type SessionEvent, type SessionStreamEvent } from "eve/client";
|
|
2
|
+
import type { Delivery } from "eve/sessions";
|
|
2
3
|
import { vi } from "vitest";
|
|
3
4
|
|
|
5
|
+
export const createEveDelivery = (
|
|
6
|
+
status: "completed" | "failed" = "completed",
|
|
7
|
+
): Delivery => ({
|
|
8
|
+
sessionId: "session_resumed",
|
|
9
|
+
deliveryId: "delivery_1",
|
|
10
|
+
async *stream() {},
|
|
11
|
+
async result() {
|
|
12
|
+
const result = {
|
|
13
|
+
sessionId: "session_resumed",
|
|
14
|
+
deliveryId: "delivery_1",
|
|
15
|
+
outcome: status === "failed" ? "failed" : "handled",
|
|
16
|
+
sessionEnded: false,
|
|
17
|
+
reply: undefined,
|
|
18
|
+
data: undefined,
|
|
19
|
+
interactions: [],
|
|
20
|
+
} as const;
|
|
21
|
+
return status === "failed"
|
|
22
|
+
? { ...result, status, error: { code: "internal", message: "boom" } }
|
|
23
|
+
: { ...result, status };
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const isDelivery = (value: unknown): value is Delivery =>
|
|
28
|
+
typeof value === "object" &&
|
|
29
|
+
value !== null &&
|
|
30
|
+
"result" in value &&
|
|
31
|
+
typeof value.result === "function";
|
|
32
|
+
|
|
33
|
+
export const createEveDeliveryAfter = (pending: Promise<unknown>): Delivery => {
|
|
34
|
+
const receipt = createEveDelivery();
|
|
35
|
+
return {
|
|
36
|
+
...receipt,
|
|
37
|
+
async result(options) {
|
|
38
|
+
const signal = options?.signal;
|
|
39
|
+
const value = await new Promise<unknown>((resolve, reject) => {
|
|
40
|
+
if (signal?.aborted) {
|
|
41
|
+
reject(new DOMException("The operation was aborted.", "AbortError"));
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const onAbort = () => {
|
|
45
|
+
signal?.removeEventListener("abort", onAbort);
|
|
46
|
+
reject(new DOMException("The operation was aborted.", "AbortError"));
|
|
47
|
+
};
|
|
48
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
49
|
+
pending.then(
|
|
50
|
+
(result) => {
|
|
51
|
+
signal?.removeEventListener("abort", onAbort);
|
|
52
|
+
resolve(result);
|
|
53
|
+
},
|
|
54
|
+
(error) => {
|
|
55
|
+
signal?.removeEventListener("abort", onAbort);
|
|
56
|
+
reject(error);
|
|
57
|
+
},
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
return isDelivery(value)
|
|
61
|
+
? value.result(options)
|
|
62
|
+
: receipt.result(options);
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export const createEveEvents = (
|
|
68
|
+
entries: readonly { at: string; event: SessionEvent }[],
|
|
69
|
+
startLine = 0,
|
|
70
|
+
): readonly SessionStreamEvent[] =>
|
|
71
|
+
entries.map(({ at, event }, index) => ({
|
|
72
|
+
...event,
|
|
73
|
+
meta: { at, position: { line: startLine + index, index: 0 } },
|
|
74
|
+
}));
|
|
75
|
+
|
|
76
|
+
export const createCompletedTurnEvents = ({
|
|
77
|
+
turnId,
|
|
78
|
+
text,
|
|
79
|
+
reply,
|
|
80
|
+
deliveryId = `delivery_${turnId}`,
|
|
81
|
+
startLine = 0,
|
|
82
|
+
follows = null,
|
|
83
|
+
userAt = "2026-01-02T10:00:01.000Z",
|
|
84
|
+
assistantAt = "2026-01-02T10:00:03.000Z",
|
|
85
|
+
}: {
|
|
86
|
+
turnId: string;
|
|
87
|
+
text: string;
|
|
88
|
+
reply: string;
|
|
89
|
+
deliveryId?: string;
|
|
90
|
+
startLine?: number;
|
|
91
|
+
follows?: string | null;
|
|
92
|
+
userAt?: string;
|
|
93
|
+
assistantAt?: string;
|
|
94
|
+
}): readonly SessionStreamEvent[] => {
|
|
95
|
+
const runId = `run_${turnId}`;
|
|
96
|
+
const partId = `part_${turnId}`;
|
|
97
|
+
return createEveEvents(
|
|
98
|
+
[
|
|
99
|
+
...(startLine === 0
|
|
100
|
+
? [
|
|
101
|
+
{
|
|
102
|
+
at: userAt,
|
|
103
|
+
event: { type: "session.started" as const, data: {} },
|
|
104
|
+
},
|
|
105
|
+
]
|
|
106
|
+
: []),
|
|
107
|
+
{
|
|
108
|
+
at: userAt,
|
|
109
|
+
event: { type: "delivery.admitted", data: { deliveryId } },
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
at: userAt,
|
|
113
|
+
event: {
|
|
114
|
+
type: "turn.started",
|
|
115
|
+
data: { turnId, cause: { deliveryId }, follows },
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
at: userAt,
|
|
120
|
+
event: {
|
|
121
|
+
type: "delivery.consumed",
|
|
122
|
+
data: { deliveryId, turnId, parts: [{ kind: "text", text }] },
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
at: assistantAt,
|
|
127
|
+
event: {
|
|
128
|
+
type: "model.requested",
|
|
129
|
+
data: { runId, owner: { turnId } },
|
|
130
|
+
scope: { turnId },
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
at: assistantAt,
|
|
135
|
+
event: {
|
|
136
|
+
type: "model.started",
|
|
137
|
+
data: { runId, modelId: "test-model", attempt: 0 },
|
|
138
|
+
scope: { turnId, runId },
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
at: assistantAt,
|
|
143
|
+
event: {
|
|
144
|
+
type: "content.completed",
|
|
145
|
+
data: { partId, runId, kind: "text", value: reply, phase: "reply" },
|
|
146
|
+
scope: { turnId, runId },
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
at: assistantAt,
|
|
151
|
+
event: {
|
|
152
|
+
type: "model.settled",
|
|
153
|
+
data: { runId, outcome: "completed", finishReason: "stop" },
|
|
154
|
+
scope: { turnId, runId },
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
at: assistantAt,
|
|
159
|
+
event: {
|
|
160
|
+
type: "turn.settled",
|
|
161
|
+
data: { turnId, outcome: "completed", reply: [partId] },
|
|
162
|
+
scope: { turnId },
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
at: assistantAt,
|
|
167
|
+
event: {
|
|
168
|
+
type: "delivery.settled",
|
|
169
|
+
data: { deliveryId, outcome: "handled", turnId },
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
] satisfies readonly { at: string; event: SessionEvent }[],
|
|
173
|
+
startLine,
|
|
174
|
+
);
|
|
175
|
+
};
|
|
176
|
+
|
|
4
177
|
export const createEveSessionFixture = ({
|
|
5
178
|
events = [],
|
|
6
179
|
tailIndex = events.length - 1,
|
|
7
180
|
onSend,
|
|
8
181
|
}: {
|
|
9
|
-
events?: readonly
|
|
182
|
+
events?: readonly SessionStreamEvent[];
|
|
10
183
|
tailIndex?: number;
|
|
11
184
|
onSend?: () => void;
|
|
12
185
|
}) => {
|
|
13
186
|
const encoder = new TextEncoder();
|
|
14
187
|
let controller: ReadableStreamDefaultController<Uint8Array> | undefined;
|
|
15
188
|
const pending = [...events];
|
|
16
|
-
const
|
|
189
|
+
const encode = (event: SessionStreamEvent) => {
|
|
190
|
+
const { meta, ...fact } = event;
|
|
191
|
+
return `${JSON.stringify(
|
|
192
|
+
event.type.endsWith(".delta") || event.type.endsWith(".progress")
|
|
193
|
+
? { progress: fact }
|
|
194
|
+
: { at: meta.at, facts: [fact] },
|
|
195
|
+
)}\n`;
|
|
196
|
+
};
|
|
197
|
+
const push = (events: readonly SessionStreamEvent[]) => {
|
|
17
198
|
if (controller === undefined) {
|
|
18
199
|
pending.push(...events);
|
|
19
200
|
return;
|
|
20
201
|
}
|
|
21
202
|
for (const event of events) {
|
|
22
|
-
controller.enqueue(encoder.encode(
|
|
203
|
+
controller.enqueue(encoder.encode(encode(event)));
|
|
23
204
|
}
|
|
24
205
|
};
|
|
25
206
|
const fetch = vi.fn(async (_input: RequestInfo | URL, init?: RequestInit) => {
|
|
@@ -42,16 +223,16 @@ export const createEveSessionFixture = ({
|
|
|
42
223
|
}),
|
|
43
224
|
{
|
|
44
225
|
headers: {
|
|
45
|
-
"x-eve-stream-version": "
|
|
226
|
+
"x-eve-stream-version": "27",
|
|
46
227
|
"x-eve-stream-tail-index": String(tailIndex),
|
|
47
228
|
},
|
|
48
229
|
},
|
|
49
230
|
);
|
|
50
231
|
});
|
|
51
232
|
vi.stubGlobal("fetch", fetch);
|
|
52
|
-
const session = new
|
|
53
|
-
|
|
54
|
-
{
|
|
233
|
+
const session = new Client({ baseUrl: "http://localhost" }).sessions.attach(
|
|
234
|
+
"session_resumed",
|
|
235
|
+
{ streamIndex: 0 },
|
|
55
236
|
);
|
|
56
237
|
return { fetch, push, session };
|
|
57
238
|
};
|