@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
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
4
|
import {
|
|
5
5
|
fromThreadMessageLike,
|
|
6
6
|
generateId,
|
|
7
|
+
MessageNotSentError,
|
|
7
8
|
pickExternalStoreSharedOptions,
|
|
8
9
|
type AppendMessage,
|
|
9
10
|
type AttachmentAdapter,
|
|
@@ -25,22 +26,29 @@ import {
|
|
|
25
26
|
type UseEveAgentOptions,
|
|
26
27
|
type UseEveAgentStatus,
|
|
27
28
|
} from "eve/react";
|
|
28
|
-
import type { SendTurnPayload } from "eve/client";
|
|
29
29
|
import {
|
|
30
30
|
convertEveMessages,
|
|
31
31
|
getEveMessageContent,
|
|
32
32
|
toEveInputResponse,
|
|
33
33
|
} from "./convertEveMessages";
|
|
34
|
+
import { eveExtras } from "./eveExtras";
|
|
34
35
|
|
|
35
36
|
const USER_STAGED_STATUS = {
|
|
36
37
|
type: "complete",
|
|
37
38
|
reason: "unknown",
|
|
38
39
|
} as const;
|
|
39
40
|
|
|
40
|
-
const sendCancelledError = new
|
|
41
|
+
const sendCancelledError = new MessageNotSentError(
|
|
41
42
|
"eve send was dropped because the run was cancelled.",
|
|
42
43
|
);
|
|
43
44
|
|
|
45
|
+
const sendAbandonedError = new Error(
|
|
46
|
+
"eve send was dropped because the runtime unmounted.",
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const isDroppedSend = (error: unknown) =>
|
|
50
|
+
error === sendCancelledError || error === sendAbandonedError;
|
|
51
|
+
|
|
44
52
|
type EveLifecycleCallbackName =
|
|
45
53
|
| "onError"
|
|
46
54
|
| "onEvent"
|
|
@@ -75,22 +83,35 @@ const hasRunConfig = (
|
|
|
75
83
|
): runConfig is NonNullable<AppendMessage["runConfig"]> =>
|
|
76
84
|
runConfig?.custom !== undefined && Object.keys(runConfig.custom).length > 0;
|
|
77
85
|
|
|
86
|
+
type EveJsonValue =
|
|
87
|
+
| string
|
|
88
|
+
| number
|
|
89
|
+
| boolean
|
|
90
|
+
| null
|
|
91
|
+
| readonly EveJsonValue[]
|
|
92
|
+
| { readonly [key: string]: EveJsonValue };
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Structural equivalent of eve's `JsonObject`. Eve demoted its public send
|
|
96
|
+
* payload type once already (`SendTurnPayload`, public through 0.30, internal
|
|
97
|
+
* from 0.31), so the helpers declare the shapes they forward instead of
|
|
98
|
+
* re-exporting eve's option types, and assignability is checked at the send
|
|
99
|
+
* call sites against the installed version.
|
|
100
|
+
*/
|
|
101
|
+
type EveClientContext = { readonly [key: string]: EveJsonValue };
|
|
102
|
+
|
|
78
103
|
/**
|
|
79
104
|
* Only the `custom` bag crosses the wire. Eve reads `clientContext` as its own
|
|
80
105
|
* namespace and serializes it into a model-visible context message, so sending
|
|
81
106
|
* the assistant-ui envelope would surface a literal `"custom"` key in the
|
|
82
107
|
* prompt and to every eve-side handler.
|
|
83
108
|
*/
|
|
84
|
-
const
|
|
109
|
+
const toEveSendOptions = (
|
|
85
110
|
runConfig: AppendMessage["runConfig"],
|
|
86
|
-
):
|
|
111
|
+
): { readonly clientContext: EveClientContext } | undefined =>
|
|
87
112
|
hasRunConfig(runConfig)
|
|
88
|
-
? {
|
|
89
|
-
|
|
90
|
-
SendTurnPayload["clientContext"]
|
|
91
|
-
>,
|
|
92
|
-
}
|
|
93
|
-
: {};
|
|
113
|
+
? { clientContext: runConfig.custom as EveClientContext }
|
|
114
|
+
: undefined;
|
|
94
115
|
|
|
95
116
|
export type UseEveAgentRuntimeOptions = Omit<
|
|
96
117
|
UseEveAgentOptions<EveMessageData>,
|
|
@@ -210,27 +231,40 @@ export const useEveAgentRuntime = (options: UseEveAgentRuntimeOptions = {}) => {
|
|
|
210
231
|
const messagesRef = useRef(messages);
|
|
211
232
|
messagesRef.current = messages;
|
|
212
233
|
|
|
213
|
-
// Upstream `EveAgentStore
|
|
214
|
-
//
|
|
215
|
-
// an active turn; chaining every
|
|
216
|
-
// status.
|
|
234
|
+
// Upstream `EveAgentStore` `send` and `respond` reject while a turn is in
|
|
235
|
+
// flight and only resolve once the turn's stream parks, so a pending chain
|
|
236
|
+
// link is exactly an active turn; chaining every dispatch serializes them
|
|
237
|
+
// without watching status.
|
|
217
238
|
const sendChainRef = useRef<Promise<void>>(Promise.resolve());
|
|
218
239
|
const sendEpochRef = useRef(0);
|
|
240
|
+
// A cancel drops the queued send but keeps its draft for a later promotion;
|
|
241
|
+
// only a reset discards the draft with the session, so the two need separate
|
|
242
|
+
// counters.
|
|
243
|
+
const resetEpochRef = useRef(0);
|
|
244
|
+
const isMountedRef = useRef(true);
|
|
245
|
+
const runtimeRef = useRef<ReturnType<typeof useExternalStoreRuntime> | null>(
|
|
246
|
+
null,
|
|
247
|
+
);
|
|
219
248
|
|
|
220
|
-
const enqueueSend = (
|
|
249
|
+
const enqueueSend = (dispatch: () => Promise<void>) => {
|
|
221
250
|
const epoch = sendEpochRef.current;
|
|
222
251
|
const next = sendChainRef.current.then(() => {
|
|
223
|
-
if (epoch !== sendEpochRef.current)
|
|
224
|
-
|
|
252
|
+
if (epoch !== sendEpochRef.current)
|
|
253
|
+
throw isMountedRef.current ? sendCancelledError : sendAbandonedError;
|
|
254
|
+
return dispatch();
|
|
225
255
|
});
|
|
226
256
|
sendChainRef.current = next.catch(() => {});
|
|
227
257
|
return next;
|
|
228
258
|
};
|
|
229
259
|
|
|
230
260
|
// The store outlives the component (useEveAgent holds it in a ref with no
|
|
231
|
-
// cleanup), so queued sends must not fire server turns after unmount.
|
|
261
|
+
// cleanup), so queued sends must not fire server turns after unmount. The
|
|
262
|
+
// flag separates that teardown from a user cancel, and is re-armed in setup
|
|
263
|
+
// for a remounted tree.
|
|
232
264
|
useEffect(() => {
|
|
265
|
+
isMountedRef.current = true;
|
|
233
266
|
return () => {
|
|
267
|
+
isMountedRef.current = false;
|
|
234
268
|
sendEpochRef.current += 1;
|
|
235
269
|
};
|
|
236
270
|
}, []);
|
|
@@ -276,10 +310,37 @@ export const useEveAgentRuntime = (options: UseEveAgentRuntimeOptions = {}) => {
|
|
|
276
310
|
setStagedMessages(nextMessages);
|
|
277
311
|
};
|
|
278
312
|
|
|
279
|
-
|
|
313
|
+
const reset = useCallback(() => {
|
|
314
|
+
runtimeRef.current?.thread.unstable_notifySessionReset();
|
|
315
|
+
// Sends parked behind an active turn captured the pre-reset epoch, so the
|
|
316
|
+
// epoch has to advance before the session is torn down or they dispatch
|
|
317
|
+
// into the new one; `lastFinishStatusRef` is run-scoped for the same
|
|
318
|
+
// reason.
|
|
319
|
+
sendEpochRef.current += 1;
|
|
320
|
+
resetEpochRef.current += 1;
|
|
321
|
+
lastFinishStatusRef.current = null;
|
|
322
|
+
setStagedMessages(null);
|
|
323
|
+
stagedInputsRef.current.clear();
|
|
324
|
+
setToolStatuses({});
|
|
325
|
+
agent.reset();
|
|
326
|
+
}, [agent]);
|
|
327
|
+
|
|
328
|
+
const extras = useMemo(
|
|
329
|
+
() =>
|
|
330
|
+
eveExtras.provide({
|
|
331
|
+
error: agent.error,
|
|
332
|
+
events: agent.events,
|
|
333
|
+
session: agent.session,
|
|
334
|
+
reset,
|
|
335
|
+
}),
|
|
336
|
+
[agent.error, agent.events, agent.session, reset],
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
const runtime = useExternalStoreRuntime({
|
|
280
340
|
...pickExternalStoreSharedOptions(options),
|
|
281
341
|
messages,
|
|
282
342
|
isRunning,
|
|
343
|
+
extras,
|
|
283
344
|
unstable_enableToolInvocations: true,
|
|
284
345
|
setToolStatuses,
|
|
285
346
|
adapters: {
|
|
@@ -295,12 +356,18 @@ export const useEveAgentRuntime = (options: UseEveAgentRuntimeOptions = {}) => {
|
|
|
295
356
|
return;
|
|
296
357
|
}
|
|
297
358
|
try {
|
|
298
|
-
await enqueueSend(
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
359
|
+
await enqueueSend(() =>
|
|
360
|
+
agent.send(
|
|
361
|
+
getEveMessageContent(message),
|
|
362
|
+
toEveSendOptions(message.runConfig),
|
|
363
|
+
),
|
|
364
|
+
);
|
|
302
365
|
} catch (error) {
|
|
303
|
-
|
|
366
|
+
// A cancelled send never reached the session, so it rethrows for the
|
|
367
|
+
// composer to take the draft back; an unmounted one has no composer
|
|
368
|
+
// left to restore.
|
|
369
|
+
if (error === sendAbandonedError) return;
|
|
370
|
+
throw error;
|
|
304
371
|
}
|
|
305
372
|
},
|
|
306
373
|
...(stagedMessages
|
|
@@ -310,6 +377,7 @@ export const useEveAgentRuntime = (options: UseEveAgentRuntimeOptions = {}) => {
|
|
|
310
377
|
if (!stagedRun)
|
|
311
378
|
throw new Error("Runtime does not support reloading messages.");
|
|
312
379
|
const epoch = sendEpochRef.current;
|
|
380
|
+
const resetEpoch = resetEpochRef.current;
|
|
313
381
|
for (const { message: stagedMessage, input } of stagedRun) {
|
|
314
382
|
if (epoch !== sendEpochRef.current) return;
|
|
315
383
|
const previousMessages = messagesRef.current;
|
|
@@ -329,15 +397,21 @@ export const useEveAgentRuntime = (options: UseEveAgentRuntimeOptions = {}) => {
|
|
|
329
397
|
? config.runConfig
|
|
330
398
|
: input.runConfig;
|
|
331
399
|
try {
|
|
332
|
-
await enqueueSend(
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
400
|
+
await enqueueSend(() =>
|
|
401
|
+
agent.send(
|
|
402
|
+
getEveMessageContent(input.message),
|
|
403
|
+
toEveSendOptions(runConfig),
|
|
404
|
+
),
|
|
405
|
+
);
|
|
336
406
|
} catch (error) {
|
|
407
|
+
// A reset discarded the draft along with the session, so
|
|
408
|
+
// restoring it here would refloat a message the thread no
|
|
409
|
+
// longer has. A cancel still restores.
|
|
410
|
+
if (resetEpoch !== resetEpochRef.current) return;
|
|
337
411
|
stagedInputsRef.current.set(stagedMessage.id, input);
|
|
338
412
|
messagesRef.current = previousMessages;
|
|
339
413
|
setStagedMessages(previousMessages);
|
|
340
|
-
if (error
|
|
414
|
+
if (isDroppedSend(error)) return;
|
|
341
415
|
throw error;
|
|
342
416
|
}
|
|
343
417
|
if (lastFinishStatusRef.current === "error") return;
|
|
@@ -352,10 +426,13 @@ export const useEveAgentRuntime = (options: UseEveAgentRuntimeOptions = {}) => {
|
|
|
352
426
|
},
|
|
353
427
|
onRespondToToolApproval: async (response) => {
|
|
354
428
|
try {
|
|
355
|
-
await enqueueSend(
|
|
429
|
+
await enqueueSend(() => agent.respond([toEveInputResponse(response)]));
|
|
356
430
|
} catch (error) {
|
|
357
|
-
if (error
|
|
431
|
+
if (!isDroppedSend(error)) throw error;
|
|
358
432
|
}
|
|
359
433
|
},
|
|
360
434
|
});
|
|
435
|
+
runtimeRef.current = runtime;
|
|
436
|
+
|
|
437
|
+
return runtime;
|
|
361
438
|
};
|