@assistant-ui/eve 0.0.12 → 0.0.14
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 +17 -3
- package/dist/convertEveMessages.d.ts.map +1 -1
- package/dist/convertEveMessages.js +11 -6
- package/dist/convertEveMessages.js.map +1 -1
- package/dist/eveExtras.d.ts +7 -0
- package/dist/eveExtras.d.ts.map +1 -0
- package/dist/eveExtras.js +7 -0
- package/dist/eveExtras.js.map +1 -0
- package/dist/hooks.d.ts +25 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +32 -0
- package/dist/hooks.js.map +1 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +2 -1
- package/dist/useEveAgentRuntime.d.ts.map +1 -1
- package/dist/useEveAgentRuntime.js +49 -20
- package/dist/useEveAgentRuntime.js.map +1 -1
- package/package.json +6 -5
- package/src/convertEveMessages.test.ts +299 -4
- package/src/convertEveMessages.ts +42 -6
- package/src/eveExtras.ts +10 -0
- package/src/hooks.test.tsx +84 -0
- package/src/hooks.ts +36 -0
- package/src/index.ts +3 -0
- package/src/useEveAgentRuntime.test.tsx +618 -51
- package/src/useEveAgentRuntime.ts +109 -32
|
@@ -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(
|
|
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(
|
|
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
|
});
|
|
@@ -341,6 +339,51 @@ const settledData: EveMessageData = {
|
|
|
341
339
|
],
|
|
342
340
|
};
|
|
343
341
|
|
|
342
|
+
const executingToolData: EveMessageData = {
|
|
343
|
+
messages: [
|
|
344
|
+
{ id: "u1", role: "user", parts: [{ type: "text", text: "run it" }] },
|
|
345
|
+
{
|
|
346
|
+
id: "a1",
|
|
347
|
+
role: "assistant",
|
|
348
|
+
parts: [
|
|
349
|
+
{
|
|
350
|
+
type: "dynamic-tool",
|
|
351
|
+
state: "input-available",
|
|
352
|
+
toolCallId: "call_slow",
|
|
353
|
+
toolName: "slow_tool",
|
|
354
|
+
input: {},
|
|
355
|
+
},
|
|
356
|
+
],
|
|
357
|
+
},
|
|
358
|
+
],
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
const twoExecutingToolsData: EveMessageData = {
|
|
362
|
+
messages: [
|
|
363
|
+
{ id: "u1", role: "user", parts: [{ type: "text", text: "run them" }] },
|
|
364
|
+
{
|
|
365
|
+
id: "a1",
|
|
366
|
+
role: "assistant",
|
|
367
|
+
parts: [
|
|
368
|
+
{
|
|
369
|
+
type: "dynamic-tool",
|
|
370
|
+
state: "input-available",
|
|
371
|
+
toolCallId: "call_slow_a",
|
|
372
|
+
toolName: "slow_tool",
|
|
373
|
+
input: {},
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
type: "dynamic-tool",
|
|
377
|
+
state: "input-available",
|
|
378
|
+
toolCallId: "call_slow_b",
|
|
379
|
+
toolName: "slow_tool",
|
|
380
|
+
input: {},
|
|
381
|
+
},
|
|
382
|
+
],
|
|
383
|
+
},
|
|
384
|
+
],
|
|
385
|
+
};
|
|
386
|
+
|
|
344
387
|
const getText = (runtime: ReturnType<typeof useEveAgentRuntime>) =>
|
|
345
388
|
runtime.thread
|
|
346
389
|
.getState()
|
|
@@ -394,9 +437,7 @@ describe("useEveAgentRuntime staged messages", () => {
|
|
|
394
437
|
content: [{ type: "text", text: "hello" }],
|
|
395
438
|
});
|
|
396
439
|
});
|
|
397
|
-
expect(agent.send).toHaveBeenCalledWith(
|
|
398
|
-
message: "hello",
|
|
399
|
-
});
|
|
440
|
+
expect(agent.send).toHaveBeenCalledWith("hello", undefined);
|
|
400
441
|
|
|
401
442
|
mockUseEveAgent.mockReturnValue(
|
|
402
443
|
createAgent({
|
|
@@ -504,8 +545,8 @@ describe("useEveAgentRuntime staged messages", () => {
|
|
|
504
545
|
});
|
|
505
546
|
|
|
506
547
|
expect(send).toHaveBeenCalledTimes(3);
|
|
507
|
-
expect(send).toHaveBeenNthCalledWith(2,
|
|
508
|
-
expect(send).toHaveBeenNthCalledWith(3,
|
|
548
|
+
expect(send).toHaveBeenNthCalledWith(2, "first staged", undefined);
|
|
549
|
+
expect(send).toHaveBeenNthCalledWith(3, "second staged", undefined);
|
|
509
550
|
await waitFor(() => {
|
|
510
551
|
expect(getText(result.current)).toEqual(["earlier", "earlier answer"]);
|
|
511
552
|
});
|
|
@@ -541,12 +582,10 @@ describe("useEveAgentRuntime staged messages", () => {
|
|
|
541
582
|
|
|
542
583
|
// the earlier draft keeps the context it was staged with; only the
|
|
543
584
|
// reloaded message takes the reload-time config
|
|
544
|
-
expect(agent.send).toHaveBeenNthCalledWith(1, {
|
|
545
|
-
message: "first staged",
|
|
585
|
+
expect(agent.send).toHaveBeenNthCalledWith(1, "first staged", {
|
|
546
586
|
clientContext: { page: "/first" },
|
|
547
587
|
});
|
|
548
|
-
expect(agent.send).toHaveBeenNthCalledWith(2, {
|
|
549
|
-
message: "second staged",
|
|
588
|
+
expect(agent.send).toHaveBeenNthCalledWith(2, "second staged", {
|
|
550
589
|
clientContext: { page: "/reloaded" },
|
|
551
590
|
});
|
|
552
591
|
});
|
|
@@ -569,9 +608,7 @@ describe("useEveAgentRuntime staged messages", () => {
|
|
|
569
608
|
});
|
|
570
609
|
|
|
571
610
|
expect(agent.send).toHaveBeenCalledTimes(1);
|
|
572
|
-
expect(agent.send).toHaveBeenCalledWith(
|
|
573
|
-
message: "first staged",
|
|
574
|
-
});
|
|
611
|
+
expect(agent.send).toHaveBeenCalledWith("first staged", undefined);
|
|
575
612
|
await waitFor(() => {
|
|
576
613
|
expect(getText(result.current)).toEqual([
|
|
577
614
|
"earlier",
|
|
@@ -609,17 +646,13 @@ describe("useEveAgentRuntime staged messages", () => {
|
|
|
609
646
|
});
|
|
610
647
|
|
|
611
648
|
await waitFor(() => expect(send).toHaveBeenCalledTimes(1));
|
|
612
|
-
expect(send).toHaveBeenNthCalledWith(1,
|
|
613
|
-
message: "first staged",
|
|
614
|
-
});
|
|
649
|
+
expect(send).toHaveBeenNthCalledWith(1, "first staged", undefined);
|
|
615
650
|
|
|
616
651
|
await act(async () => {
|
|
617
652
|
resolveFirstSend();
|
|
618
653
|
});
|
|
619
654
|
await waitFor(() => expect(send).toHaveBeenCalledTimes(2));
|
|
620
|
-
expect(send).toHaveBeenNthCalledWith(2,
|
|
621
|
-
message: "second staged",
|
|
622
|
-
});
|
|
655
|
+
expect(send).toHaveBeenNthCalledWith(2, "second staged", undefined);
|
|
623
656
|
|
|
624
657
|
await waitFor(() => {
|
|
625
658
|
expect(getText(result.current)).toEqual(["earlier", "earlier answer"]);
|
|
@@ -681,7 +714,7 @@ describe("useEveAgentRuntime staged messages", () => {
|
|
|
681
714
|
});
|
|
682
715
|
|
|
683
716
|
expect(send).toHaveBeenCalledTimes(1);
|
|
684
|
-
expect(send).toHaveBeenCalledWith(
|
|
717
|
+
expect(send).toHaveBeenCalledWith("first staged", undefined);
|
|
685
718
|
await waitFor(() => {
|
|
686
719
|
expect(getText(result.current)).toEqual([
|
|
687
720
|
"earlier",
|
|
@@ -756,6 +789,406 @@ describe("useEveAgentRuntime staged messages", () => {
|
|
|
756
789
|
});
|
|
757
790
|
});
|
|
758
791
|
|
|
792
|
+
describe("useEveAgentRuntime extras wiring", () => {
|
|
793
|
+
it("provides error, events, and session through the runtime extras", () => {
|
|
794
|
+
const error = new Error("boom");
|
|
795
|
+
const events = [{ type: "session.started" }];
|
|
796
|
+
const session = { sessionId: "s1" };
|
|
797
|
+
mockUseEveAgent.mockReturnValue(
|
|
798
|
+
createAgent({ status: "error", error, events, session }) as never,
|
|
799
|
+
);
|
|
800
|
+
|
|
801
|
+
const { result } = renderHook(() => useEveAgentRuntime());
|
|
802
|
+
|
|
803
|
+
expect(
|
|
804
|
+
eveExtras.tryGet(result.current.thread.getState().extras),
|
|
805
|
+
).toMatchObject({ error, events, session });
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
it("discards staged inputs when reset is invoked", async () => {
|
|
809
|
+
const agent = createAgent({ data: settledData });
|
|
810
|
+
mockUseEveAgent.mockReturnValue(agent as never);
|
|
811
|
+
const { result } = renderHook(() => useEveAgentRuntime());
|
|
812
|
+
|
|
813
|
+
await stageMessage(result.current, "discarded draft");
|
|
814
|
+
const discardedId = result.current.thread.getState().messages[2]!.id;
|
|
815
|
+
|
|
816
|
+
act(() => {
|
|
817
|
+
eveExtras.tryGet(result.current.thread.getState().extras)!.reset();
|
|
818
|
+
});
|
|
819
|
+
await waitFor(() => {
|
|
820
|
+
expect(getText(result.current)).toEqual(["earlier", "earlier answer"]);
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
// `onReload` is only wired while staged messages exist, so a fresh draft
|
|
824
|
+
// is needed to reach the staged-run lookup that reads `stagedInputsRef`.
|
|
825
|
+
await stageMessage(result.current, "fresh draft");
|
|
826
|
+
|
|
827
|
+
await expect(
|
|
828
|
+
Promise.resolve(
|
|
829
|
+
result.current.thread.startRun({
|
|
830
|
+
parentId: discardedId,
|
|
831
|
+
sourceId: null,
|
|
832
|
+
runConfig: {},
|
|
833
|
+
}),
|
|
834
|
+
),
|
|
835
|
+
).rejects.toThrow("Runtime does not support reloading messages.");
|
|
836
|
+
expect(agent.send).not.toHaveBeenCalled();
|
|
837
|
+
expect(agent.reset).toHaveBeenCalledTimes(1);
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
it("clears executing tool state when reset is invoked", async () => {
|
|
841
|
+
const agent = createAgent({ data: settledData });
|
|
842
|
+
mockUseEveAgent.mockReturnValue(agent as never);
|
|
843
|
+
const { result, rerender } = renderHook(() => useEveAgentRuntime());
|
|
844
|
+
|
|
845
|
+
act(() => {
|
|
846
|
+
result.current.registerModelContextProvider({
|
|
847
|
+
getModelContext: () => ({
|
|
848
|
+
tools: {
|
|
849
|
+
slow_tool: {
|
|
850
|
+
parameters: { type: "object", properties: {} },
|
|
851
|
+
execute: () => new Promise<never>(() => {}),
|
|
852
|
+
},
|
|
853
|
+
},
|
|
854
|
+
}),
|
|
855
|
+
});
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
// The tracker treats its first snapshot as historical, so the tool call
|
|
859
|
+
// has to arrive on a later one to actually execute.
|
|
860
|
+
mockUseEveAgent.mockReturnValue(
|
|
861
|
+
createAgent({ data: executingToolData, reset: agent.reset }) as never,
|
|
862
|
+
);
|
|
863
|
+
rerender();
|
|
864
|
+
|
|
865
|
+
await waitFor(() => {
|
|
866
|
+
expect(result.current.thread.getState().isRunning).toBe(true);
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
act(() => {
|
|
870
|
+
eveExtras.tryGet(result.current.thread.getState().extras)!.reset();
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
await waitFor(() => {
|
|
874
|
+
expect(result.current.thread.getState().isRunning).toBe(false);
|
|
875
|
+
});
|
|
876
|
+
expect(agent.reset).toHaveBeenCalledTimes(1);
|
|
877
|
+
});
|
|
878
|
+
|
|
879
|
+
it("drops a send queued behind an active turn when reset is invoked", async () => {
|
|
880
|
+
let releaseFirstSend: (() => void) | undefined;
|
|
881
|
+
const send = vi
|
|
882
|
+
.fn()
|
|
883
|
+
.mockImplementationOnce(
|
|
884
|
+
() =>
|
|
885
|
+
new Promise<void>((resolve) => {
|
|
886
|
+
releaseFirstSend = resolve;
|
|
887
|
+
}),
|
|
888
|
+
)
|
|
889
|
+
.mockResolvedValue(undefined);
|
|
890
|
+
const agent = createAgent({ data: settledData, send });
|
|
891
|
+
mockUseEveAgent.mockReturnValue(agent as never);
|
|
892
|
+
const { result } = renderHook(() => useEveAgentRuntime());
|
|
893
|
+
|
|
894
|
+
await stageMessage(result.current, "queued draft");
|
|
895
|
+
const queuedDraftId = result.current.thread.getState().messages[2]!.id;
|
|
896
|
+
|
|
897
|
+
act(() => {
|
|
898
|
+
result.current.thread.append({
|
|
899
|
+
role: "user",
|
|
900
|
+
content: [{ type: "text", text: "first" }],
|
|
901
|
+
});
|
|
902
|
+
});
|
|
903
|
+
await waitFor(() => {
|
|
904
|
+
expect(send).toHaveBeenCalledTimes(1);
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
const queuedReload = Promise.resolve(
|
|
908
|
+
result.current.thread.startRun({
|
|
909
|
+
parentId: queuedDraftId,
|
|
910
|
+
sourceId: null,
|
|
911
|
+
runConfig: {},
|
|
912
|
+
}),
|
|
913
|
+
);
|
|
914
|
+
// Let the reload reach `enqueueSend` and park behind the active turn.
|
|
915
|
+
await act(async () => {});
|
|
916
|
+
|
|
917
|
+
act(() => {
|
|
918
|
+
eveExtras.tryGet(result.current.thread.getState().extras)!.reset();
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
await act(async () => {
|
|
922
|
+
releaseFirstSend?.();
|
|
923
|
+
await queuedReload;
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
expect(send).toHaveBeenCalledTimes(1);
|
|
927
|
+
await waitFor(() => {
|
|
928
|
+
expect(getText(result.current)).toEqual(["earlier", "earlier answer"]);
|
|
929
|
+
});
|
|
930
|
+
});
|
|
931
|
+
|
|
932
|
+
it("keeps a staged draft when a queued send is cancelled instead of reset", async () => {
|
|
933
|
+
let releaseFirstSend: (() => void) | undefined;
|
|
934
|
+
const send = vi
|
|
935
|
+
.fn()
|
|
936
|
+
.mockImplementationOnce(
|
|
937
|
+
() =>
|
|
938
|
+
new Promise<void>((resolve) => {
|
|
939
|
+
releaseFirstSend = resolve;
|
|
940
|
+
}),
|
|
941
|
+
)
|
|
942
|
+
.mockResolvedValue(undefined);
|
|
943
|
+
const agent = createAgent({ data: settledData, send });
|
|
944
|
+
mockUseEveAgent.mockReturnValue(agent as never);
|
|
945
|
+
const { result } = renderHook(() => useEveAgentRuntime());
|
|
946
|
+
|
|
947
|
+
await stageMessage(result.current, "queued draft");
|
|
948
|
+
const queuedDraftId = result.current.thread.getState().messages[2]!.id;
|
|
949
|
+
|
|
950
|
+
act(() => {
|
|
951
|
+
result.current.thread.append({
|
|
952
|
+
role: "user",
|
|
953
|
+
content: [{ type: "text", text: "first" }],
|
|
954
|
+
});
|
|
955
|
+
});
|
|
956
|
+
await waitFor(() => {
|
|
957
|
+
expect(send).toHaveBeenCalledTimes(1);
|
|
958
|
+
});
|
|
959
|
+
|
|
960
|
+
const queuedReload = Promise.resolve(
|
|
961
|
+
result.current.thread.startRun({
|
|
962
|
+
parentId: queuedDraftId,
|
|
963
|
+
sourceId: null,
|
|
964
|
+
runConfig: {},
|
|
965
|
+
}),
|
|
966
|
+
);
|
|
967
|
+
await act(async () => {});
|
|
968
|
+
|
|
969
|
+
act(() => {
|
|
970
|
+
result.current.thread.cancelRun();
|
|
971
|
+
});
|
|
972
|
+
|
|
973
|
+
await act(async () => {
|
|
974
|
+
releaseFirstSend?.();
|
|
975
|
+
await queuedReload;
|
|
976
|
+
});
|
|
977
|
+
|
|
978
|
+
expect(send).toHaveBeenCalledTimes(1);
|
|
979
|
+
expect(agent.stop).toHaveBeenCalledTimes(1);
|
|
980
|
+
await waitFor(() => {
|
|
981
|
+
expect(getText(result.current)).toEqual([
|
|
982
|
+
"earlier",
|
|
983
|
+
"earlier answer",
|
|
984
|
+
"queued draft",
|
|
985
|
+
]);
|
|
986
|
+
});
|
|
987
|
+
});
|
|
988
|
+
|
|
989
|
+
it("aborts client tool executions when reset is invoked", async () => {
|
|
990
|
+
const agentReset = vi.fn(() => {
|
|
991
|
+
mockUseEveAgent.mockReturnValue(
|
|
992
|
+
createAgent({ data: { messages: [] }, reset: agentReset }) as never,
|
|
993
|
+
);
|
|
994
|
+
});
|
|
995
|
+
const agent = createAgent({ data: settledData, reset: agentReset });
|
|
996
|
+
mockUseEveAgent.mockReturnValue(agent as never);
|
|
997
|
+
const { result, rerender } = renderHook(() => useEveAgentRuntime());
|
|
998
|
+
|
|
999
|
+
const abortReasons: string[] = [];
|
|
1000
|
+
act(() => {
|
|
1001
|
+
result.current.registerModelContextProvider({
|
|
1002
|
+
getModelContext: () => ({
|
|
1003
|
+
tools: {
|
|
1004
|
+
slow_tool: {
|
|
1005
|
+
parameters: { type: "object", properties: {} },
|
|
1006
|
+
execute: (
|
|
1007
|
+
_args: unknown,
|
|
1008
|
+
context: { toolCallId: string; abortSignal: AbortSignal },
|
|
1009
|
+
) =>
|
|
1010
|
+
new Promise<never>(() => {
|
|
1011
|
+
context.abortSignal.addEventListener("abort", () => {
|
|
1012
|
+
abortReasons.push(context.toolCallId);
|
|
1013
|
+
});
|
|
1014
|
+
}),
|
|
1015
|
+
},
|
|
1016
|
+
},
|
|
1017
|
+
}),
|
|
1018
|
+
});
|
|
1019
|
+
});
|
|
1020
|
+
|
|
1021
|
+
mockUseEveAgent.mockReturnValue(
|
|
1022
|
+
createAgent({ data: twoExecutingToolsData, reset: agentReset }) as never,
|
|
1023
|
+
);
|
|
1024
|
+
rerender();
|
|
1025
|
+
|
|
1026
|
+
await waitFor(() => {
|
|
1027
|
+
expect(result.current.thread.getState().isRunning).toBe(true);
|
|
1028
|
+
});
|
|
1029
|
+
|
|
1030
|
+
act(() => {
|
|
1031
|
+
eveExtras.tryGet(result.current.thread.getState().extras)!.reset();
|
|
1032
|
+
});
|
|
1033
|
+
rerender();
|
|
1034
|
+
|
|
1035
|
+
await waitFor(() => {
|
|
1036
|
+
expect(abortReasons.sort()).toEqual(["call_slow_a", "call_slow_b"]);
|
|
1037
|
+
});
|
|
1038
|
+
expect(result.current.thread.getState().isRunning).toBe(false);
|
|
1039
|
+
});
|
|
1040
|
+
|
|
1041
|
+
it("rejects a pending human-input request when reset is invoked", async () => {
|
|
1042
|
+
const agentReset = vi.fn(() => {
|
|
1043
|
+
mockUseEveAgent.mockReturnValue(
|
|
1044
|
+
createAgent({ data: { messages: [] }, reset: agentReset }) as never,
|
|
1045
|
+
);
|
|
1046
|
+
});
|
|
1047
|
+
const agent = createAgent({ data: settledData, reset: agentReset });
|
|
1048
|
+
mockUseEveAgent.mockReturnValue(agent as never);
|
|
1049
|
+
const { result, rerender } = renderHook(() => useEveAgentRuntime());
|
|
1050
|
+
|
|
1051
|
+
const humanRejections: unknown[] = [];
|
|
1052
|
+
act(() => {
|
|
1053
|
+
result.current.registerModelContextProvider({
|
|
1054
|
+
getModelContext: () => ({
|
|
1055
|
+
tools: {
|
|
1056
|
+
slow_tool: {
|
|
1057
|
+
parameters: { type: "object", properties: {} },
|
|
1058
|
+
execute: async (
|
|
1059
|
+
_args: unknown,
|
|
1060
|
+
context: { human: (payload: unknown) => Promise<unknown> },
|
|
1061
|
+
) => {
|
|
1062
|
+
try {
|
|
1063
|
+
return await context.human({ request: "approve" });
|
|
1064
|
+
} catch (error) {
|
|
1065
|
+
humanRejections.push(error);
|
|
1066
|
+
throw error;
|
|
1067
|
+
}
|
|
1068
|
+
},
|
|
1069
|
+
},
|
|
1070
|
+
},
|
|
1071
|
+
}),
|
|
1072
|
+
});
|
|
1073
|
+
});
|
|
1074
|
+
|
|
1075
|
+
mockUseEveAgent.mockReturnValue(
|
|
1076
|
+
createAgent({ data: twoExecutingToolsData, reset: agentReset }) as never,
|
|
1077
|
+
);
|
|
1078
|
+
rerender();
|
|
1079
|
+
|
|
1080
|
+
await waitFor(() => {
|
|
1081
|
+
expect(result.current.thread.getState().isRunning).toBe(true);
|
|
1082
|
+
});
|
|
1083
|
+
|
|
1084
|
+
act(() => {
|
|
1085
|
+
eveExtras.tryGet(result.current.thread.getState().extras)!.reset();
|
|
1086
|
+
});
|
|
1087
|
+
rerender();
|
|
1088
|
+
|
|
1089
|
+
await waitFor(() => {
|
|
1090
|
+
expect(humanRejections).toHaveLength(2);
|
|
1091
|
+
});
|
|
1092
|
+
expect((humanRejections[0] as Error).message).toBe(
|
|
1093
|
+
"Tool execution aborted",
|
|
1094
|
+
);
|
|
1095
|
+
});
|
|
1096
|
+
|
|
1097
|
+
it("ignores tool statuses left over from a discarded session", async () => {
|
|
1098
|
+
const releases: Record<string, () => void> = {};
|
|
1099
|
+
const agentReset = vi.fn(() => {
|
|
1100
|
+
mockUseEveAgent.mockReturnValue(
|
|
1101
|
+
createAgent({ data: { messages: [] }, reset: agentReset }) as never,
|
|
1102
|
+
);
|
|
1103
|
+
});
|
|
1104
|
+
const agent = createAgent({ data: settledData, reset: agentReset });
|
|
1105
|
+
mockUseEveAgent.mockReturnValue(agent as never);
|
|
1106
|
+
const { result, rerender } = renderHook(() => useEveAgentRuntime());
|
|
1107
|
+
|
|
1108
|
+
act(() => {
|
|
1109
|
+
result.current.registerModelContextProvider({
|
|
1110
|
+
getModelContext: () => ({
|
|
1111
|
+
tools: {
|
|
1112
|
+
slow_tool: {
|
|
1113
|
+
parameters: { type: "object", properties: {} },
|
|
1114
|
+
execute: (_args: unknown, context: { toolCallId: string }) =>
|
|
1115
|
+
new Promise<never>((_resolve, reject) => {
|
|
1116
|
+
releases[context.toolCallId] = () =>
|
|
1117
|
+
reject(new Error("aborted"));
|
|
1118
|
+
}),
|
|
1119
|
+
},
|
|
1120
|
+
},
|
|
1121
|
+
}),
|
|
1122
|
+
});
|
|
1123
|
+
});
|
|
1124
|
+
|
|
1125
|
+
mockUseEveAgent.mockReturnValue(
|
|
1126
|
+
createAgent({
|
|
1127
|
+
data: twoExecutingToolsData,
|
|
1128
|
+
reset: agentReset,
|
|
1129
|
+
}) as never,
|
|
1130
|
+
);
|
|
1131
|
+
rerender();
|
|
1132
|
+
|
|
1133
|
+
await waitFor(() => {
|
|
1134
|
+
expect(result.current.thread.getState().isRunning).toBe(true);
|
|
1135
|
+
});
|
|
1136
|
+
|
|
1137
|
+
act(() => {
|
|
1138
|
+
eveExtras.tryGet(result.current.thread.getState().extras)!.reset();
|
|
1139
|
+
});
|
|
1140
|
+
rerender();
|
|
1141
|
+
|
|
1142
|
+
await waitFor(() => {
|
|
1143
|
+
expect(result.current.thread.getState().isRunning).toBe(false);
|
|
1144
|
+
});
|
|
1145
|
+
|
|
1146
|
+
await act(async () => {
|
|
1147
|
+
releases["call_slow_a"]?.();
|
|
1148
|
+
await Promise.resolve();
|
|
1149
|
+
});
|
|
1150
|
+
|
|
1151
|
+
await waitFor(() => {
|
|
1152
|
+
expect(result.current.thread.getState().isRunning).toBe(false);
|
|
1153
|
+
});
|
|
1154
|
+
expect(agentReset).toHaveBeenCalledTimes(1);
|
|
1155
|
+
});
|
|
1156
|
+
|
|
1157
|
+
it("promotes every staged draft after reset when the discarded run failed", async () => {
|
|
1158
|
+
const send = vi.fn().mockResolvedValue(undefined);
|
|
1159
|
+
const agent = createAgent({ data: settledData, send });
|
|
1160
|
+
let capturedOptions: { onFinish?: (snapshot: unknown) => void } = {};
|
|
1161
|
+
mockUseEveAgent.mockImplementation((options) => {
|
|
1162
|
+
capturedOptions = options as typeof capturedOptions;
|
|
1163
|
+
return agent as never;
|
|
1164
|
+
});
|
|
1165
|
+
const { result } = renderHook(() => useEveAgentRuntime());
|
|
1166
|
+
|
|
1167
|
+
act(() => {
|
|
1168
|
+
capturedOptions.onFinish?.({ status: "error" });
|
|
1169
|
+
});
|
|
1170
|
+
act(() => {
|
|
1171
|
+
eveExtras.tryGet(result.current.thread.getState().extras)!.reset();
|
|
1172
|
+
});
|
|
1173
|
+
|
|
1174
|
+
await stageMessage(result.current, "first staged");
|
|
1175
|
+
await stageMessage(result.current, "second staged");
|
|
1176
|
+
|
|
1177
|
+
const secondStagedId = result.current.thread.getState().messages[3]!.id;
|
|
1178
|
+
await act(async () => {
|
|
1179
|
+
await result.current.thread.startRun({
|
|
1180
|
+
parentId: secondStagedId,
|
|
1181
|
+
sourceId: null,
|
|
1182
|
+
runConfig: {},
|
|
1183
|
+
});
|
|
1184
|
+
});
|
|
1185
|
+
|
|
1186
|
+
expect(send).toHaveBeenCalledTimes(2);
|
|
1187
|
+
expect(send).toHaveBeenNthCalledWith(1, "first staged", undefined);
|
|
1188
|
+
expect(send).toHaveBeenNthCalledWith(2, "second staged", undefined);
|
|
1189
|
+
});
|
|
1190
|
+
});
|
|
1191
|
+
|
|
759
1192
|
const approvalData: EveMessageData = {
|
|
760
1193
|
messages: [
|
|
761
1194
|
{ id: "u1", role: "user", parts: [{ type: "text", text: "send it" }] },
|
|
@@ -776,11 +1209,12 @@ const approvalData: EveMessageData = {
|
|
|
776
1209
|
name: "send_email",
|
|
777
1210
|
inputRequest: {
|
|
778
1211
|
requestId: "req_1",
|
|
1212
|
+
kind: "tool-approval",
|
|
779
1213
|
prompt: "Send the email?",
|
|
780
1214
|
display: "confirmation",
|
|
781
1215
|
options: [
|
|
782
1216
|
{ id: "approve", label: "Approve" },
|
|
783
|
-
{ id: "
|
|
1217
|
+
{ id: "cancel", label: "Cancel" },
|
|
784
1218
|
],
|
|
785
1219
|
},
|
|
786
1220
|
},
|
|
@@ -794,16 +1228,14 @@ const approvalData: EveMessageData = {
|
|
|
794
1228
|
describe("useEveAgentRuntime concurrent sends", () => {
|
|
795
1229
|
it("defers an approval clicked while a turn is in flight until the turn parks", async () => {
|
|
796
1230
|
let resolveFirstSend!: () => void;
|
|
797
|
-
const send = vi
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
.mockResolvedValue(undefined);
|
|
806
|
-
const agent = createAgent({ data: approvalData, send });
|
|
1231
|
+
const send = vi.fn().mockImplementationOnce(
|
|
1232
|
+
() =>
|
|
1233
|
+
new Promise<void>((resolve) => {
|
|
1234
|
+
resolveFirstSend = resolve;
|
|
1235
|
+
}),
|
|
1236
|
+
);
|
|
1237
|
+
const respond = vi.fn().mockResolvedValue(undefined);
|
|
1238
|
+
const agent = createAgent({ data: approvalData, send, respond });
|
|
807
1239
|
mockUseEveAgent.mockReturnValue(agent as never);
|
|
808
1240
|
const { result } = renderHook(() => useEveAgentRuntime());
|
|
809
1241
|
|
|
@@ -821,15 +1253,15 @@ describe("useEveAgentRuntime concurrent sends", () => {
|
|
|
821
1253
|
.getMessagePartByToolCallId("call_1")
|
|
822
1254
|
.respondToToolApproval({ optionId: "approve" });
|
|
823
1255
|
});
|
|
824
|
-
expect(
|
|
1256
|
+
expect(respond).not.toHaveBeenCalled();
|
|
825
1257
|
|
|
826
1258
|
await act(async () => {
|
|
827
1259
|
resolveFirstSend();
|
|
828
1260
|
});
|
|
829
|
-
await waitFor(() => expect(
|
|
830
|
-
expect(
|
|
831
|
-
|
|
832
|
-
|
|
1261
|
+
await waitFor(() => expect(respond).toHaveBeenCalledTimes(1));
|
|
1262
|
+
expect(respond).toHaveBeenCalledWith([
|
|
1263
|
+
{ requestId: "req_1", optionId: "approve" },
|
|
1264
|
+
]);
|
|
833
1265
|
});
|
|
834
1266
|
|
|
835
1267
|
it("queues a send issued while a turn is in flight and preserves order", async () => {
|
|
@@ -867,8 +1299,8 @@ describe("useEveAgentRuntime concurrent sends", () => {
|
|
|
867
1299
|
resolveFirstSend();
|
|
868
1300
|
});
|
|
869
1301
|
await waitFor(() => expect(send).toHaveBeenCalledTimes(2));
|
|
870
|
-
expect(send).toHaveBeenNthCalledWith(1,
|
|
871
|
-
expect(send).toHaveBeenNthCalledWith(2,
|
|
1302
|
+
expect(send).toHaveBeenNthCalledWith(1, "first", undefined);
|
|
1303
|
+
expect(send).toHaveBeenNthCalledWith(2, "second", undefined);
|
|
872
1304
|
});
|
|
873
1305
|
|
|
874
1306
|
it("drops a queued send when the run is cancelled before it dispatches", async () => {
|
|
@@ -911,6 +1343,141 @@ describe("useEveAgentRuntime concurrent sends", () => {
|
|
|
911
1343
|
expect(send).toHaveBeenCalledTimes(1);
|
|
912
1344
|
});
|
|
913
1345
|
|
|
1346
|
+
it("returns a cancelled queued send to the composer", async () => {
|
|
1347
|
+
let resolveFirstSend!: () => void;
|
|
1348
|
+
const send = vi
|
|
1349
|
+
.fn()
|
|
1350
|
+
.mockImplementationOnce(
|
|
1351
|
+
() =>
|
|
1352
|
+
new Promise<void>((resolve) => {
|
|
1353
|
+
resolveFirstSend = resolve;
|
|
1354
|
+
}),
|
|
1355
|
+
)
|
|
1356
|
+
.mockResolvedValue(undefined);
|
|
1357
|
+
const agent = createAgent({ data: settledData, send });
|
|
1358
|
+
mockUseEveAgent.mockReturnValue(agent as never);
|
|
1359
|
+
const { result } = renderHook(() => useEveAgentRuntime());
|
|
1360
|
+
|
|
1361
|
+
await act(async () => {
|
|
1362
|
+
result.current.thread.append({
|
|
1363
|
+
role: "user",
|
|
1364
|
+
content: [{ type: "text", text: "first" }],
|
|
1365
|
+
});
|
|
1366
|
+
});
|
|
1367
|
+
await waitFor(() => expect(send).toHaveBeenCalledTimes(1));
|
|
1368
|
+
|
|
1369
|
+
await act(async () => {
|
|
1370
|
+
result.current.thread.composer.setText("queued");
|
|
1371
|
+
result.current.thread.composer.send();
|
|
1372
|
+
});
|
|
1373
|
+
expect(result.current.thread.composer.getState().text).toBe("");
|
|
1374
|
+
|
|
1375
|
+
act(() => {
|
|
1376
|
+
result.current.thread.cancelRun();
|
|
1377
|
+
});
|
|
1378
|
+
await act(async () => {
|
|
1379
|
+
resolveFirstSend();
|
|
1380
|
+
});
|
|
1381
|
+
|
|
1382
|
+
await waitFor(() => {
|
|
1383
|
+
expect(result.current.thread.composer.getState().text).toBe("queued");
|
|
1384
|
+
});
|
|
1385
|
+
expect(send).toHaveBeenCalledTimes(1);
|
|
1386
|
+
expect(getText(result.current)).toEqual(["earlier", "earlier answer"]);
|
|
1387
|
+
});
|
|
1388
|
+
|
|
1389
|
+
it("returns the queued send when cancel keeps the trailing message", async () => {
|
|
1390
|
+
let resolveFirstSend!: () => void;
|
|
1391
|
+
const send = vi
|
|
1392
|
+
.fn()
|
|
1393
|
+
.mockImplementationOnce(
|
|
1394
|
+
() =>
|
|
1395
|
+
new Promise<void>((resolve) => {
|
|
1396
|
+
resolveFirstSend = resolve;
|
|
1397
|
+
}),
|
|
1398
|
+
)
|
|
1399
|
+
.mockResolvedValue(undefined);
|
|
1400
|
+
// Before the turn streams, the dispatched message is the thread's trailing
|
|
1401
|
+
// user leaf. Eve owns that message and cannot remove it on cancel.
|
|
1402
|
+
const agent = createAgent({
|
|
1403
|
+
data: {
|
|
1404
|
+
messages: [
|
|
1405
|
+
...settledData.messages,
|
|
1406
|
+
{ id: "u2", role: "user", parts: [{ type: "text", text: "first" }] },
|
|
1407
|
+
],
|
|
1408
|
+
} satisfies EveMessageData,
|
|
1409
|
+
status: "submitted",
|
|
1410
|
+
send,
|
|
1411
|
+
});
|
|
1412
|
+
mockUseEveAgent.mockReturnValue(agent as never);
|
|
1413
|
+
const { result } = renderHook(() => useEveAgentRuntime());
|
|
1414
|
+
|
|
1415
|
+
await act(async () => {
|
|
1416
|
+
result.current.thread.composer.setText("first");
|
|
1417
|
+
result.current.thread.composer.send();
|
|
1418
|
+
});
|
|
1419
|
+
await waitFor(() => expect(send).toHaveBeenCalledTimes(1));
|
|
1420
|
+
|
|
1421
|
+
await act(async () => {
|
|
1422
|
+
result.current.thread.composer.setText("queued");
|
|
1423
|
+
result.current.thread.composer.send();
|
|
1424
|
+
});
|
|
1425
|
+
act(() => {
|
|
1426
|
+
result.current.thread.cancelRun();
|
|
1427
|
+
});
|
|
1428
|
+
expect(result.current.thread.composer.getState().text).toBe("");
|
|
1429
|
+
|
|
1430
|
+
await act(async () => {
|
|
1431
|
+
resolveFirstSend();
|
|
1432
|
+
});
|
|
1433
|
+
|
|
1434
|
+
await waitFor(() => {
|
|
1435
|
+
expect(result.current.thread.composer.getState().text).toBe("queued");
|
|
1436
|
+
});
|
|
1437
|
+
expect(send).toHaveBeenCalledTimes(1);
|
|
1438
|
+
});
|
|
1439
|
+
|
|
1440
|
+
it("keeps a cancelled tool approval discarded", async () => {
|
|
1441
|
+
let resolveFirstSend!: () => void;
|
|
1442
|
+
const send = vi.fn().mockImplementationOnce(
|
|
1443
|
+
() =>
|
|
1444
|
+
new Promise<void>((resolve) => {
|
|
1445
|
+
resolveFirstSend = resolve;
|
|
1446
|
+
}),
|
|
1447
|
+
);
|
|
1448
|
+
const respond = vi.fn().mockResolvedValue(undefined);
|
|
1449
|
+
const agent = createAgent({ data: approvalData, send, respond });
|
|
1450
|
+
mockUseEveAgent.mockReturnValue(agent as never);
|
|
1451
|
+
const { result } = renderHook(() => useEveAgentRuntime());
|
|
1452
|
+
const before = getText(result.current);
|
|
1453
|
+
|
|
1454
|
+
await act(async () => {
|
|
1455
|
+
result.current.thread.append({
|
|
1456
|
+
role: "user",
|
|
1457
|
+
content: [{ type: "text", text: "go" }],
|
|
1458
|
+
});
|
|
1459
|
+
});
|
|
1460
|
+
await waitFor(() => expect(send).toHaveBeenCalledTimes(1));
|
|
1461
|
+
|
|
1462
|
+
act(() => {
|
|
1463
|
+
result.current.thread
|
|
1464
|
+
.getMessageByIndex(1)
|
|
1465
|
+
.getMessagePartByToolCallId("call_1")
|
|
1466
|
+
.respondToToolApproval({ optionId: "approve" });
|
|
1467
|
+
});
|
|
1468
|
+
act(() => {
|
|
1469
|
+
result.current.thread.cancelRun();
|
|
1470
|
+
});
|
|
1471
|
+
await act(async () => {
|
|
1472
|
+
resolveFirstSend();
|
|
1473
|
+
});
|
|
1474
|
+
|
|
1475
|
+
expect(send).toHaveBeenCalledTimes(1);
|
|
1476
|
+
expect(respond).not.toHaveBeenCalled();
|
|
1477
|
+
expect(result.current.thread.composer.getState().text).toBe("");
|
|
1478
|
+
expect(getText(result.current)).toEqual(before);
|
|
1479
|
+
});
|
|
1480
|
+
|
|
914
1481
|
it("drops a queued send when the hook unmounts before it dispatches", async () => {
|
|
915
1482
|
let resolveFirstSend!: () => void;
|
|
916
1483
|
const send = vi
|
|
@@ -1006,10 +1573,10 @@ describe("useEveAgentRuntime concurrent sends", () => {
|
|
|
1006
1573
|
});
|
|
1007
1574
|
await waitFor(() => expect(send).toHaveBeenCalledTimes(3));
|
|
1008
1575
|
|
|
1009
|
-
expect(send.mock.calls.map(([
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1576
|
+
expect(send.mock.calls.map(([message]) => message)).toEqual([
|
|
1577
|
+
"first staged",
|
|
1578
|
+
"interleaved",
|
|
1579
|
+
"second staged",
|
|
1013
1580
|
]);
|
|
1014
1581
|
});
|
|
1015
1582
|
});
|