@assistant-ui/eve 0.0.15 → 0.0.17
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 +2 -1
- package/dist/convertEveMessages.d.ts.map +1 -1
- package/dist/convertEveMessages.js +32 -5
- package/dist/convertEveMessages.js.map +1 -1
- package/dist/deriveCreatedAt.d.ts +23 -0
- package/dist/deriveCreatedAt.d.ts.map +1 -0
- package/dist/deriveCreatedAt.js +79 -0
- package/dist/deriveCreatedAt.js.map +1 -0
- package/dist/useEveAgentRuntime.d.ts.map +1 -1
- package/dist/useEveAgentRuntime.js +13 -4
- package/dist/useEveAgentRuntime.js.map +1 -1
- package/package.json +6 -6
- package/src/convertEveMessages.test.ts +177 -2
- package/src/convertEveMessages.ts +52 -12
- package/src/deriveCreatedAt.test.ts +190 -0
- package/src/deriveCreatedAt.ts +96 -0
- package/src/useEveAgentRuntime.eveResume.test.tsx +80 -0
- package/src/useEveAgentRuntime.test.tsx +224 -5
- package/src/useEveAgentRuntime.ts +39 -8
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
// Deliberately does not mock `eve/react`: the real `useEveAgent` resumes an
|
|
4
|
+
// offline session so the durable `meta.at` values travel through eve's own
|
|
5
|
+
// store and reducer before the adapter reads them. Supplying `session` keeps
|
|
6
|
+
// the store from constructing a network client.
|
|
7
|
+
|
|
8
|
+
import { renderHook, waitFor } from "@testing-library/react";
|
|
9
|
+
import { describe, expect, it } from "vitest";
|
|
10
|
+
import type { MessageStreamEvent } from "eve/client";
|
|
11
|
+
|
|
12
|
+
import { useEveAgentRuntime } from "./useEveAgentRuntime";
|
|
13
|
+
|
|
14
|
+
const TURN = "turn_resumed";
|
|
15
|
+
|
|
16
|
+
const USER_AT = "2026-01-02T10:00:01.000Z";
|
|
17
|
+
const ASSISTANT_AT = "2026-01-02T10:02:00.000Z";
|
|
18
|
+
|
|
19
|
+
const resumedEvents = [
|
|
20
|
+
{
|
|
21
|
+
type: "turn.started",
|
|
22
|
+
data: { sequence: 1, turnId: TURN },
|
|
23
|
+
meta: { at: "2026-01-02T10:00:00.000Z", id: "evt_001" },
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
type: "message.received",
|
|
27
|
+
data: { message: "hi", sequence: 2, turnId: TURN },
|
|
28
|
+
meta: { at: USER_AT, id: "evt_101" },
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
type: "step.started",
|
|
32
|
+
data: { modelId: "test-model", sequence: 3, stepIndex: 0, turnId: TURN },
|
|
33
|
+
meta: { at: ASSISTANT_AT, id: "evt_102" },
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
type: "message.completed",
|
|
37
|
+
data: {
|
|
38
|
+
finishReason: "stop",
|
|
39
|
+
message: "hello",
|
|
40
|
+
sequence: 5,
|
|
41
|
+
stepIndex: 0,
|
|
42
|
+
turnId: TURN,
|
|
43
|
+
},
|
|
44
|
+
meta: { at: "2026-01-02T10:02:06.000Z", id: "evt_003" },
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
type: "turn.completed",
|
|
48
|
+
data: { sequence: 6, turnId: TURN },
|
|
49
|
+
meta: { at: "2026-01-02T10:02:07.000Z", id: "evt_004" },
|
|
50
|
+
},
|
|
51
|
+
] as const satisfies readonly MessageStreamEvent[];
|
|
52
|
+
|
|
53
|
+
const offlineSession = {
|
|
54
|
+
state: { sessionId: "session_resumed", streamIndex: 0 },
|
|
55
|
+
cancel: async () => ({ status: "no_active_turn" }),
|
|
56
|
+
send: () => {
|
|
57
|
+
throw new Error("the resume test must not reach the network");
|
|
58
|
+
},
|
|
59
|
+
stream: async function* () {
|
|
60
|
+
yield* JSON.parse(
|
|
61
|
+
JSON.stringify(resumedEvents),
|
|
62
|
+
) as readonly MessageStreamEvent[];
|
|
63
|
+
},
|
|
64
|
+
} as never;
|
|
65
|
+
|
|
66
|
+
describe("useEveAgentRuntime against a resumed eve session", () => {
|
|
67
|
+
it("renders resumed history at its durable event times, not the current time", async () => {
|
|
68
|
+
const { result } = renderHook(() =>
|
|
69
|
+
useEveAgentRuntime({ resume: true, session: offlineSession }),
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
await waitFor(() =>
|
|
73
|
+
expect(result.current.thread.getState().messages).toHaveLength(2),
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const messages = result.current.thread.getState().messages;
|
|
77
|
+
expect(messages[0]?.createdAt).toEqual(new Date(USER_AT));
|
|
78
|
+
expect(messages[1]?.createdAt).toEqual(new Date(ASSISTANT_AT));
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
// @vitest-environment jsdom
|
|
2
2
|
|
|
3
|
-
import { act, renderHook, waitFor } from "@testing-library/react";
|
|
3
|
+
import { act, render, renderHook, waitFor } from "@testing-library/react";
|
|
4
|
+
import {
|
|
5
|
+
startTransition,
|
|
6
|
+
Suspense,
|
|
7
|
+
useLayoutEffect,
|
|
8
|
+
type PropsWithChildren,
|
|
9
|
+
} from "react";
|
|
4
10
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
5
11
|
|
|
6
12
|
const { mockUseEveAgent } = vi.hoisted(() => ({
|
|
@@ -12,6 +18,7 @@ vi.mock("eve/react", async (importOriginal) => ({
|
|
|
12
18
|
useEveAgent: mockUseEveAgent,
|
|
13
19
|
}));
|
|
14
20
|
|
|
21
|
+
import type { MessageStreamEvent } from "eve/client";
|
|
15
22
|
import type { EveMessageData } from "eve/react";
|
|
16
23
|
import { useEveAgentRuntime } from "./useEveAgentRuntime";
|
|
17
24
|
import { eveExtras } from "./eveExtras";
|
|
@@ -388,9 +395,9 @@ describe("useEveAgentRuntime tool approval responses", () => {
|
|
|
388
395
|
try {
|
|
389
396
|
const { result } = renderHook(() => useEveAgentRuntime());
|
|
390
397
|
|
|
391
|
-
expect(
|
|
392
|
-
|
|
393
|
-
);
|
|
398
|
+
await expect(
|
|
399
|
+
respondToTextRequest(result, { approved: true }),
|
|
400
|
+
).rejects.toThrow(/was not answered by this response/);
|
|
394
401
|
|
|
395
402
|
await flushMicrotasks();
|
|
396
403
|
await flushMicrotasks();
|
|
@@ -1205,6 +1212,7 @@ describe("useEveAgentRuntime extras wiring", () => {
|
|
|
1205
1212
|
mockUseEveAgent.mockReturnValue(agent as never);
|
|
1206
1213
|
const { result, rerender } = renderHook(() => useEveAgentRuntime());
|
|
1207
1214
|
|
|
1215
|
+
let humanRequests = 0;
|
|
1208
1216
|
const humanRejections: unknown[] = [];
|
|
1209
1217
|
act(() => {
|
|
1210
1218
|
result.current.registerModelContextProvider({
|
|
@@ -1216,6 +1224,7 @@ describe("useEveAgentRuntime extras wiring", () => {
|
|
|
1216
1224
|
_args: unknown,
|
|
1217
1225
|
context: { human: (payload: unknown) => Promise<unknown> },
|
|
1218
1226
|
) => {
|
|
1227
|
+
humanRequests += 1;
|
|
1219
1228
|
try {
|
|
1220
1229
|
return await context.human({ request: "approve" });
|
|
1221
1230
|
} catch (error) {
|
|
@@ -1235,7 +1244,7 @@ describe("useEveAgentRuntime extras wiring", () => {
|
|
|
1235
1244
|
rerender();
|
|
1236
1245
|
|
|
1237
1246
|
await waitFor(() => {
|
|
1238
|
-
expect(
|
|
1247
|
+
expect(humanRequests).toBe(2);
|
|
1239
1248
|
});
|
|
1240
1249
|
|
|
1241
1250
|
act(() => {
|
|
@@ -1817,6 +1826,131 @@ describe("useEveAgentRuntime cancel binding", () => {
|
|
|
1817
1826
|
describe("useEveAgentRuntime thread refetch", () => {
|
|
1818
1827
|
const session = { sessionId: "s1" };
|
|
1819
1828
|
|
|
1829
|
+
const mockWorkspaceAgents = () => {
|
|
1830
|
+
const resumeA = vi.fn().mockResolvedValue(undefined);
|
|
1831
|
+
const resumeB = vi.fn().mockResolvedValue(undefined);
|
|
1832
|
+
const agentA = createAgent({
|
|
1833
|
+
data: {
|
|
1834
|
+
messages: [
|
|
1835
|
+
{
|
|
1836
|
+
id: "workspace-a",
|
|
1837
|
+
role: "user",
|
|
1838
|
+
parts: [{ type: "text", text: "workspace A" }],
|
|
1839
|
+
},
|
|
1840
|
+
],
|
|
1841
|
+
},
|
|
1842
|
+
session,
|
|
1843
|
+
resume: resumeA,
|
|
1844
|
+
});
|
|
1845
|
+
const agentB = createAgent({
|
|
1846
|
+
data: {
|
|
1847
|
+
messages: [
|
|
1848
|
+
{
|
|
1849
|
+
id: "workspace-b",
|
|
1850
|
+
role: "user",
|
|
1851
|
+
parts: [{ type: "text", text: "workspace B" }],
|
|
1852
|
+
},
|
|
1853
|
+
],
|
|
1854
|
+
},
|
|
1855
|
+
session,
|
|
1856
|
+
resume: resumeB,
|
|
1857
|
+
});
|
|
1858
|
+
mockUseEveAgent.mockImplementation((options) =>
|
|
1859
|
+
(options as { workspace: string }).workspace === "A"
|
|
1860
|
+
? (agentA as never)
|
|
1861
|
+
: (agentB as never),
|
|
1862
|
+
);
|
|
1863
|
+
return { agentA, agentB, resumeA, resumeB };
|
|
1864
|
+
};
|
|
1865
|
+
|
|
1866
|
+
it("publishes committed state before descendant layout effects", async () => {
|
|
1867
|
+
const { resumeA, resumeB } = mockWorkspaceAgents();
|
|
1868
|
+
|
|
1869
|
+
let refetch: Promise<void> | undefined;
|
|
1870
|
+
let currentRuntime: ReturnType<typeof useEveAgentRuntime> | undefined;
|
|
1871
|
+
const RefetchOnLayout = ({
|
|
1872
|
+
runtime,
|
|
1873
|
+
enabled,
|
|
1874
|
+
}: {
|
|
1875
|
+
runtime: ReturnType<typeof useEveAgentRuntime>;
|
|
1876
|
+
enabled: boolean;
|
|
1877
|
+
}) => {
|
|
1878
|
+
useLayoutEffect(() => {
|
|
1879
|
+
if (!enabled) return;
|
|
1880
|
+
runtime.thread.append({
|
|
1881
|
+
role: "user",
|
|
1882
|
+
content: [{ type: "text", text: "draft from B" }],
|
|
1883
|
+
startRun: false,
|
|
1884
|
+
});
|
|
1885
|
+
refetch = runtime.threads.reloadMainThread();
|
|
1886
|
+
}, [enabled, runtime]);
|
|
1887
|
+
return null;
|
|
1888
|
+
};
|
|
1889
|
+
const Probe = ({
|
|
1890
|
+
workspace,
|
|
1891
|
+
refetchOnLayout,
|
|
1892
|
+
}: {
|
|
1893
|
+
workspace: string;
|
|
1894
|
+
refetchOnLayout: boolean;
|
|
1895
|
+
}) => {
|
|
1896
|
+
const runtime = useEveAgentRuntime({ workspace } as never);
|
|
1897
|
+
currentRuntime = runtime;
|
|
1898
|
+
return <RefetchOnLayout runtime={runtime} enabled={refetchOnLayout} />;
|
|
1899
|
+
};
|
|
1900
|
+
|
|
1901
|
+
const view = render(<Probe workspace="A" refetchOnLayout={false} />);
|
|
1902
|
+
view.rerender(<Probe workspace="B" refetchOnLayout />);
|
|
1903
|
+
await act(async () => {
|
|
1904
|
+
await refetch;
|
|
1905
|
+
});
|
|
1906
|
+
|
|
1907
|
+
expect(resumeA).not.toHaveBeenCalled();
|
|
1908
|
+
expect(resumeB).toHaveBeenCalledTimes(1);
|
|
1909
|
+
expect(getText(currentRuntime!)).toEqual(["workspace B", "draft from B"]);
|
|
1910
|
+
view.unmount();
|
|
1911
|
+
});
|
|
1912
|
+
|
|
1913
|
+
it("keeps refetches scoped to the committed agent", async () => {
|
|
1914
|
+
const { agentB, resumeA, resumeB } = mockWorkspaceAgents();
|
|
1915
|
+
|
|
1916
|
+
const pending = new Promise<never>(() => {});
|
|
1917
|
+
let blocked = false;
|
|
1918
|
+
const Blocker = () => {
|
|
1919
|
+
if (blocked) throw pending;
|
|
1920
|
+
return null;
|
|
1921
|
+
};
|
|
1922
|
+
const Wrapper = ({ children }: PropsWithChildren) => (
|
|
1923
|
+
<Suspense fallback={null}>
|
|
1924
|
+
{children}
|
|
1925
|
+
<Blocker />
|
|
1926
|
+
</Suspense>
|
|
1927
|
+
);
|
|
1928
|
+
|
|
1929
|
+
const { result, rerender } = renderHook(
|
|
1930
|
+
({ workspace }) => useEveAgentRuntime({ workspace } as never),
|
|
1931
|
+
{
|
|
1932
|
+
initialProps: { workspace: "A" },
|
|
1933
|
+
wrapper: Wrapper,
|
|
1934
|
+
},
|
|
1935
|
+
);
|
|
1936
|
+
|
|
1937
|
+
act(() => {
|
|
1938
|
+
blocked = true;
|
|
1939
|
+
startTransition(() => rerender({ workspace: "B" }));
|
|
1940
|
+
});
|
|
1941
|
+
expect(mockUseEveAgent.mock.results.at(-1)?.value).toBe(agentB);
|
|
1942
|
+
|
|
1943
|
+
await act(async () => {
|
|
1944
|
+
await result.current.threads.reloadMainThread();
|
|
1945
|
+
});
|
|
1946
|
+
|
|
1947
|
+
expect(resumeB).not.toHaveBeenCalled();
|
|
1948
|
+
expect(resumeA).toHaveBeenCalledTimes(1);
|
|
1949
|
+
|
|
1950
|
+
await stageMessage(result.current, "draft from A");
|
|
1951
|
+
expect(getText(result.current)).toEqual(["workspace A", "draft from A"]);
|
|
1952
|
+
});
|
|
1953
|
+
|
|
1820
1954
|
it("replays the session through eve resume when the thread is refetched", async () => {
|
|
1821
1955
|
const resume = vi.fn().mockResolvedValue(undefined);
|
|
1822
1956
|
const agent = createAgent({ session, resume });
|
|
@@ -2019,3 +2153,88 @@ describe("useEveAgentRuntime thread refetch", () => {
|
|
|
2019
2153
|
});
|
|
2020
2154
|
});
|
|
2021
2155
|
});
|
|
2156
|
+
|
|
2157
|
+
describe("useEveAgentRuntime createdAt derivation", () => {
|
|
2158
|
+
const TURN = "turn_ts";
|
|
2159
|
+
const USER_AT = "2026-01-02T10:00:01.000Z";
|
|
2160
|
+
const ASSISTANT_AT = "2026-01-02T10:02:00.000Z";
|
|
2161
|
+
|
|
2162
|
+
const turnEvents = [
|
|
2163
|
+
{
|
|
2164
|
+
type: "turn.started",
|
|
2165
|
+
data: { sequence: 1, turnId: TURN },
|
|
2166
|
+
meta: { at: "2026-01-02T10:00:00.000Z", id: "evt_1" },
|
|
2167
|
+
},
|
|
2168
|
+
{
|
|
2169
|
+
type: "message.received",
|
|
2170
|
+
data: { message: "hi", sequence: 2, turnId: TURN },
|
|
2171
|
+
meta: { at: USER_AT, id: "evt_2" },
|
|
2172
|
+
},
|
|
2173
|
+
{
|
|
2174
|
+
type: "step.started",
|
|
2175
|
+
data: { modelId: "m", sequence: 3, stepIndex: 0, turnId: TURN },
|
|
2176
|
+
meta: { at: ASSISTANT_AT, id: "evt_3" },
|
|
2177
|
+
},
|
|
2178
|
+
] satisfies readonly MessageStreamEvent[];
|
|
2179
|
+
|
|
2180
|
+
const turnData: EveMessageData = {
|
|
2181
|
+
messages: [
|
|
2182
|
+
{
|
|
2183
|
+
id: `${TURN}:user`,
|
|
2184
|
+
role: "user",
|
|
2185
|
+
metadata: { turnId: TURN },
|
|
2186
|
+
parts: [{ type: "text", text: "hi" }],
|
|
2187
|
+
},
|
|
2188
|
+
{
|
|
2189
|
+
id: `${TURN}:assistant`,
|
|
2190
|
+
role: "assistant",
|
|
2191
|
+
metadata: { turnId: TURN },
|
|
2192
|
+
parts: [{ type: "text", text: "hello" }],
|
|
2193
|
+
},
|
|
2194
|
+
],
|
|
2195
|
+
};
|
|
2196
|
+
|
|
2197
|
+
it("stamps a message with the wall clock when no event covers its turn, and keeps that stamp across renders", () => {
|
|
2198
|
+
const before = Date.now();
|
|
2199
|
+
const agent = createAgent({ data: turnData, events: [] });
|
|
2200
|
+
mockUseEveAgent.mockReturnValue(agent as never);
|
|
2201
|
+
|
|
2202
|
+
const { result, rerender } = renderHook(() => useEveAgentRuntime());
|
|
2203
|
+
|
|
2204
|
+
const stamped = result.current.thread.getState().messages[0]?.createdAt;
|
|
2205
|
+
expect(stamped?.getTime()).toBeGreaterThanOrEqual(before);
|
|
2206
|
+
|
|
2207
|
+
mockUseEveAgent.mockReturnValue({
|
|
2208
|
+
...agent,
|
|
2209
|
+
data: { messages: [...turnData.messages] },
|
|
2210
|
+
} as never);
|
|
2211
|
+
rerender();
|
|
2212
|
+
|
|
2213
|
+
expect(result.current.thread.getState().messages[0]?.createdAt).toBe(
|
|
2214
|
+
stamped,
|
|
2215
|
+
);
|
|
2216
|
+
});
|
|
2217
|
+
|
|
2218
|
+
it("keeps the message list identity when appended events stamp no message", () => {
|
|
2219
|
+
const agent = createAgent({ data: turnData, events: turnEvents });
|
|
2220
|
+
mockUseEveAgent.mockReturnValue(agent as never);
|
|
2221
|
+
|
|
2222
|
+
const { result, rerender } = renderHook(() => useEveAgentRuntime());
|
|
2223
|
+
const first = result.current.thread.getState().messages;
|
|
2224
|
+
|
|
2225
|
+
mockUseEveAgent.mockReturnValue({
|
|
2226
|
+
...agent,
|
|
2227
|
+
events: [
|
|
2228
|
+
...turnEvents,
|
|
2229
|
+
{
|
|
2230
|
+
type: "turn.started",
|
|
2231
|
+
data: { sequence: 4, turnId: "turn_next" },
|
|
2232
|
+
meta: { at: "2026-01-02T10:05:00.000Z", id: "evt_4" },
|
|
2233
|
+
},
|
|
2234
|
+
],
|
|
2235
|
+
} as never);
|
|
2236
|
+
rerender();
|
|
2237
|
+
|
|
2238
|
+
expect(result.current.thread.getState().messages).toBe(first);
|
|
2239
|
+
});
|
|
2240
|
+
});
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
useCallback,
|
|
5
|
+
useEffect,
|
|
6
|
+
useInsertionEffect,
|
|
7
|
+
useMemo,
|
|
8
|
+
useRef,
|
|
9
|
+
useState,
|
|
10
|
+
} from "react";
|
|
4
11
|
import {
|
|
5
12
|
fromThreadMessageLike,
|
|
6
13
|
generateId,
|
|
@@ -32,6 +39,10 @@ import {
|
|
|
32
39
|
getEveMessageContent,
|
|
33
40
|
toEveInputResponse,
|
|
34
41
|
} from "./convertEveMessages";
|
|
42
|
+
import {
|
|
43
|
+
collectTurnTimestamps,
|
|
44
|
+
createTurnTimestampCache,
|
|
45
|
+
} from "./deriveCreatedAt";
|
|
35
46
|
import { eveExtras } from "./eveExtras";
|
|
36
47
|
|
|
37
48
|
const USER_STAGED_STATUS = {
|
|
@@ -204,6 +215,14 @@ export const useEveAgentRuntime = (options: UseEveAgentRuntimeOptions = {}) => {
|
|
|
204
215
|
agent.status === "submitted" || agent.status === "streaming";
|
|
205
216
|
const isRunning = providerIsRunning || hasExecutingTools;
|
|
206
217
|
|
|
218
|
+
// Kept apart from the message memo so events that teach no timestamp keep
|
|
219
|
+
// the map identity and skip rebuilding every ThreadMessage.
|
|
220
|
+
const turnTimestampCacheRef = useRef(createTurnTimestampCache());
|
|
221
|
+
const turnTimestamps = useMemo(
|
|
222
|
+
() => collectTurnTimestamps(agent.events, turnTimestampCacheRef.current),
|
|
223
|
+
[agent.events],
|
|
224
|
+
);
|
|
225
|
+
|
|
207
226
|
const convertedMessages = useMemo(() => {
|
|
208
227
|
const createdAtByMessageId = createdAtByMessageIdRef.current;
|
|
209
228
|
const messageIds = new Set(
|
|
@@ -217,6 +236,13 @@ export const useEveAgentRuntime = (options: UseEveAgentRuntimeOptions = {}) => {
|
|
|
217
236
|
isRunning,
|
|
218
237
|
error: agent.error,
|
|
219
238
|
getCreatedAt: (message) => {
|
|
239
|
+
const turnId = message.metadata?.turnId;
|
|
240
|
+
const durable =
|
|
241
|
+
turnId === undefined
|
|
242
|
+
? undefined
|
|
243
|
+
: turnTimestamps.get(turnId)?.[message.role];
|
|
244
|
+
if (durable !== undefined) return durable;
|
|
245
|
+
|
|
220
246
|
const existing = createdAtByMessageId.get(message.id);
|
|
221
247
|
if (existing) return existing;
|
|
222
248
|
|
|
@@ -225,13 +251,17 @@ export const useEveAgentRuntime = (options: UseEveAgentRuntimeOptions = {}) => {
|
|
|
225
251
|
return createdAt;
|
|
226
252
|
},
|
|
227
253
|
});
|
|
228
|
-
}, [agent.data, agent.error, isRunning]);
|
|
254
|
+
}, [agent.data, agent.error, isRunning, turnTimestamps]);
|
|
229
255
|
|
|
230
256
|
const messages = stagedMessages ?? convertedMessages;
|
|
231
257
|
const messagesRef = useRef(messages);
|
|
232
|
-
messagesRef.current = messages;
|
|
233
258
|
const agentRef = useRef(agent);
|
|
234
|
-
|
|
259
|
+
// Descendant layout effects can dispatch against these refs synchronously;
|
|
260
|
+
// this matches useA2ARuntime's commit-scoped ref publication.
|
|
261
|
+
useInsertionEffect(() => {
|
|
262
|
+
messagesRef.current = messages;
|
|
263
|
+
agentRef.current = agent;
|
|
264
|
+
}, [agent, messages]);
|
|
235
265
|
|
|
236
266
|
// Upstream `EveAgentStore` `send` and `respond` reject while a turn is in
|
|
237
267
|
// flight and only resolve once the turn's stream parks, so a pending chain
|
|
@@ -464,10 +494,11 @@ export const useEveAgentRuntime = (options: UseEveAgentRuntimeOptions = {}) => {
|
|
|
464
494
|
}
|
|
465
495
|
: {}),
|
|
466
496
|
onRespondToToolApproval: (response) => {
|
|
467
|
-
// Eve
|
|
468
|
-
//
|
|
469
|
-
//
|
|
470
|
-
//
|
|
497
|
+
// Eve resolves a request the moment any response for it arrives, and an
|
|
498
|
+
// empty one is recorded as an answer with no content. Mapping before the
|
|
499
|
+
// send is enqueued keeps an unmappable response unsent, so the request
|
|
500
|
+
// stays answerable; the mapper's error reaches the caller as the seam's
|
|
501
|
+
// rejection.
|
|
471
502
|
const inputResponse = toEveInputResponse(
|
|
472
503
|
response,
|
|
473
504
|
findEveInputRequest(agent.data, response.approvalId),
|