@assistant-ui/eve 0.0.24 → 0.0.26

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 +146 -117
  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 +925 -729
  25. package/src/convertEveMessages.ts +282 -227
  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,9 +1,11 @@
1
1
  // @vitest-environment jsdom
2
2
 
3
- import { act, type ReactNode } from "react";
4
- import type { AssistantCloud } from "assistant-cloud";
3
+ import { Activity, act, type ReactNode, version } from "react";
4
+ import { AssistantCloud } from "assistant-cloud";
5
+ import { emptySessionView } from "eve/events";
5
6
  import { afterAll, afterEach, expect, it, vi } from "vitest";
6
7
  import type { EveCloudSessions } from "./eveCloudSessions";
8
+ import { createEveDelivery, createEveDeliveryAfter } from "./testUtils";
7
9
 
8
10
  const { mockUseEveAgent, directRuntimeHook, sessionInstances } = vi.hoisted(
9
11
  () => ({
@@ -104,6 +106,9 @@ afterEach(() => {
104
106
  });
105
107
  afterAll(() => vi.unstubAllGlobals());
106
108
 
109
+ // Activity is React 19 only
110
+ const onReact18 = version.startsWith("18.");
111
+
107
112
  const refresh = async (Before: unknown, After: unknown) => {
108
113
  const family: Family = { current: After };
109
114
  renderer!.setRefreshHandler((type) =>
@@ -121,24 +126,23 @@ const refresh = async (Before: unknown, After: unknown) => {
121
126
  };
122
127
 
123
128
  const makeAgent = (send: (content: string) => Promise<void>) => ({
124
- data: {
125
- messages: [
126
- { id: "u1", role: "user", parts: [{ type: "text", text: "earlier" }] },
127
- {
128
- id: "a1",
129
- role: "assistant",
130
- parts: [{ type: "text", text: "earlier answer" }],
131
- },
132
- ],
133
- },
129
+ messages: [
130
+ { id: "u1", role: "user", parts: [{ type: "text", text: "earlier" }] },
131
+ {
132
+ id: "a1",
133
+ role: "assistant",
134
+ parts: [{ type: "text", text: "earlier answer" }],
135
+ },
136
+ ],
137
+ view: emptySessionView(),
134
138
  error: undefined,
135
139
  events: [],
136
140
  session: undefined,
137
- status: "ready",
138
- send: vi.fn(send),
139
- respond: vi.fn(async () => {}),
140
- cancel: vi.fn(async () => ({ status: "no_active_turn" })),
141
- reset: vi.fn(),
141
+ status: "idle",
142
+ send: vi.fn(async (content: string) => createEveDeliveryAfter(send(content))),
143
+ respond: vi.fn(async () => createEveDelivery()),
144
+ cancel: vi.fn(async () => createEveDelivery()),
145
+ newSession: vi.fn(),
142
146
  resume: vi.fn(async () => {}),
143
147
  });
144
148
 
@@ -214,25 +218,29 @@ it("abandons an Eve send still queued when the runtime unmounts", async () => {
214
218
  });
215
219
 
216
220
  const makeCloud = () => {
217
- const create = vi.fn(async () => ({ thread_id: "cloud-1" }));
218
- const cloud = {
219
- threads: {
220
- list: vi.fn(async () => ({ threads: [] })),
221
- create,
222
- update: vi.fn(async () => {}),
223
- delete: vi.fn(async () => {}),
224
- get: vi.fn(),
225
- messages: {
226
- list: vi.fn(async () => ({ messages: [] })),
227
- create: vi.fn(async () => ({ message_id: "cloud-message" })),
228
- update: vi.fn(async () => {}),
229
- },
230
- },
231
- events: { track: vi.fn() },
232
- telemetry: { enabled: false },
233
- runs: { report: vi.fn(async () => {}) },
234
- registerSdk: vi.fn(),
235
- } as unknown as AssistantCloud;
221
+ const cloud = new AssistantCloud({
222
+ baseUrl: "http://localhost",
223
+ apiKey: "test-key",
224
+ userId: "test-user",
225
+ workspaceId: "test-workspace",
226
+ telemetry: false,
227
+ });
228
+ let created = 0;
229
+ vi.spyOn(cloud.threads, "list").mockResolvedValue({ threads: [] });
230
+ const create = vi
231
+ .spyOn(cloud.threads, "create")
232
+ .mockImplementation(async () => ({
233
+ thread_id: `cloud-${++created}`,
234
+ }));
235
+ vi.spyOn(cloud.threads, "update").mockResolvedValue(undefined);
236
+ vi.spyOn(cloud.threads, "delete").mockResolvedValue(undefined);
237
+ vi.spyOn(cloud.threads.messages, "list").mockResolvedValue({ messages: [] });
238
+ vi.spyOn(cloud.threads.messages, "create").mockResolvedValue({
239
+ message_id: "cloud-message",
240
+ });
241
+ vi.spyOn(cloud.events, "track").mockImplementation(() => {});
242
+ vi.spyOn(cloud.runs, "report").mockResolvedValue({ run_id: "test-run" });
243
+ vi.spyOn(cloud, "registerSdk").mockImplementation(() => {});
236
244
  return { cloud, create };
237
245
  };
238
246
 
@@ -242,7 +250,7 @@ it("resolves a pending first Eve session after Fast Refresh", async () => {
242
250
  | ((session: { sessionId: string; streamIndex: number }) => void)
243
251
  | undefined;
244
252
  const agent = makeAgent(async () => {});
245
- agent.data.messages = [];
253
+ agent.messages = [];
246
254
  mockUseEveAgent.mockImplementation((options) => {
247
255
  onSessionChange = options.onSessionChange;
248
256
  return agent;
@@ -340,3 +348,153 @@ it("keeps a first Eve session wait alive when a nested runtimeHook refreshes", a
340
348
  await expect(waiting).resolves.toBe("session-1");
341
349
  view.unmount();
342
350
  });
351
+
352
+ it.skipIf(onReact18)(
353
+ "reads Eve delivery results again after an Activity hides and shows the runtime",
354
+ async () => {
355
+ const agent = makeAgent(async () => {});
356
+ agent.send.mockImplementation(async () => {
357
+ const receipt = createEveDelivery();
358
+ return {
359
+ ...receipt,
360
+ result: (options?: { readonly signal?: AbortSignal }) =>
361
+ options?.signal?.aborted
362
+ ? Promise.reject(options.signal.reason)
363
+ : receipt.result(),
364
+ };
365
+ });
366
+ mockUseEveAgent.mockReturnValue(agent);
367
+ let runtime!: ReturnType<typeof useEveAgentRuntime>;
368
+ const Host = () => {
369
+ runtime = useEveAgentRuntime();
370
+ return null;
371
+ };
372
+ const view = render(
373
+ <Activity mode="visible">
374
+ <Host />
375
+ </Activity>,
376
+ );
377
+ view.rerender(
378
+ <Activity mode="hidden">
379
+ <Host />
380
+ </Activity>,
381
+ );
382
+ await act(async () => {});
383
+ view.rerender(
384
+ <Activity mode="visible">
385
+ <Host />
386
+ </Activity>,
387
+ );
388
+ await act(async () => {});
389
+
390
+ act(() => {
391
+ runtime.thread.composer.setText("after show");
392
+ runtime.thread.composer.send();
393
+ });
394
+ await waitFor(() =>
395
+ expect(agent.send).toHaveBeenCalledWith("after show", undefined),
396
+ );
397
+ await act(async () => {});
398
+ expect(runtime.thread.composer.getState().text).toBe("");
399
+ view.unmount();
400
+ },
401
+ );
402
+
403
+ it.skipIf(onReact18)(
404
+ "keeps a send queued after an Activity re-show behind the turn still in flight",
405
+ async () => {
406
+ let releaseFirst!: () => void;
407
+ const agent = makeAgent(
408
+ vi
409
+ .fn()
410
+ .mockImplementationOnce(
411
+ () =>
412
+ new Promise<void>((resolve) => {
413
+ releaseFirst = resolve;
414
+ }),
415
+ )
416
+ .mockResolvedValue(undefined),
417
+ );
418
+ mockUseEveAgent.mockReturnValue(agent);
419
+ let runtime!: ReturnType<typeof useEveAgentRuntime>;
420
+ const Host = () => {
421
+ runtime = useEveAgentRuntime();
422
+ return null;
423
+ };
424
+ const view = render(
425
+ <Activity mode="visible">
426
+ <Host />
427
+ </Activity>,
428
+ );
429
+
430
+ await act(async () => runtime.thread.append("first"));
431
+ await waitFor(() => expect(agent.send).toHaveBeenCalledTimes(1));
432
+ view.rerender(
433
+ <Activity mode="hidden">
434
+ <Host />
435
+ </Activity>,
436
+ );
437
+ await act(async () => {});
438
+ view.rerender(
439
+ <Activity mode="visible">
440
+ <Host />
441
+ </Activity>,
442
+ );
443
+ await act(async () => {});
444
+
445
+ await act(async () => runtime.thread.append("second"));
446
+ await act(async () => {});
447
+ expect(agent.send).toHaveBeenCalledTimes(1);
448
+
449
+ await act(async () => releaseFirst());
450
+ await waitFor(() => expect(agent.send).toHaveBeenCalledTimes(2));
451
+ expect(agent.send).toHaveBeenNthCalledWith(2, "second", undefined);
452
+ view.unmount();
453
+ },
454
+ );
455
+
456
+ it("keeps the composer empty when a reset ends a send Eve already accepted", async () => {
457
+ const agent = makeAgent(async () => {});
458
+ agent.send.mockImplementation(async () => {
459
+ const receipt = createEveDelivery();
460
+ return {
461
+ ...receipt,
462
+ result: (options?: { readonly signal?: AbortSignal }) =>
463
+ new Promise<never>((_, reject) => {
464
+ options?.signal?.addEventListener("abort", () =>
465
+ reject(options.signal?.reason),
466
+ );
467
+ }),
468
+ };
469
+ });
470
+ mockUseEveAgent.mockReturnValue(agent);
471
+ let runtime!: ReturnType<typeof useEveAgentRuntime>;
472
+ let reset!: () => void;
473
+ const Probe = () => {
474
+ reset = useEveReset();
475
+ return null;
476
+ };
477
+ const Host = () => {
478
+ runtime = useEveAgentRuntime();
479
+ return (
480
+ <AssistantRuntimeProvider runtime={runtime}>
481
+ <Probe />
482
+ </AssistantRuntimeProvider>
483
+ );
484
+ };
485
+ const view = render(<Host />);
486
+
487
+ act(() => {
488
+ runtime.thread.composer.setText("sent");
489
+ runtime.thread.composer.send();
490
+ });
491
+ await waitFor(() =>
492
+ expect(agent.send).toHaveBeenCalledWith("sent", undefined),
493
+ );
494
+ act(() => reset());
495
+ await act(async () => {});
496
+
497
+ expect(agent.newSession).toHaveBeenCalledTimes(1);
498
+ expect(runtime.thread.composer.getState().text).toBe("");
499
+ view.unmount();
500
+ });
@@ -1,48 +1,93 @@
1
1
  // @vitest-environment jsdom
2
2
 
3
3
  import { renderHook, waitFor } from "@testing-library/react";
4
+ import type { SessionEvent } from "eve/client";
4
5
  import { afterEach, describe, expect, it, vi } from "vitest";
5
-
6
- import { createEveSessionFixture } from "./testUtils";
6
+ import { createEveEvents, createEveSessionFixture } from "./testUtils";
7
7
  import { useEveAgentRuntime } from "./useEveAgentRuntime";
8
8
 
9
- const meta = (second: number) => ({
10
- at: `2026-01-02T10:00:0${second}.000Z`,
11
- id: `t1-${second}`,
12
- });
13
-
14
- const interruptedTurn = (ending: Record<string, unknown>) => [
15
- { type: "turn.started", data: { sequence: 1, turnId: "t1" }, meta: meta(0) },
16
- {
17
- type: "message.received",
18
- data: { message: "hi", sequence: 2, turnId: "t1" },
19
- meta: meta(1),
20
- },
21
- {
22
- type: "step.started",
23
- data: { modelId: "m", sequence: 3, stepIndex: 0, turnId: "t1" },
24
- meta: meta(2),
25
- },
26
- {
27
- type: "message.appended",
28
- data: {
29
- messageDelta: "Let me th",
30
- sequence: 4,
31
- stepIndex: 0,
32
- turnId: "t1",
9
+ const renderInterruptedTurn = (
10
+ ending: Extract<SessionEvent, { type: "turn.settled" }>["data"],
11
+ ) => {
12
+ const at = "2026-01-02T10:00:00.000Z";
13
+ const events = createEveEvents([
14
+ { at, event: { type: "session.started", data: {} } },
15
+ {
16
+ at,
17
+ event: { type: "delivery.admitted", data: { deliveryId: "d1" } },
33
18
  },
34
- meta: meta(3),
35
- },
36
- { ...ending, meta: meta(4) },
37
- ];
38
-
39
- const renderInterruptedTurn = (ending: Record<string, unknown>) => {
19
+ {
20
+ at,
21
+ event: {
22
+ type: "turn.started",
23
+ data: { turnId: "t1", cause: { deliveryId: "d1" }, follows: null },
24
+ },
25
+ },
26
+ {
27
+ at,
28
+ event: {
29
+ type: "delivery.consumed",
30
+ data: {
31
+ deliveryId: "d1",
32
+ turnId: "t1",
33
+ parts: [{ kind: "text", text: "hi" }],
34
+ },
35
+ },
36
+ },
37
+ {
38
+ at,
39
+ event: {
40
+ type: "model.requested",
41
+ data: { runId: "r1", owner: { turnId: "t1" } },
42
+ scope: { turnId: "t1" },
43
+ },
44
+ },
45
+ {
46
+ at,
47
+ event: {
48
+ type: "model.started",
49
+ data: { runId: "r1", modelId: "m", attempt: 0 },
50
+ scope: { turnId: "t1", runId: "r1" },
51
+ },
52
+ },
53
+ {
54
+ at,
55
+ event: {
56
+ type: "content.completed",
57
+ data: {
58
+ partId: "p1",
59
+ runId: "r1",
60
+ kind: "text",
61
+ value: "Let me th",
62
+ phase: "reply",
63
+ interrupted: true,
64
+ },
65
+ scope: { turnId: "t1", runId: "r1" },
66
+ },
67
+ },
68
+ {
69
+ at,
70
+ event: {
71
+ type: "model.settled",
72
+ data: { runId: "r1", outcome: "interrupted" },
73
+ scope: { turnId: "t1", runId: "r1" },
74
+ },
75
+ },
76
+ {
77
+ at,
78
+ event: { type: "turn.settled", data: ending, scope: { turnId: "t1" } },
79
+ },
80
+ {
81
+ at,
82
+ event: {
83
+ type: "delivery.settled",
84
+ data: { deliveryId: "d1", turnId: "t1", outcome: "handled" },
85
+ },
86
+ },
87
+ ] satisfies readonly { at: string; event: SessionEvent }[]);
40
88
  const { session } = createEveSessionFixture({});
41
89
  return renderHook(() =>
42
- useEveAgentRuntime({
43
- initialEvents: interruptedTurn(ending) as never,
44
- session,
45
- }),
90
+ useEveAgentRuntime({ initialEvents: events, session }),
46
91
  );
47
92
  };
48
93
 
@@ -51,8 +96,9 @@ afterEach(() => vi.unstubAllGlobals());
51
96
  describe("useEveAgentRuntime with an interrupted turn", () => {
52
97
  it("shows a failed turn's assistant message as an error", async () => {
53
98
  const { result } = renderInterruptedTurn({
54
- type: "turn.failed",
55
- data: { code: "internal", message: "boom", sequence: 5, turnId: "t1" },
99
+ turnId: "t1",
100
+ outcome: "failed",
101
+ error: { code: "internal", message: "boom" },
56
102
  });
57
103
 
58
104
  await waitFor(() =>
@@ -66,8 +112,8 @@ describe("useEveAgentRuntime with an interrupted turn", () => {
66
112
 
67
113
  it("shows a cancelled turn's assistant message as cancelled", async () => {
68
114
  const { result } = renderInterruptedTurn({
69
- type: "turn.cancelled",
70
- data: { sequence: 5, turnId: "t1" },
115
+ turnId: "t1",
116
+ outcome: "cancelled",
71
117
  });
72
118
 
73
119
  await waitFor(() =>
@@ -2,75 +2,36 @@
2
2
 
3
3
  import { act, renderHook } from "@testing-library/react";
4
4
  import { afterEach, describe, expect, it, vi } from "vitest";
5
- import type { HandleMessageStreamEvent } from "eve/client";
6
-
7
- import { createEveSessionFixture } from "./testUtils";
5
+ import {
6
+ createCompletedTurnEvents,
7
+ createEveSessionFixture,
8
+ } from "./testUtils";
8
9
  import { useEveAgentRuntime } from "./useEveAgentRuntime";
9
10
 
10
- const completedTurn = (
11
- turnId: string,
12
- text: string,
13
- reply: string,
14
- sequence: number,
15
- ) =>
16
- [
17
- {
18
- type: "turn.started",
19
- data: { sequence, turnId },
20
- meta: { at: "2026-01-02T10:00:00.000Z", id: `${turnId}-1` },
21
- },
22
- {
23
- type: "message.received",
24
- data: { message: text, sequence: sequence + 1, turnId },
25
- meta: { at: "2026-01-02T10:00:01.000Z", id: `${turnId}-2` },
26
- },
27
- {
28
- type: "step.started",
29
- data: { modelId: "m", sequence: sequence + 2, stepIndex: 0, turnId },
30
- meta: { at: "2026-01-02T10:00:02.000Z", id: `${turnId}-3` },
31
- },
32
- {
33
- type: "message.completed",
34
- data: {
35
- finishReason: "stop",
36
- message: reply,
37
- sequence: sequence + 3,
38
- stepIndex: 0,
39
- turnId,
40
- },
41
- meta: { at: "2026-01-02T10:00:03.000Z", id: `${turnId}-4` },
42
- },
43
- {
44
- type: "turn.completed",
45
- data: { sequence: sequence + 4, turnId },
46
- meta: { at: "2026-01-02T10:00:04.000Z", id: `${turnId}-5` },
47
- },
48
- ] as const satisfies readonly HandleMessageStreamEvent[];
49
-
50
- const knownTurn = completedTurn("t1", "hi", "hello", 1);
51
- const outOfBandTurn = completedTurn("t2", "answered elsewhere", "done", 6);
11
+ const knownTurn = createCompletedTurnEvents({
12
+ turnId: "t1",
13
+ text: "hi",
14
+ reply: "hello",
15
+ });
16
+ const outOfBandTurn = createCompletedTurnEvents({
17
+ turnId: "t2",
18
+ text: "answered elsewhere",
19
+ reply: "done",
20
+ startLine: knownTurn.length,
21
+ follows: "t1",
22
+ });
52
23
 
53
24
  afterEach(() => vi.unstubAllGlobals());
54
25
 
55
26
  describe("useEveAgentRuntime against a durable eve session", () => {
56
27
  it("surfaces a turn the server appended out of band after reloadMainThread", async () => {
57
28
  const { fetch, session } = createEveSessionFixture({
58
- events: [
59
- ...knownTurn,
60
- ...outOfBandTurn,
61
- {
62
- type: "session.waiting",
63
- data: {
64
- continuationToken: "session_resumed",
65
- wait: "next-user-message",
66
- },
67
- meta: { at: "2026-01-02T10:00:05.000Z", id: "waiting" },
68
- },
69
- ],
29
+ events: outOfBandTurn,
30
+ tailIndex: knownTurn.length + outOfBandTurn.length - 1,
70
31
  });
71
32
 
72
33
  const { result } = renderHook(() =>
73
- useEveAgentRuntime({ initialEvents: knownTurn as never, session }),
34
+ useEveAgentRuntime({ initialEvents: knownTurn, session }),
74
35
  );
75
36
  expect(
76
37
  result.current.thread.getState().messages.map((m) => m.role),