@assistant-ui/eve 0.0.13 → 0.0.15

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.
@@ -14,6 +14,7 @@ vi.mock("eve/react", async (importOriginal) => ({
14
14
 
15
15
  import type { EveMessageData } from "eve/react";
16
16
  import { useEveAgentRuntime } from "./useEveAgentRuntime";
17
+ import { eveExtras } from "./eveExtras";
17
18
 
18
19
  const stuckStreamingData: EveMessageData = {
19
20
  messages: [
@@ -34,6 +35,7 @@ const createAgent = (overrides: Record<string, unknown>) => ({
34
35
  session: undefined,
35
36
  status: "ready",
36
37
  send: vi.fn(),
38
+ respond: vi.fn(),
37
39
  stop: vi.fn(),
38
40
  reset: vi.fn(),
39
41
  ...overrides,
@@ -160,8 +162,7 @@ describe("useEveAgentRuntime status forwarding", () => {
160
162
  });
161
163
 
162
164
  await waitFor(() => {
163
- expect(agent.send).toHaveBeenCalledWith({
164
- message: "hello",
165
+ expect(agent.send).toHaveBeenCalledWith("hello", {
165
166
  clientContext: { page: "/pricing" },
166
167
  });
167
168
  });
@@ -183,7 +184,7 @@ describe("useEveAgentRuntime status forwarding", () => {
183
184
  });
184
185
 
185
186
  await waitFor(() => {
186
- expect(agent.send).toHaveBeenCalledWith({ message: "hello" });
187
+ expect(agent.send).toHaveBeenCalledWith("hello", undefined);
187
188
  });
188
189
  });
189
190
 
@@ -204,7 +205,7 @@ describe("useEveAgentRuntime status forwarding", () => {
204
205
  });
205
206
 
206
207
  await waitFor(() => {
207
- expect(agent.send).toHaveBeenCalledWith({ message: "hello" });
208
+ expect(agent.send).toHaveBeenCalledWith("hello", undefined);
208
209
  });
209
210
  });
210
211
 
@@ -238,8 +239,7 @@ describe("useEveAgentRuntime status forwarding", () => {
238
239
  });
239
240
 
240
241
  await waitFor(() => {
241
- expect(agent.send).toHaveBeenCalledWith({
242
- message: "hello",
242
+ expect(agent.send).toHaveBeenCalledWith("hello", {
243
243
  clientContext: { page: "/reload" },
244
244
  });
245
245
  });
@@ -272,8 +272,7 @@ describe("useEveAgentRuntime status forwarding", () => {
272
272
  });
273
273
 
274
274
  await waitFor(() => {
275
- expect(agent.send).toHaveBeenCalledWith({
276
- message: "hello",
275
+ expect(agent.send).toHaveBeenCalledWith("hello", {
277
276
  clientContext: { page: "/staged" },
278
277
  });
279
278
  });
@@ -310,8 +309,7 @@ describe("useEveAgentRuntime status forwarding", () => {
310
309
  });
311
310
 
312
311
  await waitFor(() => {
313
- expect(agent.send).toHaveBeenCalledWith({
314
- message: "hello",
312
+ expect(agent.send).toHaveBeenCalledWith("hello", {
315
313
  clientContext: { page: "/staged" },
316
314
  });
317
315
  });
@@ -330,6 +328,163 @@ describe("useEveAgentRuntime status forwarding", () => {
330
328
  });
331
329
  });
332
330
 
331
+ describe("useEveAgentRuntime tool approval responses", () => {
332
+ const textRequestData: EveMessageData = {
333
+ messages: [
334
+ { id: "u1", role: "user", parts: [{ type: "text", text: "hi" }] },
335
+ {
336
+ id: "a1",
337
+ role: "assistant",
338
+ parts: [
339
+ {
340
+ type: "dynamic-tool",
341
+ state: "approval-requested",
342
+ toolCallId: "call_1",
343
+ toolName: "ask_question",
344
+ input: {},
345
+ approval: { id: "req_1" },
346
+ toolMetadata: {
347
+ eve: {
348
+ kind: "tool-call",
349
+ name: "ask_question",
350
+ inputRequest: {
351
+ requestId: "req_1",
352
+ prompt: "What should the subject line be?",
353
+ kind: "question",
354
+ display: "text",
355
+ },
356
+ },
357
+ },
358
+ },
359
+ ],
360
+ },
361
+ ],
362
+ };
363
+
364
+ const flushMicrotasks = () =>
365
+ new Promise((resolve) => setTimeout(resolve, 0));
366
+
367
+ const processEvents = process as unknown as {
368
+ on(event: "unhandledRejection", listener: (reason: unknown) => void): void;
369
+ off(event: "unhandledRejection", listener: (reason: unknown) => void): void;
370
+ };
371
+
372
+ const respondToTextRequest = (
373
+ result: { current: ReturnType<typeof useEveAgentRuntime> },
374
+ response: { approved: boolean; reason?: string; optionId?: string },
375
+ ) =>
376
+ result.current.thread
377
+ .getMessageById("a1")
378
+ .getMessagePartByToolCallId("call_1")
379
+ .respondToToolApproval(response);
380
+
381
+ it("keeps an unanswered free-form request pending through the default controls", async () => {
382
+ const rejections: unknown[] = [];
383
+ const onUnhandledRejection = (reason: unknown) => rejections.push(reason);
384
+ processEvents.on("unhandledRejection", onUnhandledRejection);
385
+ const agent = createAgent({ data: textRequestData });
386
+ mockUseEveAgent.mockReturnValue(agent as never);
387
+
388
+ try {
389
+ const { result } = renderHook(() => useEveAgentRuntime());
390
+
391
+ expect(() => respondToTextRequest(result, { approved: true })).toThrow(
392
+ /was not answered by this response/,
393
+ );
394
+
395
+ await flushMicrotasks();
396
+ await flushMicrotasks();
397
+
398
+ expect(agent.send).not.toHaveBeenCalled();
399
+ expect(agent.respond).not.toHaveBeenCalled();
400
+ expect(rejections).toEqual([]);
401
+ } finally {
402
+ processEvents.off("unhandledRejection", onUnhandledRejection);
403
+ }
404
+ });
405
+
406
+ const bareApprovalData = (toolMetadata?: {
407
+ eve: { kind: "tool-call"; name: string };
408
+ }): EveMessageData => ({
409
+ messages: [
410
+ { id: "u1", role: "user", parts: [{ type: "text", text: "hi" }] },
411
+ {
412
+ id: "a1",
413
+ role: "assistant",
414
+ parts: [
415
+ {
416
+ type: "dynamic-tool",
417
+ state: "approval-requested",
418
+ toolCallId: "call_1",
419
+ toolName: "delete_file",
420
+ input: {},
421
+ approval: { id: "req_1" },
422
+ ...(toolMetadata && { toolMetadata }),
423
+ },
424
+ ],
425
+ },
426
+ ],
427
+ });
428
+
429
+ it.each([
430
+ ["no eve tool metadata at all", undefined],
431
+ [
432
+ "eve metadata carrying no input request",
433
+ { eve: { kind: "tool-call", name: "delete_file" } },
434
+ ],
435
+ ])(
436
+ "still answers an ordinary approval when the part has %s",
437
+ async (_label, toolMetadata) => {
438
+ const agent = createAgent({
439
+ data: bareApprovalData(toolMetadata as never),
440
+ });
441
+ mockUseEveAgent.mockReturnValue(agent as never);
442
+
443
+ const { result } = renderHook(() => useEveAgentRuntime());
444
+ expect(() =>
445
+ respondToTextRequest(result, { approved: true }),
446
+ ).not.toThrow();
447
+
448
+ await flushMicrotasks();
449
+
450
+ expect(agent.respond).toHaveBeenCalledWith([
451
+ { requestId: "req_1", optionId: "approve" },
452
+ ]);
453
+ },
454
+ );
455
+
456
+ it("maps a refusal on a request the data does not carry", async () => {
457
+ const agent = createAgent({ data: bareApprovalData(undefined) });
458
+ mockUseEveAgent.mockReturnValue(agent as never);
459
+
460
+ const { result } = renderHook(() => useEveAgentRuntime());
461
+ respondToTextRequest(result, { approved: false, reason: "not safe" });
462
+
463
+ await flushMicrotasks();
464
+
465
+ expect(agent.respond).toHaveBeenCalledWith([
466
+ { requestId: "req_1", optionId: "cancel", text: "not safe" },
467
+ ]);
468
+ });
469
+
470
+ it("submits a free-form answer as text without an option id", async () => {
471
+ const agent = createAgent({ data: textRequestData });
472
+ mockUseEveAgent.mockReturnValue(agent as never);
473
+
474
+ const { result } = renderHook(() => useEveAgentRuntime());
475
+ respondToTextRequest(result, {
476
+ approved: true,
477
+ reason: "Quarterly results",
478
+ });
479
+
480
+ await flushMicrotasks();
481
+
482
+ expect(agent.respond).toHaveBeenCalledWith([
483
+ { requestId: "req_1", text: "Quarterly results" },
484
+ ]);
485
+ });
486
+ });
487
+
333
488
  const settledData: EveMessageData = {
334
489
  messages: [
335
490
  { id: "u1", role: "user", parts: [{ type: "text", text: "earlier" }] },
@@ -341,6 +496,51 @@ const settledData: EveMessageData = {
341
496
  ],
342
497
  };
343
498
 
499
+ const executingToolData: EveMessageData = {
500
+ messages: [
501
+ { id: "u1", role: "user", parts: [{ type: "text", text: "run it" }] },
502
+ {
503
+ id: "a1",
504
+ role: "assistant",
505
+ parts: [
506
+ {
507
+ type: "dynamic-tool",
508
+ state: "input-available",
509
+ toolCallId: "call_slow",
510
+ toolName: "slow_tool",
511
+ input: {},
512
+ },
513
+ ],
514
+ },
515
+ ],
516
+ };
517
+
518
+ const twoExecutingToolsData: EveMessageData = {
519
+ messages: [
520
+ { id: "u1", role: "user", parts: [{ type: "text", text: "run them" }] },
521
+ {
522
+ id: "a1",
523
+ role: "assistant",
524
+ parts: [
525
+ {
526
+ type: "dynamic-tool",
527
+ state: "input-available",
528
+ toolCallId: "call_slow_a",
529
+ toolName: "slow_tool",
530
+ input: {},
531
+ },
532
+ {
533
+ type: "dynamic-tool",
534
+ state: "input-available",
535
+ toolCallId: "call_slow_b",
536
+ toolName: "slow_tool",
537
+ input: {},
538
+ },
539
+ ],
540
+ },
541
+ ],
542
+ };
543
+
344
544
  const getText = (runtime: ReturnType<typeof useEveAgentRuntime>) =>
345
545
  runtime.thread
346
546
  .getState()
@@ -394,9 +594,7 @@ describe("useEveAgentRuntime staged messages", () => {
394
594
  content: [{ type: "text", text: "hello" }],
395
595
  });
396
596
  });
397
- expect(agent.send).toHaveBeenCalledWith({
398
- message: "hello",
399
- });
597
+ expect(agent.send).toHaveBeenCalledWith("hello", undefined);
400
598
 
401
599
  mockUseEveAgent.mockReturnValue(
402
600
  createAgent({
@@ -504,8 +702,8 @@ describe("useEveAgentRuntime staged messages", () => {
504
702
  });
505
703
 
506
704
  expect(send).toHaveBeenCalledTimes(3);
507
- expect(send).toHaveBeenNthCalledWith(2, { message: "first staged" });
508
- expect(send).toHaveBeenNthCalledWith(3, { message: "second staged" });
705
+ expect(send).toHaveBeenNthCalledWith(2, "first staged", undefined);
706
+ expect(send).toHaveBeenNthCalledWith(3, "second staged", undefined);
509
707
  await waitFor(() => {
510
708
  expect(getText(result.current)).toEqual(["earlier", "earlier answer"]);
511
709
  });
@@ -541,12 +739,10 @@ describe("useEveAgentRuntime staged messages", () => {
541
739
 
542
740
  // the earlier draft keeps the context it was staged with; only the
543
741
  // reloaded message takes the reload-time config
544
- expect(agent.send).toHaveBeenNthCalledWith(1, {
545
- message: "first staged",
742
+ expect(agent.send).toHaveBeenNthCalledWith(1, "first staged", {
546
743
  clientContext: { page: "/first" },
547
744
  });
548
- expect(agent.send).toHaveBeenNthCalledWith(2, {
549
- message: "second staged",
745
+ expect(agent.send).toHaveBeenNthCalledWith(2, "second staged", {
550
746
  clientContext: { page: "/reloaded" },
551
747
  });
552
748
  });
@@ -569,9 +765,7 @@ describe("useEveAgentRuntime staged messages", () => {
569
765
  });
570
766
 
571
767
  expect(agent.send).toHaveBeenCalledTimes(1);
572
- expect(agent.send).toHaveBeenCalledWith({
573
- message: "first staged",
574
- });
768
+ expect(agent.send).toHaveBeenCalledWith("first staged", undefined);
575
769
  await waitFor(() => {
576
770
  expect(getText(result.current)).toEqual([
577
771
  "earlier",
@@ -609,17 +803,13 @@ describe("useEveAgentRuntime staged messages", () => {
609
803
  });
610
804
 
611
805
  await waitFor(() => expect(send).toHaveBeenCalledTimes(1));
612
- expect(send).toHaveBeenNthCalledWith(1, {
613
- message: "first staged",
614
- });
806
+ expect(send).toHaveBeenNthCalledWith(1, "first staged", undefined);
615
807
 
616
808
  await act(async () => {
617
809
  resolveFirstSend();
618
810
  });
619
811
  await waitFor(() => expect(send).toHaveBeenCalledTimes(2));
620
- expect(send).toHaveBeenNthCalledWith(2, {
621
- message: "second staged",
622
- });
812
+ expect(send).toHaveBeenNthCalledWith(2, "second staged", undefined);
623
813
 
624
814
  await waitFor(() => {
625
815
  expect(getText(result.current)).toEqual(["earlier", "earlier answer"]);
@@ -681,7 +871,7 @@ describe("useEveAgentRuntime staged messages", () => {
681
871
  });
682
872
 
683
873
  expect(send).toHaveBeenCalledTimes(1);
684
- expect(send).toHaveBeenCalledWith({ message: "first staged" });
874
+ expect(send).toHaveBeenCalledWith("first staged", undefined);
685
875
  await waitFor(() => {
686
876
  expect(getText(result.current)).toEqual([
687
877
  "earlier",
@@ -756,129 +946,101 @@ describe("useEveAgentRuntime staged messages", () => {
756
946
  });
757
947
  });
758
948
 
759
- const approvalData: EveMessageData = {
760
- messages: [
761
- { id: "u1", role: "user", parts: [{ type: "text", text: "send it" }] },
762
- {
763
- id: "a1",
764
- role: "assistant",
765
- parts: [
766
- {
767
- type: "dynamic-tool",
768
- state: "approval-requested",
769
- toolCallId: "call_1",
770
- toolName: "send_email",
771
- input: { to: "dev@example.com" },
772
- approval: { id: "req_1" },
773
- toolMetadata: {
774
- eve: {
775
- kind: "tool-call",
776
- name: "send_email",
777
- inputRequest: {
778
- requestId: "req_1",
779
- prompt: "Send the email?",
780
- display: "confirmation",
781
- options: [
782
- { id: "approve", label: "Approve" },
783
- { id: "deny", label: "Deny" },
784
- ],
785
- },
786
- },
787
- },
788
- },
789
- ],
790
- },
791
- ],
792
- };
949
+ describe("useEveAgentRuntime extras wiring", () => {
950
+ it("provides error, events, and session through the runtime extras", () => {
951
+ const error = new Error("boom");
952
+ const events = [{ type: "session.started" }];
953
+ const session = { sessionId: "s1" };
954
+ mockUseEveAgent.mockReturnValue(
955
+ createAgent({ status: "error", error, events, session }) as never,
956
+ );
793
957
 
794
- describe("useEveAgentRuntime concurrent sends", () => {
795
- it("defers an approval clicked while a turn is in flight until the turn parks", async () => {
796
- let resolveFirstSend!: () => void;
797
- const send = vi
798
- .fn()
799
- .mockImplementationOnce(
800
- () =>
801
- new Promise<void>((resolve) => {
802
- resolveFirstSend = resolve;
803
- }),
804
- )
805
- .mockResolvedValue(undefined);
806
- const agent = createAgent({ data: approvalData, send });
958
+ const { result } = renderHook(() => useEveAgentRuntime());
959
+
960
+ expect(
961
+ eveExtras.tryGet(result.current.thread.getState().extras),
962
+ ).toMatchObject({ error, events, session });
963
+ });
964
+
965
+ it("discards staged inputs when reset is invoked", async () => {
966
+ const agent = createAgent({ data: settledData });
807
967
  mockUseEveAgent.mockReturnValue(agent as never);
808
968
  const { result } = renderHook(() => useEveAgentRuntime());
809
969
 
810
- await act(async () => {
811
- result.current.thread.append({
812
- role: "user",
813
- content: [{ type: "text", text: "go" }],
814
- });
815
- });
816
- await waitFor(() => expect(send).toHaveBeenCalledTimes(1));
970
+ await stageMessage(result.current, "discarded draft");
971
+ const discardedId = result.current.thread.getState().messages[2]!.id;
817
972
 
818
973
  act(() => {
819
- result.current.thread
820
- .getMessageByIndex(1)
821
- .getMessagePartByToolCallId("call_1")
822
- .respondToToolApproval({ optionId: "approve" });
823
- });
824
- expect(send).toHaveBeenCalledTimes(1);
825
-
826
- await act(async () => {
827
- resolveFirstSend();
974
+ eveExtras.tryGet(result.current.thread.getState().extras)!.reset();
828
975
  });
829
- await waitFor(() => expect(send).toHaveBeenCalledTimes(2));
830
- expect(send).toHaveBeenNthCalledWith(2, {
831
- inputResponses: [{ requestId: "req_1", optionId: "approve" }],
976
+ await waitFor(() => {
977
+ expect(getText(result.current)).toEqual(["earlier", "earlier answer"]);
832
978
  });
979
+
980
+ // `onReload` is only wired while staged messages exist, so a fresh draft
981
+ // is needed to reach the staged-run lookup that reads `stagedInputsRef`.
982
+ await stageMessage(result.current, "fresh draft");
983
+
984
+ await expect(
985
+ Promise.resolve(
986
+ result.current.thread.startRun({
987
+ parentId: discardedId,
988
+ sourceId: null,
989
+ runConfig: {},
990
+ }),
991
+ ),
992
+ ).rejects.toThrow("Runtime does not support reloading messages.");
993
+ expect(agent.send).not.toHaveBeenCalled();
994
+ expect(agent.reset).toHaveBeenCalledTimes(1);
833
995
  });
834
996
 
835
- it("queues a send issued while a turn is in flight and preserves order", async () => {
836
- let resolveFirstSend!: () => void;
837
- const send = vi
838
- .fn()
839
- .mockImplementationOnce(
840
- () =>
841
- new Promise<void>((resolve) => {
842
- resolveFirstSend = resolve;
843
- }),
844
- )
845
- .mockResolvedValue(undefined);
846
- const agent = createAgent({ data: settledData, send });
997
+ it("clears executing tool state when reset is invoked", async () => {
998
+ const agent = createAgent({ data: settledData });
847
999
  mockUseEveAgent.mockReturnValue(agent as never);
848
- const { result } = renderHook(() => useEveAgentRuntime());
1000
+ const { result, rerender } = renderHook(() => useEveAgentRuntime());
849
1001
 
850
- await act(async () => {
851
- result.current.thread.append({
852
- role: "user",
853
- content: [{ type: "text", text: "first" }],
1002
+ act(() => {
1003
+ result.current.registerModelContextProvider({
1004
+ getModelContext: () => ({
1005
+ tools: {
1006
+ slow_tool: {
1007
+ parameters: { type: "object", properties: {} },
1008
+ execute: () => new Promise<never>(() => {}),
1009
+ },
1010
+ },
1011
+ }),
854
1012
  });
855
1013
  });
856
- await waitFor(() => expect(send).toHaveBeenCalledTimes(1));
857
1014
 
858
- await act(async () => {
859
- result.current.thread.append({
860
- role: "user",
861
- content: [{ type: "text", text: "second" }],
862
- });
1015
+ // The tracker treats its first snapshot as historical, so the tool call
1016
+ // has to arrive on a later one to actually execute.
1017
+ mockUseEveAgent.mockReturnValue(
1018
+ createAgent({ data: executingToolData, reset: agent.reset }) as never,
1019
+ );
1020
+ rerender();
1021
+
1022
+ await waitFor(() => {
1023
+ expect(result.current.thread.getState().isRunning).toBe(true);
863
1024
  });
864
- expect(send).toHaveBeenCalledTimes(1);
865
1025
 
866
- await act(async () => {
867
- resolveFirstSend();
1026
+ act(() => {
1027
+ eveExtras.tryGet(result.current.thread.getState().extras)!.reset();
868
1028
  });
869
- await waitFor(() => expect(send).toHaveBeenCalledTimes(2));
870
- expect(send).toHaveBeenNthCalledWith(1, { message: "first" });
871
- expect(send).toHaveBeenNthCalledWith(2, { message: "second" });
1029
+
1030
+ await waitFor(() => {
1031
+ expect(result.current.thread.getState().isRunning).toBe(false);
1032
+ });
1033
+ expect(agent.reset).toHaveBeenCalledTimes(1);
872
1034
  });
873
1035
 
874
- it("drops a queued send when the run is cancelled before it dispatches", async () => {
875
- let resolveFirstSend!: () => void;
1036
+ it("drops a send queued behind an active turn when reset is invoked", async () => {
1037
+ let releaseFirstSend: (() => void) | undefined;
876
1038
  const send = vi
877
1039
  .fn()
878
1040
  .mockImplementationOnce(
879
1041
  () =>
880
1042
  new Promise<void>((resolve) => {
881
- resolveFirstSend = resolve;
1043
+ releaseFirstSend = resolve;
882
1044
  }),
883
1045
  )
884
1046
  .mockResolvedValue(undefined);
@@ -886,39 +1048,52 @@ describe("useEveAgentRuntime concurrent sends", () => {
886
1048
  mockUseEveAgent.mockReturnValue(agent as never);
887
1049
  const { result } = renderHook(() => useEveAgentRuntime());
888
1050
 
889
- await act(async () => {
1051
+ await stageMessage(result.current, "queued draft");
1052
+ const queuedDraftId = result.current.thread.getState().messages[2]!.id;
1053
+
1054
+ act(() => {
890
1055
  result.current.thread.append({
891
1056
  role: "user",
892
1057
  content: [{ type: "text", text: "first" }],
893
1058
  });
894
1059
  });
895
- await waitFor(() => expect(send).toHaveBeenCalledTimes(1));
896
-
897
- await act(async () => {
898
- result.current.thread.append({
899
- role: "user",
900
- content: [{ type: "text", text: "queued" }],
901
- });
1060
+ await waitFor(() => {
1061
+ expect(send).toHaveBeenCalledTimes(1);
902
1062
  });
1063
+
1064
+ const queuedReload = Promise.resolve(
1065
+ result.current.thread.startRun({
1066
+ parentId: queuedDraftId,
1067
+ sourceId: null,
1068
+ runConfig: {},
1069
+ }),
1070
+ );
1071
+ // Let the reload reach `enqueueSend` and park behind the active turn.
1072
+ await act(async () => {});
1073
+
903
1074
  act(() => {
904
- result.current.thread.cancelRun();
1075
+ eveExtras.tryGet(result.current.thread.getState().extras)!.reset();
905
1076
  });
906
- expect(agent.stop).toHaveBeenCalledTimes(1);
907
1077
 
908
1078
  await act(async () => {
909
- resolveFirstSend();
1079
+ releaseFirstSend?.();
1080
+ await queuedReload;
910
1081
  });
1082
+
911
1083
  expect(send).toHaveBeenCalledTimes(1);
1084
+ await waitFor(() => {
1085
+ expect(getText(result.current)).toEqual(["earlier", "earlier answer"]);
1086
+ });
912
1087
  });
913
1088
 
914
- it("returns a cancelled queued send to the composer", async () => {
915
- let resolveFirstSend!: () => void;
1089
+ it("keeps a staged draft when a queued send is cancelled instead of reset", async () => {
1090
+ let releaseFirstSend: (() => void) | undefined;
916
1091
  const send = vi
917
1092
  .fn()
918
1093
  .mockImplementationOnce(
919
1094
  () =>
920
1095
  new Promise<void>((resolve) => {
921
- resolveFirstSend = resolve;
1096
+ releaseFirstSend = resolve;
922
1097
  }),
923
1098
  )
924
1099
  .mockResolvedValue(undefined);
@@ -926,35 +1101,449 @@ describe("useEveAgentRuntime concurrent sends", () => {
926
1101
  mockUseEveAgent.mockReturnValue(agent as never);
927
1102
  const { result } = renderHook(() => useEveAgentRuntime());
928
1103
 
929
- await act(async () => {
1104
+ await stageMessage(result.current, "queued draft");
1105
+ const queuedDraftId = result.current.thread.getState().messages[2]!.id;
1106
+
1107
+ act(() => {
930
1108
  result.current.thread.append({
931
1109
  role: "user",
932
1110
  content: [{ type: "text", text: "first" }],
933
1111
  });
934
1112
  });
935
- await waitFor(() => expect(send).toHaveBeenCalledTimes(1));
936
-
937
- await act(async () => {
938
- result.current.thread.composer.setText("queued");
939
- result.current.thread.composer.send();
1113
+ await waitFor(() => {
1114
+ expect(send).toHaveBeenCalledTimes(1);
940
1115
  });
941
- expect(result.current.thread.composer.getState().text).toBe("");
1116
+
1117
+ const queuedReload = Promise.resolve(
1118
+ result.current.thread.startRun({
1119
+ parentId: queuedDraftId,
1120
+ sourceId: null,
1121
+ runConfig: {},
1122
+ }),
1123
+ );
1124
+ await act(async () => {});
942
1125
 
943
1126
  act(() => {
944
1127
  result.current.thread.cancelRun();
945
1128
  });
1129
+
946
1130
  await act(async () => {
947
- resolveFirstSend();
1131
+ releaseFirstSend?.();
1132
+ await queuedReload;
948
1133
  });
949
1134
 
1135
+ expect(send).toHaveBeenCalledTimes(1);
1136
+ expect(agent.stop).toHaveBeenCalledTimes(1);
950
1137
  await waitFor(() => {
951
- expect(result.current.thread.composer.getState().text).toBe("queued");
1138
+ expect(getText(result.current)).toEqual([
1139
+ "earlier",
1140
+ "earlier answer",
1141
+ "queued draft",
1142
+ ]);
952
1143
  });
953
- expect(send).toHaveBeenCalledTimes(1);
954
- expect(getText(result.current)).toEqual(["earlier", "earlier answer"]);
955
1144
  });
956
1145
 
957
- it("returns the queued send when cancel keeps the trailing message", async () => {
1146
+ it("aborts client tool executions when reset is invoked", async () => {
1147
+ const agentReset = vi.fn(() => {
1148
+ mockUseEveAgent.mockReturnValue(
1149
+ createAgent({ data: { messages: [] }, reset: agentReset }) as never,
1150
+ );
1151
+ });
1152
+ const agent = createAgent({ data: settledData, reset: agentReset });
1153
+ mockUseEveAgent.mockReturnValue(agent as never);
1154
+ const { result, rerender } = renderHook(() => useEveAgentRuntime());
1155
+
1156
+ const abortReasons: string[] = [];
1157
+ act(() => {
1158
+ result.current.registerModelContextProvider({
1159
+ getModelContext: () => ({
1160
+ tools: {
1161
+ slow_tool: {
1162
+ parameters: { type: "object", properties: {} },
1163
+ execute: (
1164
+ _args: unknown,
1165
+ context: { toolCallId: string; abortSignal: AbortSignal },
1166
+ ) =>
1167
+ new Promise<never>(() => {
1168
+ context.abortSignal.addEventListener("abort", () => {
1169
+ abortReasons.push(context.toolCallId);
1170
+ });
1171
+ }),
1172
+ },
1173
+ },
1174
+ }),
1175
+ });
1176
+ });
1177
+
1178
+ mockUseEveAgent.mockReturnValue(
1179
+ createAgent({ data: twoExecutingToolsData, reset: agentReset }) as never,
1180
+ );
1181
+ rerender();
1182
+
1183
+ await waitFor(() => {
1184
+ expect(result.current.thread.getState().isRunning).toBe(true);
1185
+ });
1186
+
1187
+ act(() => {
1188
+ eveExtras.tryGet(result.current.thread.getState().extras)!.reset();
1189
+ });
1190
+ rerender();
1191
+
1192
+ await waitFor(() => {
1193
+ expect(abortReasons.sort()).toEqual(["call_slow_a", "call_slow_b"]);
1194
+ });
1195
+ expect(result.current.thread.getState().isRunning).toBe(false);
1196
+ });
1197
+
1198
+ it("rejects a pending human-input request when reset is invoked", async () => {
1199
+ const agentReset = vi.fn(() => {
1200
+ mockUseEveAgent.mockReturnValue(
1201
+ createAgent({ data: { messages: [] }, reset: agentReset }) as never,
1202
+ );
1203
+ });
1204
+ const agent = createAgent({ data: settledData, reset: agentReset });
1205
+ mockUseEveAgent.mockReturnValue(agent as never);
1206
+ const { result, rerender } = renderHook(() => useEveAgentRuntime());
1207
+
1208
+ const humanRejections: unknown[] = [];
1209
+ act(() => {
1210
+ result.current.registerModelContextProvider({
1211
+ getModelContext: () => ({
1212
+ tools: {
1213
+ slow_tool: {
1214
+ parameters: { type: "object", properties: {} },
1215
+ execute: async (
1216
+ _args: unknown,
1217
+ context: { human: (payload: unknown) => Promise<unknown> },
1218
+ ) => {
1219
+ try {
1220
+ return await context.human({ request: "approve" });
1221
+ } catch (error) {
1222
+ humanRejections.push(error);
1223
+ throw error;
1224
+ }
1225
+ },
1226
+ },
1227
+ },
1228
+ }),
1229
+ });
1230
+ });
1231
+
1232
+ mockUseEveAgent.mockReturnValue(
1233
+ createAgent({ data: twoExecutingToolsData, reset: agentReset }) as never,
1234
+ );
1235
+ rerender();
1236
+
1237
+ await waitFor(() => {
1238
+ expect(result.current.thread.getState().isRunning).toBe(true);
1239
+ });
1240
+
1241
+ act(() => {
1242
+ eveExtras.tryGet(result.current.thread.getState().extras)!.reset();
1243
+ });
1244
+ rerender();
1245
+
1246
+ await waitFor(() => {
1247
+ expect(humanRejections).toHaveLength(2);
1248
+ });
1249
+ expect((humanRejections[0] as Error).message).toBe(
1250
+ "Tool execution aborted",
1251
+ );
1252
+ });
1253
+
1254
+ it("ignores tool statuses left over from a discarded session", async () => {
1255
+ const releases: Record<string, () => void> = {};
1256
+ const agentReset = vi.fn(() => {
1257
+ mockUseEveAgent.mockReturnValue(
1258
+ createAgent({ data: { messages: [] }, reset: agentReset }) as never,
1259
+ );
1260
+ });
1261
+ const agent = createAgent({ data: settledData, reset: agentReset });
1262
+ mockUseEveAgent.mockReturnValue(agent as never);
1263
+ const { result, rerender } = renderHook(() => useEveAgentRuntime());
1264
+
1265
+ act(() => {
1266
+ result.current.registerModelContextProvider({
1267
+ getModelContext: () => ({
1268
+ tools: {
1269
+ slow_tool: {
1270
+ parameters: { type: "object", properties: {} },
1271
+ execute: (_args: unknown, context: { toolCallId: string }) =>
1272
+ new Promise<never>((_resolve, reject) => {
1273
+ releases[context.toolCallId] = () =>
1274
+ reject(new Error("aborted"));
1275
+ }),
1276
+ },
1277
+ },
1278
+ }),
1279
+ });
1280
+ });
1281
+
1282
+ mockUseEveAgent.mockReturnValue(
1283
+ createAgent({
1284
+ data: twoExecutingToolsData,
1285
+ reset: agentReset,
1286
+ }) as never,
1287
+ );
1288
+ rerender();
1289
+
1290
+ await waitFor(() => {
1291
+ expect(result.current.thread.getState().isRunning).toBe(true);
1292
+ });
1293
+
1294
+ act(() => {
1295
+ eveExtras.tryGet(result.current.thread.getState().extras)!.reset();
1296
+ });
1297
+ rerender();
1298
+
1299
+ await waitFor(() => {
1300
+ expect(result.current.thread.getState().isRunning).toBe(false);
1301
+ });
1302
+
1303
+ await act(async () => {
1304
+ releases["call_slow_a"]?.();
1305
+ await Promise.resolve();
1306
+ });
1307
+
1308
+ await waitFor(() => {
1309
+ expect(result.current.thread.getState().isRunning).toBe(false);
1310
+ });
1311
+ expect(agentReset).toHaveBeenCalledTimes(1);
1312
+ });
1313
+
1314
+ it("promotes every staged draft after reset when the discarded run failed", async () => {
1315
+ const send = vi.fn().mockResolvedValue(undefined);
1316
+ const agent = createAgent({ data: settledData, send });
1317
+ let capturedOptions: { onFinish?: (snapshot: unknown) => void } = {};
1318
+ mockUseEveAgent.mockImplementation((options) => {
1319
+ capturedOptions = options as typeof capturedOptions;
1320
+ return agent as never;
1321
+ });
1322
+ const { result } = renderHook(() => useEveAgentRuntime());
1323
+
1324
+ act(() => {
1325
+ capturedOptions.onFinish?.({ status: "error" });
1326
+ });
1327
+ act(() => {
1328
+ eveExtras.tryGet(result.current.thread.getState().extras)!.reset();
1329
+ });
1330
+
1331
+ await stageMessage(result.current, "first staged");
1332
+ await stageMessage(result.current, "second staged");
1333
+
1334
+ const secondStagedId = result.current.thread.getState().messages[3]!.id;
1335
+ await act(async () => {
1336
+ await result.current.thread.startRun({
1337
+ parentId: secondStagedId,
1338
+ sourceId: null,
1339
+ runConfig: {},
1340
+ });
1341
+ });
1342
+
1343
+ expect(send).toHaveBeenCalledTimes(2);
1344
+ expect(send).toHaveBeenNthCalledWith(1, "first staged", undefined);
1345
+ expect(send).toHaveBeenNthCalledWith(2, "second staged", undefined);
1346
+ });
1347
+ });
1348
+
1349
+ const approvalData: EveMessageData = {
1350
+ messages: [
1351
+ { id: "u1", role: "user", parts: [{ type: "text", text: "send it" }] },
1352
+ {
1353
+ id: "a1",
1354
+ role: "assistant",
1355
+ parts: [
1356
+ {
1357
+ type: "dynamic-tool",
1358
+ state: "approval-requested",
1359
+ toolCallId: "call_1",
1360
+ toolName: "send_email",
1361
+ input: { to: "dev@example.com" },
1362
+ approval: { id: "req_1" },
1363
+ toolMetadata: {
1364
+ eve: {
1365
+ kind: "tool-call",
1366
+ name: "send_email",
1367
+ inputRequest: {
1368
+ requestId: "req_1",
1369
+ kind: "tool-approval",
1370
+ prompt: "Send the email?",
1371
+ display: "confirmation",
1372
+ options: [
1373
+ { id: "approve", label: "Approve" },
1374
+ { id: "cancel", label: "Cancel" },
1375
+ ],
1376
+ },
1377
+ },
1378
+ },
1379
+ },
1380
+ ],
1381
+ },
1382
+ ],
1383
+ };
1384
+
1385
+ describe("useEveAgentRuntime concurrent sends", () => {
1386
+ it("defers an approval clicked while a turn is in flight until the turn parks", async () => {
1387
+ let resolveFirstSend!: () => void;
1388
+ const send = vi.fn().mockImplementationOnce(
1389
+ () =>
1390
+ new Promise<void>((resolve) => {
1391
+ resolveFirstSend = resolve;
1392
+ }),
1393
+ );
1394
+ const respond = vi.fn().mockResolvedValue(undefined);
1395
+ const agent = createAgent({ data: approvalData, send, respond });
1396
+ mockUseEveAgent.mockReturnValue(agent as never);
1397
+ const { result } = renderHook(() => useEveAgentRuntime());
1398
+
1399
+ await act(async () => {
1400
+ result.current.thread.append({
1401
+ role: "user",
1402
+ content: [{ type: "text", text: "go" }],
1403
+ });
1404
+ });
1405
+ await waitFor(() => expect(send).toHaveBeenCalledTimes(1));
1406
+
1407
+ act(() => {
1408
+ result.current.thread
1409
+ .getMessageByIndex(1)
1410
+ .getMessagePartByToolCallId("call_1")
1411
+ .respondToToolApproval({ optionId: "approve" });
1412
+ });
1413
+ expect(respond).not.toHaveBeenCalled();
1414
+
1415
+ await act(async () => {
1416
+ resolveFirstSend();
1417
+ });
1418
+ await waitFor(() => expect(respond).toHaveBeenCalledTimes(1));
1419
+ expect(respond).toHaveBeenCalledWith([
1420
+ { requestId: "req_1", optionId: "approve" },
1421
+ ]);
1422
+ });
1423
+
1424
+ it("queues a send issued while a turn is in flight and preserves order", async () => {
1425
+ let resolveFirstSend!: () => void;
1426
+ const send = vi
1427
+ .fn()
1428
+ .mockImplementationOnce(
1429
+ () =>
1430
+ new Promise<void>((resolve) => {
1431
+ resolveFirstSend = resolve;
1432
+ }),
1433
+ )
1434
+ .mockResolvedValue(undefined);
1435
+ const agent = createAgent({ data: settledData, send });
1436
+ mockUseEveAgent.mockReturnValue(agent as never);
1437
+ const { result } = renderHook(() => useEveAgentRuntime());
1438
+
1439
+ await act(async () => {
1440
+ result.current.thread.append({
1441
+ role: "user",
1442
+ content: [{ type: "text", text: "first" }],
1443
+ });
1444
+ });
1445
+ await waitFor(() => expect(send).toHaveBeenCalledTimes(1));
1446
+
1447
+ await act(async () => {
1448
+ result.current.thread.append({
1449
+ role: "user",
1450
+ content: [{ type: "text", text: "second" }],
1451
+ });
1452
+ });
1453
+ expect(send).toHaveBeenCalledTimes(1);
1454
+
1455
+ await act(async () => {
1456
+ resolveFirstSend();
1457
+ });
1458
+ await waitFor(() => expect(send).toHaveBeenCalledTimes(2));
1459
+ expect(send).toHaveBeenNthCalledWith(1, "first", undefined);
1460
+ expect(send).toHaveBeenNthCalledWith(2, "second", undefined);
1461
+ });
1462
+
1463
+ it("drops a queued send when the run is cancelled before it dispatches", async () => {
1464
+ let resolveFirstSend!: () => void;
1465
+ const send = vi
1466
+ .fn()
1467
+ .mockImplementationOnce(
1468
+ () =>
1469
+ new Promise<void>((resolve) => {
1470
+ resolveFirstSend = resolve;
1471
+ }),
1472
+ )
1473
+ .mockResolvedValue(undefined);
1474
+ const agent = createAgent({ data: settledData, send });
1475
+ mockUseEveAgent.mockReturnValue(agent as never);
1476
+ const { result } = renderHook(() => useEveAgentRuntime());
1477
+
1478
+ await act(async () => {
1479
+ result.current.thread.append({
1480
+ role: "user",
1481
+ content: [{ type: "text", text: "first" }],
1482
+ });
1483
+ });
1484
+ await waitFor(() => expect(send).toHaveBeenCalledTimes(1));
1485
+
1486
+ await act(async () => {
1487
+ result.current.thread.append({
1488
+ role: "user",
1489
+ content: [{ type: "text", text: "queued" }],
1490
+ });
1491
+ });
1492
+ act(() => {
1493
+ result.current.thread.cancelRun();
1494
+ });
1495
+ expect(agent.stop).toHaveBeenCalledTimes(1);
1496
+
1497
+ await act(async () => {
1498
+ resolveFirstSend();
1499
+ });
1500
+ expect(send).toHaveBeenCalledTimes(1);
1501
+ });
1502
+
1503
+ it("returns a cancelled queued send to the composer", async () => {
1504
+ let resolveFirstSend!: () => void;
1505
+ const send = vi
1506
+ .fn()
1507
+ .mockImplementationOnce(
1508
+ () =>
1509
+ new Promise<void>((resolve) => {
1510
+ resolveFirstSend = resolve;
1511
+ }),
1512
+ )
1513
+ .mockResolvedValue(undefined);
1514
+ const agent = createAgent({ data: settledData, send });
1515
+ mockUseEveAgent.mockReturnValue(agent as never);
1516
+ const { result } = renderHook(() => useEveAgentRuntime());
1517
+
1518
+ await act(async () => {
1519
+ result.current.thread.append({
1520
+ role: "user",
1521
+ content: [{ type: "text", text: "first" }],
1522
+ });
1523
+ });
1524
+ await waitFor(() => expect(send).toHaveBeenCalledTimes(1));
1525
+
1526
+ await act(async () => {
1527
+ result.current.thread.composer.setText("queued");
1528
+ result.current.thread.composer.send();
1529
+ });
1530
+ expect(result.current.thread.composer.getState().text).toBe("");
1531
+
1532
+ act(() => {
1533
+ result.current.thread.cancelRun();
1534
+ });
1535
+ await act(async () => {
1536
+ resolveFirstSend();
1537
+ });
1538
+
1539
+ await waitFor(() => {
1540
+ expect(result.current.thread.composer.getState().text).toBe("queued");
1541
+ });
1542
+ expect(send).toHaveBeenCalledTimes(1);
1543
+ expect(getText(result.current)).toEqual(["earlier", "earlier answer"]);
1544
+ });
1545
+
1546
+ it("returns the queued send when cancel keeps the trailing message", async () => {
958
1547
  let resolveFirstSend!: () => void;
959
1548
  const send = vi
960
1549
  .fn()
@@ -1007,16 +1596,14 @@ describe("useEveAgentRuntime concurrent sends", () => {
1007
1596
 
1008
1597
  it("keeps a cancelled tool approval discarded", async () => {
1009
1598
  let resolveFirstSend!: () => void;
1010
- const send = vi
1011
- .fn()
1012
- .mockImplementationOnce(
1013
- () =>
1014
- new Promise<void>((resolve) => {
1015
- resolveFirstSend = resolve;
1016
- }),
1017
- )
1018
- .mockResolvedValue(undefined);
1019
- const agent = createAgent({ data: approvalData, send });
1599
+ const send = vi.fn().mockImplementationOnce(
1600
+ () =>
1601
+ new Promise<void>((resolve) => {
1602
+ resolveFirstSend = resolve;
1603
+ }),
1604
+ );
1605
+ const respond = vi.fn().mockResolvedValue(undefined);
1606
+ const agent = createAgent({ data: approvalData, send, respond });
1020
1607
  mockUseEveAgent.mockReturnValue(agent as never);
1021
1608
  const { result } = renderHook(() => useEveAgentRuntime());
1022
1609
  const before = getText(result.current);
@@ -1043,6 +1630,7 @@ describe("useEveAgentRuntime concurrent sends", () => {
1043
1630
  });
1044
1631
 
1045
1632
  expect(send).toHaveBeenCalledTimes(1);
1633
+ expect(respond).not.toHaveBeenCalled();
1046
1634
  expect(result.current.thread.composer.getState().text).toBe("");
1047
1635
  expect(getText(result.current)).toEqual(before);
1048
1636
  });
@@ -1142,10 +1730,292 @@ describe("useEveAgentRuntime concurrent sends", () => {
1142
1730
  });
1143
1731
  await waitFor(() => expect(send).toHaveBeenCalledTimes(3));
1144
1732
 
1145
- expect(send.mock.calls.map(([payload]) => payload)).toEqual([
1146
- { message: "first staged" },
1147
- { message: "interleaved" },
1148
- { message: "second staged" },
1733
+ expect(send.mock.calls.map(([message]) => message)).toEqual([
1734
+ "first staged",
1735
+ "interleaved",
1736
+ "second staged",
1149
1737
  ]);
1150
1738
  });
1151
1739
  });
1740
+
1741
+ describe("useEveAgentRuntime cancel binding", () => {
1742
+ it("cancels through the eve 0.38+ cancel binding when the agent provides it", async () => {
1743
+ const cancel = vi.fn().mockResolvedValue({ status: "cancelled" });
1744
+ const agent = createAgent({ status: "streaming", cancel });
1745
+ mockUseEveAgent.mockReturnValue(agent as never);
1746
+ const { result } = renderHook(() => useEveAgentRuntime());
1747
+
1748
+ await act(async () => {
1749
+ result.current.thread.cancelRun();
1750
+ });
1751
+
1752
+ expect(cancel).toHaveBeenCalledTimes(1);
1753
+ expect(agent.stop).not.toHaveBeenCalled();
1754
+ });
1755
+
1756
+ it("falls back to the pre-0.38 stop binding when cancel is absent", async () => {
1757
+ const agent = createAgent({ status: "streaming" });
1758
+ mockUseEveAgent.mockReturnValue(agent as never);
1759
+ const { result } = renderHook(() => useEveAgentRuntime());
1760
+
1761
+ await act(async () => {
1762
+ result.current.thread.cancelRun();
1763
+ });
1764
+
1765
+ expect(agent.stop).toHaveBeenCalledTimes(1);
1766
+ });
1767
+
1768
+ it("drops a queued send when the durable cancel settles late", async () => {
1769
+ let resolveFirstSend!: () => void;
1770
+ const send = vi
1771
+ .fn()
1772
+ .mockImplementationOnce(
1773
+ () =>
1774
+ new Promise<void>((resolve) => {
1775
+ resolveFirstSend = resolve;
1776
+ }),
1777
+ )
1778
+ .mockResolvedValue(undefined);
1779
+ let resolveCancel!: () => void;
1780
+ const cancel = vi.fn(
1781
+ () =>
1782
+ new Promise<{ status: string }>((resolve) => {
1783
+ resolveCancel = () => resolve({ status: "cancelled" });
1784
+ }),
1785
+ );
1786
+ const agent = createAgent({ data: settledData, send, cancel });
1787
+ mockUseEveAgent.mockReturnValue(agent as never);
1788
+ const { result } = renderHook(() => useEveAgentRuntime());
1789
+
1790
+ await act(async () => {
1791
+ result.current.thread.append({
1792
+ role: "user",
1793
+ content: [{ type: "text", text: "first" }],
1794
+ });
1795
+ });
1796
+ await waitFor(() => expect(send).toHaveBeenCalledTimes(1));
1797
+
1798
+ await act(async () => {
1799
+ result.current.thread.append({
1800
+ role: "user",
1801
+ content: [{ type: "text", text: "queued" }],
1802
+ });
1803
+ });
1804
+ act(() => {
1805
+ result.current.thread.cancelRun();
1806
+ });
1807
+ expect(cancel).toHaveBeenCalledTimes(1);
1808
+
1809
+ await act(async () => {
1810
+ resolveFirstSend();
1811
+ resolveCancel();
1812
+ });
1813
+ expect(send).toHaveBeenCalledTimes(1);
1814
+ });
1815
+ });
1816
+
1817
+ describe("useEveAgentRuntime thread refetch", () => {
1818
+ const session = { sessionId: "s1" };
1819
+
1820
+ it("replays the session through eve resume when the thread is refetched", async () => {
1821
+ const resume = vi.fn().mockResolvedValue(undefined);
1822
+ const agent = createAgent({ session, resume });
1823
+ mockUseEveAgent.mockReturnValue(agent as never);
1824
+ const { result } = renderHook(() => useEveAgentRuntime());
1825
+ expect(result.current.thread.getState().capabilities.refetchThread).toBe(
1826
+ true,
1827
+ );
1828
+
1829
+ await act(async () => {
1830
+ await result.current.threads.reloadMainThread();
1831
+ });
1832
+
1833
+ expect(resume).toHaveBeenCalledTimes(1);
1834
+ });
1835
+
1836
+ it("propagates a failed replay to the refetch caller", async () => {
1837
+ const resume = vi.fn().mockRejectedValue(new Error("replay failed"));
1838
+ const agent = createAgent({ session, resume });
1839
+ mockUseEveAgent.mockReturnValue(agent as never);
1840
+ const { result } = renderHook(() => useEveAgentRuntime());
1841
+
1842
+ await act(async () => {
1843
+ await expect(result.current.threads.reloadMainThread()).rejects.toThrow(
1844
+ "replay failed",
1845
+ );
1846
+ });
1847
+ });
1848
+
1849
+ it("resolves without replaying when no session exists yet", async () => {
1850
+ const resume = vi.fn();
1851
+ const agent = createAgent({ session: undefined, resume });
1852
+ mockUseEveAgent.mockReturnValue(agent as never);
1853
+ const { result } = renderHook(() => useEveAgentRuntime());
1854
+
1855
+ await act(async () => {
1856
+ await result.current.threads.reloadMainThread();
1857
+ });
1858
+
1859
+ expect(resume).not.toHaveBeenCalled();
1860
+ });
1861
+
1862
+ it("waits for a turn dispatched before it to park, then replays", async () => {
1863
+ let resolveSend!: () => void;
1864
+ const send = vi.fn(
1865
+ () =>
1866
+ new Promise<void>((resolve) => {
1867
+ resolveSend = resolve;
1868
+ }),
1869
+ );
1870
+ const resume = vi.fn().mockResolvedValue(undefined);
1871
+ const agent = createAgent({ session, resume, send });
1872
+ mockUseEveAgent.mockReturnValue(agent as never);
1873
+ const { result } = renderHook(() => useEveAgentRuntime());
1874
+
1875
+ await act(async () => {
1876
+ result.current.thread.append({
1877
+ role: "user",
1878
+ content: [{ type: "text", text: "hi" }],
1879
+ });
1880
+ });
1881
+ await waitFor(() => expect(send).toHaveBeenCalledTimes(1));
1882
+
1883
+ let settled = false;
1884
+ await act(async () => {
1885
+ void result.current.threads.reloadMainThread().then(() => {
1886
+ settled = true;
1887
+ });
1888
+ });
1889
+ expect(resume).not.toHaveBeenCalled();
1890
+ expect(settled).toBe(false);
1891
+
1892
+ await act(async () => {
1893
+ resolveSend();
1894
+ });
1895
+ await waitFor(() => expect(settled).toBe(true));
1896
+ expect(resume).toHaveBeenCalledTimes(1);
1897
+ });
1898
+
1899
+ it("reads the session at dispatch time, after the first send created it", async () => {
1900
+ let resolveSend!: () => void;
1901
+ const send = vi.fn(
1902
+ () =>
1903
+ new Promise<void>((resolve) => {
1904
+ resolveSend = resolve;
1905
+ }),
1906
+ );
1907
+ const resume = vi.fn().mockResolvedValue(undefined);
1908
+ const agent = createAgent({ session: undefined, resume, send });
1909
+ mockUseEveAgent.mockReturnValue(agent as never);
1910
+ const { result, rerender } = renderHook(() => useEveAgentRuntime());
1911
+
1912
+ await act(async () => {
1913
+ result.current.thread.append({
1914
+ role: "user",
1915
+ content: [{ type: "text", text: "hi" }],
1916
+ });
1917
+ });
1918
+ await waitFor(() => expect(send).toHaveBeenCalledTimes(1));
1919
+
1920
+ let settled = false;
1921
+ await act(async () => {
1922
+ void result.current.threads.reloadMainThread().then(() => {
1923
+ settled = true;
1924
+ });
1925
+ });
1926
+
1927
+ mockUseEveAgent.mockReturnValue({ ...agent, session } as never);
1928
+ rerender();
1929
+ await act(async () => {
1930
+ resolveSend();
1931
+ });
1932
+ await waitFor(() => expect(settled).toBe(true));
1933
+ expect(resume).toHaveBeenCalledTimes(1);
1934
+ });
1935
+
1936
+ it("still replays a queued refetch when the run it waited on is cancelled", async () => {
1937
+ let resolveSend!: () => void;
1938
+ const send = vi.fn(
1939
+ () =>
1940
+ new Promise<void>((resolve) => {
1941
+ resolveSend = resolve;
1942
+ }),
1943
+ );
1944
+ const resume = vi.fn().mockResolvedValue(undefined);
1945
+ const cancel = vi.fn().mockResolvedValue({ status: "cancelled" });
1946
+ const agent = createAgent({ session, resume, send, cancel });
1947
+ mockUseEveAgent.mockReturnValue(agent as never);
1948
+ const { result } = renderHook(() => useEveAgentRuntime());
1949
+
1950
+ await act(async () => {
1951
+ result.current.thread.append({
1952
+ role: "user",
1953
+ content: [{ type: "text", text: "hi" }],
1954
+ });
1955
+ });
1956
+ await waitFor(() => expect(send).toHaveBeenCalledTimes(1));
1957
+
1958
+ let settled = false;
1959
+ await act(async () => {
1960
+ void result.current.threads.reloadMainThread().then(() => {
1961
+ settled = true;
1962
+ });
1963
+ result.current.thread.cancelRun();
1964
+ });
1965
+ await act(async () => {
1966
+ resolveSend();
1967
+ });
1968
+
1969
+ await waitFor(() => expect(settled).toBe(true));
1970
+ expect(resume).toHaveBeenCalledTimes(1);
1971
+ });
1972
+
1973
+ it("drops a queued refetch when the session is reset before it dispatches", async () => {
1974
+ let resolveSend!: () => void;
1975
+ const send = vi.fn(
1976
+ () =>
1977
+ new Promise<void>((resolve) => {
1978
+ resolveSend = resolve;
1979
+ }),
1980
+ );
1981
+ const resume = vi.fn();
1982
+ const agent = createAgent({ session, resume, send });
1983
+ mockUseEveAgent.mockReturnValue(agent as never);
1984
+ const { result } = renderHook(() => useEveAgentRuntime());
1985
+
1986
+ await act(async () => {
1987
+ result.current.thread.append({
1988
+ role: "user",
1989
+ content: [{ type: "text", text: "hi" }],
1990
+ });
1991
+ });
1992
+ await waitFor(() => expect(send).toHaveBeenCalledTimes(1));
1993
+
1994
+ let settled = false;
1995
+ await act(async () => {
1996
+ void result.current.threads.reloadMainThread().then(() => {
1997
+ settled = true;
1998
+ });
1999
+ eveExtras.tryGet(result.current.thread.getState().extras)!.reset();
2000
+ });
2001
+ await act(async () => {
2002
+ resolveSend();
2003
+ });
2004
+
2005
+ await waitFor(() => expect(settled).toBe(true));
2006
+ expect(resume).not.toHaveBeenCalled();
2007
+ });
2008
+
2009
+ it("leaves the capability absent when the installed eve has no resume", async () => {
2010
+ const agent = createAgent({ session });
2011
+ mockUseEveAgent.mockReturnValue(agent as never);
2012
+ const { result } = renderHook(() => useEveAgentRuntime());
2013
+
2014
+ expect(result.current.thread.getState().capabilities.refetchThread).toBe(
2015
+ false,
2016
+ );
2017
+ await act(async () => {
2018
+ await result.current.threads.reloadMainThread();
2019
+ });
2020
+ });
2021
+ });