@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.
Files changed (37) hide show
  1. package/dist/convertEveMessages.d.ts +40 -51
  2. package/dist/convertEveMessages.d.ts.map +1 -1
  3. package/dist/convertEveMessages.js +151 -122
  4. package/dist/convertEveMessages.js.map +1 -1
  5. package/dist/deriveCreatedAt.d.ts +9 -9
  6. package/dist/deriveCreatedAt.d.ts.map +1 -1
  7. package/dist/deriveCreatedAt.js +25 -34
  8. package/dist/deriveCreatedAt.js.map +1 -1
  9. package/dist/eveCloudSessions.js +1 -2
  10. package/dist/eveExtras.d.ts +4 -2
  11. package/dist/eveExtras.d.ts.map +1 -1
  12. package/dist/eveExtras.js +1 -2
  13. package/dist/eveExtras.js.map +1 -1
  14. package/dist/hooks.d.ts +4 -3
  15. package/dist/hooks.d.ts.map +1 -1
  16. package/dist/hooks.js +5 -5
  17. package/dist/hooks.js.map +1 -1
  18. package/dist/sdkIdentity.js +2 -3
  19. package/dist/useEveAgentRuntime.d.ts +3 -5
  20. package/dist/useEveAgentRuntime.d.ts.map +1 -1
  21. package/dist/useEveAgentRuntime.js +34 -23
  22. package/dist/useEveAgentRuntime.js.map +1 -1
  23. package/package.json +10 -10
  24. package/src/convertEveMessages.test.ts +940 -775
  25. package/src/convertEveMessages.ts +290 -233
  26. package/src/deriveCreatedAt.test.ts +90 -88
  27. package/src/deriveCreatedAt.ts +32 -36
  28. package/src/eveExtras.ts +4 -4
  29. package/src/hooks.ts +4 -3
  30. package/src/testUtils.ts +189 -8
  31. package/src/useEveAgentRuntime.cloud.test.tsx +105 -136
  32. package/src/useEveAgentRuntime.eveResume.test.tsx +22 -104
  33. package/src/useEveAgentRuntime.fast-refresh.test.tsx +195 -37
  34. package/src/useEveAgentRuntime.interruptedTurn.test.tsx +86 -40
  35. package/src/useEveAgentRuntime.refetch.test.tsx +19 -58
  36. package/src/useEveAgentRuntime.test.tsx +372 -291
  37. package/src/useEveAgentRuntime.ts +100 -113
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from "vitest";
2
- import type { MessageStreamEvent } from "eve/client";
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: { sequence: 1, turnId },
15
- meta: { at, id: `evt_${at}` },
16
- }) as const satisfies MessageStreamEvent;
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 messageReceived = (at: string, turnId = TURN) =>
18
+ const deliveryConsumed = (at: string, turnId = TURN) =>
19
19
  ({
20
- type: "message.received",
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
- modelId: "m",
30
- sequence: 3,
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, id: `evt_${at}` },
37
- }) as const satisfies MessageStreamEvent;
26
+ meta: { at, position: { line: 2, index: 0 } },
27
+ }) as const satisfies SessionStreamEvent;
38
28
 
