@assistant-ui/eve 0.0.14 → 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.
@@ -0,0 +1,89 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { act, renderHook } from "@testing-library/react";
4
+ import { describe, expect, it, vi } from "vitest";
5
+ import type { HandleMessageStreamEvent } from "eve/client";
6
+
7
+ import { useEveAgentRuntime } from "./useEveAgentRuntime";
8
+
9
+ const completedTurn = (
10
+ turnId: string,
11
+ text: string,
12
+ reply: string,
13
+ sequence: number,
14
+ ) =>
15
+ [
16
+ {
17
+ type: "turn.started",
18
+ data: { sequence, turnId },
19
+ meta: { at: "2026-01-02T10:00:00.000Z", id: `${turnId}-1` },
20
+ },
21
+ {
22
+ type: "message.received",
23
+ data: { message: text, sequence: sequence + 1, turnId },
24
+ meta: { at: "2026-01-02T10:00:01.000Z", id: `${turnId}-2` },
25
+ },
26
+ {
27
+ type: "step.started",
28
+ data: { modelId: "m", sequence: sequence + 2, stepIndex: 0, turnId },
29
+ meta: { at: "2026-01-02T10:00:02.000Z", id: `${turnId}-3` },
30
+ },
31
+ {
32
+ type: "message.completed",
33
+ data: {
34
+ finishReason: "stop",
35
+ message: reply,
36
+ sequence: sequence + 3,
37
+ stepIndex: 0,
38
+ turnId,
39
+ },
40
+ meta: { at: "2026-01-02T10:00:03.000Z", id: `${turnId}-4` },
41
+ },
42
+ {
43
+ type: "turn.completed",
44
+ data: { sequence: sequence + 4, turnId },
45
+ meta: { at: "2026-01-02T10:00:04.000Z", id: `${turnId}-5` },
46
+ },
47
+ ] as const satisfies readonly HandleMessageStreamEvent[];
48
+
49
+ const knownTurn = completedTurn("t1", "hi", "hello", 1);
50
+ const outOfBandTurn = completedTurn("t2", "answered elsewhere", "done", 6);
51
+
52
+ describe("useEveAgentRuntime against a durable eve session", () => {
53
+ it("surfaces a turn the server appended out of band after reloadMainThread", async () => {
54
+ const stream = vi.fn(async function* () {
55
+ yield* [...knownTurn, ...outOfBandTurn];
56
+ });
57
+ const session = {
58
+ state: { sessionId: "s1" },
59
+ stream,
60
+ send: () => {
61
+ throw new Error("the refetch test must not send");
62
+ },
63
+ } as never;
64
+
65
+ const { result } = renderHook(() =>
66
+ useEveAgentRuntime({ initialEvents: knownTurn as never, session }),
67
+ );
68
+ expect(
69
+ result.current.thread.getState().messages.map((m) => m.role),
70
+ ).toEqual(["user", "assistant"]);
71
+
72
+ await act(async () => {
73
+ await result.current.threads.reloadMainThread();
74
+ });
75
+
76
+ expect(stream).toHaveBeenCalled();
77
+ const messages = result.current.thread.getState().messages;
78
+ expect(messages.map((m) => m.role)).toEqual([
79
+ "user",
80
+ "assistant",
81
+ "user",
82
+ "assistant",
83
+ ]);
84
+ expect(messages[3]?.content).toMatchObject([
85
+ { type: "text", text: "done" },
86
+ ]);
87
+ expect(result.current.thread.getState().isRunning).toBe(false);
88
+ });
89
+ });
@@ -328,6 +328,163 @@ describe("useEveAgentRuntime status forwarding", () => {
328
328
  });
329
329
  });
330
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
+
331
488
  const settledData: EveMessageData = {
332
489
  messages: [
333
490
  { id: "u1", role: "user", parts: [{ type: "text", text: "earlier" }] },
@@ -1580,3 +1737,285 @@ describe("useEveAgentRuntime concurrent sends", () => {
1580
1737
  ]);
1581
1738
  });
1582
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
+ });
@@ -28,6 +28,7 @@ import {
28
28
  } from "eve/react";
29
29
  import {
30
30
  convertEveMessages,
31
+ findEveInputRequest,
31
32
  getEveMessageContent,
32
33
  toEveInputResponse,
33
34
  } from "./convertEveMessages";
@@ -199,10 +200,9 @@ export const useEveAgentRuntime = (options: UseEveAgentRuntimeOptions = {}) => {
199
200
  const hasExecutingTools = Object.values(toolStatuses).some(
200
201
  (status) => status?.type === "executing",
201
202
  );
202
- const isRunning =
203
- agent.status === "submitted" ||
204
- agent.status === "streaming" ||
205
- hasExecutingTools;
203
+ const providerIsRunning =
204
+ agent.status === "submitted" || agent.status === "streaming";
205
+ const isRunning = providerIsRunning || hasExecutingTools;
206
206
 
207
207
  const convertedMessages = useMemo(() => {
208
208
  const createdAtByMessageId = createdAtByMessageIdRef.current;
@@ -230,6 +230,8 @@ export const useEveAgentRuntime = (options: UseEveAgentRuntimeOptions = {}) => {
230
230
  const messages = stagedMessages ?? convertedMessages;
231
231
  const messagesRef = useRef(messages);
232
232
  messagesRef.current = messages;
233
+ const agentRef = useRef(agent);
234
+ agentRef.current = agent;
233
235
 
234
236
  // Upstream `EveAgentStore` `send` and `respond` reject while a turn is in
235
237
  // flight and only resolve once the turn's stream parks, so a pending chain
@@ -239,18 +241,22 @@ export const useEveAgentRuntime = (options: UseEveAgentRuntimeOptions = {}) => {
239
241
  const sendEpochRef = useRef(0);
240
242
  // A cancel drops the queued send but keeps its draft for a later promotion;
241
243
  // only a reset discards the draft with the session, so the two need separate
242
- // counters.
244
+ // counters. A queued read has no draft and outlives a cancel, so it waits on
245
+ // the reset counter instead.
243
246
  const resetEpochRef = useRef(0);
244
247
  const isMountedRef = useRef(true);
245
248
  const runtimeRef = useRef<ReturnType<typeof useExternalStoreRuntime> | null>(
246
249
  null,
247
250
  );
248
251
 
249
- const enqueueSend = (dispatch: () => Promise<void>) => {
250
- const epoch = sendEpochRef.current;
252
+ const enqueueSend = (
253
+ dispatch: () => Promise<void>,
254
+ epochRef: { current: number } = sendEpochRef,
255
+ ) => {
256
+ const epoch = epochRef.current;
251
257
  const next = sendChainRef.current.then(() => {
252
- if (epoch !== sendEpochRef.current)
253
- throw isMountedRef.current ? sendCancelledError : sendAbandonedError;
258
+ if (!isMountedRef.current) throw sendAbandonedError;
259
+ if (epoch !== epochRef.current) throw sendCancelledError;
254
260
  return dispatch();
255
261
  });
256
262
  sendChainRef.current = next.catch(() => {});
@@ -339,7 +345,7 @@ export const useEveAgentRuntime = (options: UseEveAgentRuntimeOptions = {}) => {
339
345
  const runtime = useExternalStoreRuntime({
340
346
  ...pickExternalStoreSharedOptions(options),
341
347
  messages,
342
- isRunning,
348
+ isRunning: providerIsRunning,
343
349
  extras,
344
350
  unstable_enableToolInvocations: true,
345
351
  setToolStatuses,
@@ -419,18 +425,59 @@ export const useEveAgentRuntime = (options: UseEveAgentRuntimeOptions = {}) => {
419
425
  },
420
426
  }
421
427
  : {}),
422
- onCancel: () => {
428
+ onCancel: async () => {
423
429
  sendEpochRef.current += 1;
424
- agent.stop();
425
- return Promise.resolve();
426
- },
427
- onRespondToToolApproval: async (response) => {
428
- try {
429
- await enqueueSend(() => agent.respond([toEveInputResponse(response)]));
430
- } catch (error) {
431
- if (!isDroppedSend(error)) throw error;
430
+ // Eve 0.38 replaced the binding's local-abort `stop()` with the durable
431
+ // `cancel()`, so the adapter detects which side of that break the host's
432
+ // eve provides instead of pinning the peer range to one of them.
433
+ const controls = agent as
434
+ | { readonly cancel: () => Promise<unknown> }
435
+ | { readonly stop: () => void };
436
+ if ("cancel" in controls) {
437
+ await controls.cancel();
438
+ } else {
439
+ controls.stop();
432
440
  }
433
441
  },
442
+ // Hosts below eve 0.44.1 expose no `resume`; leaving the capability absent
443
+ // keeps `threads.reloadMainThread()` on core's no-capability no-op.
444
+ ...("resume" in agent
445
+ ? {
446
+ onRefetchThread: () =>
447
+ // `resume()` rejects during a turn, so the replay rides the send
448
+ // chain like every other dispatch and runs once the turn parks; a
449
+ // cancel of that turn does not drop it, or the caller would be told
450
+ // the thread was refetched when it was not. Upstream shares one
451
+ // replay across concurrent `resume()` calls, so a refetch
452
+ // dispatched while `resume: true` is replaying on mount joins that
453
+ // read instead of taking a fresh one. The session is read at
454
+ // dispatch time because nothing durable exists before the first
455
+ // send lands.
456
+ enqueueSend(async () => {
457
+ const live = agentRef.current;
458
+ if (live.session === undefined) return;
459
+ await live.resume();
460
+ }, resetEpochRef).catch((error) => {
461
+ if (isDroppedSend(error)) return;
462
+ throw error;
463
+ }),
464
+ }
465
+ : {}),
466
+ onRespondToToolApproval: (response) => {
467
+ // Eve leaves an unanswered request pending, so an unmappable response
468
+ // must stay answerable. Mapping before the first await lets the mapper's
469
+ // own error surface synchronously to the caller that rendered the
470
+ // controls, the only signal the void `respondToApproval` seam carries.
471
+ const inputResponse = toEveInputResponse(
472
+ response,
473
+ findEveInputRequest(agent.data, response.approvalId),
474
+ );
475
+ return enqueueSend(() => agent.respond([inputResponse])).catch(
476
+ (error) => {
477
+ if (!isDroppedSend(error)) throw error;
478
+ },
479
+ );
480
+ },
434
481
  });
435
482
  runtimeRef.current = runtime;
436
483