39
- const messageAppended = (at: string, turnId = TURN) =>
29
+ const contextStarted = (at: string, turnId = TURN) =>
40
30
  ({
41
- type: "message.appended",
42
- data: {
43
- messageDelta: "he",
44
- sequence: 4,
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 stepStarted = (at: string, turnId = TURN) =>
36
+ const contentDelta = (at: string, turnId = TURN) =>
52
37
  ({
53
- type: "step.started",
54
- data: { modelId: "m", sequence: 4, stepIndex: 0, turnId },
55
- meta: { at, id: `evt_${at}` },
56
- }) as const satisfies MessageStreamEvent;
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 message.received and the assistant at step.started, skipping the turn's other events", () => {
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
- messageReceived("2026-01-02T10:00:01.000Z"),
64
- compactionRequested("2026-01-02T10:00:02.000Z"),
65
- stepStarted("2026-01-02T10:02:00.000Z"),
66
- stepStarted("2026-01-02T10:03:00.000Z"),
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
- messageReceived("2026-01-02T10:00:01.000Z"),
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, stepStarted("2026-01-02T10:02:00.000Z")],
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 an assistant created by a turn ending before its first step", () => {
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
- messageReceived("2026-01-02T10:00:01.000Z"),
130
- compactionRequested("2026-01-02T10:00:02.000Z"),
131
- messageAppended("2026-01-02T10:02:00.000Z"),
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
- { type: "message.received", data: null, meta: { at: "x", id: "e1" } },
144
- { type: "step.started", meta: { at: "2026-01-01T00:00:00Z", id: "e2" } },
145
- {
146
- type: "message.received",
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
- messageReceived("2026-01-02T10:00:01.000Z"),
175
- stepStarted("2026-01-02T10:02:00.000Z"),
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
- [messageReceived("2026-03-01T09:00:01.000Z")],
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: { sequence: 1, turnId: TURN },
203
- meta: { at: "2026-01-01T00:00:00.000Z", id: "evt_x" },
201
+ data: {},
202
+ meta: {
203
+ at: "2026-01-01T00:00:00.000Z",
204
+ position: { line: 1, index: 0 },
205
+ },
204
206
  },
205
- ] as unknown as MessageStreamEvent[];
207
+ ] as SessionStreamEvent[];
206
208
 
207
209
  const timestamps = collectTurnTimestamps(events, cache);
208
210
 
@@ -1,4 +1,4 @@
1
- import type { MessageStreamEvent } from "eve/client";
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 MessageStreamEvent[];
9
+ lastEvents: readonly SessionStreamEvent[];
10
10
  timestamps: ReadonlyMap<string, TurnTimestamps>;
11
11
  };
12
12
 
13
13
  /**
14
- * The reducer cases eve projects into a message: `message.received` creates
15
- * `${turnId}:user`, and every assistant entry reaches `updateAssistantMessage`,
16
- * which creates `${turnId}:assistant` for a turn that has none. It enumerates
17
- * that reducer across the `>=0.32.0` peer range rather than stating a rule
18
- * about turn ids, because `turn.started` and the compaction events carry one
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<MessageStreamEvent["type"], keyof TurnTimestamps>
21
+ Record<SessionStreamEvent["type"], keyof TurnTimestamps>
24
22
  > = {
25
- "message.received": "user",
26
- "step.started": "assistant",
27
- "reasoning.appended": "assistant",
28
- "reasoning.completed": "assistant",
29
- "action.input.appended": "assistant",
30
- "actions.requested": "assistant",
31
- "input.requested": "assistant",
32
- "action.result": "assistant",
33
- "action.partial": "assistant",
34
- "authorization.required": "assistant",
35
- "authorization.completed": "assistant",
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 (`[...events, event]`), so a snapshot that keeps the
50
- * previous scan's last element by identity shares the whole prefix and the
51
- * scan resumes there. Any other snapshot is re-derived into a fresh map: turn
52
- * ids are per-session sequence numbers that recur after `reset()`, so entries
53
- * must not outlive the log that produced them. Re-derivation is also what
54
- * makes the render-phase cache writes safe when React discards a render.
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 MessageStreamEvent[],
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 data: unknown = (event as { readonly data?: unknown }).data;
83
- if (typeof data !== "object" || data === null) continue;
84
- const turnId = (data as { readonly turnId?: unknown }).turnId;
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 { EveMessageData, UseEveAgentHelpers } from "eve/react";
2
+ import type { UseEveAgentReturn } from "eve/react";
3
3
 
4
4
  export type EveRuntimeExtras = Pick<
5
- UseEveAgentHelpers<EveMessageData>,
6
- "error" | "events" | "session" | "reset"
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 resets the Eve session: aborts any in-flight turn,
31
- * recreates the owned session, and clears events and projected data. Pending
32
- * staged messages and tool execution state are discarded. Safe to render
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 { ClientSession, type MessageStreamEvent } from "eve/client";
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 MessageStreamEvent[];
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 push = (events: readonly MessageStreamEvent[]) => {
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(`${JSON.stringify(event)}\n`));
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": "26",
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 ClientSession(
53
- { host: "http://localhost", resolveHeaders: async () => new Headers() },
54
- { sessionId: "session_resumed", streamIndex: 0 },
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
  };