@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.
- package/dist/convertEveMessages.d.ts +45 -5
- package/dist/convertEveMessages.d.ts.map +1 -1
- package/dist/convertEveMessages.js +91 -14
- 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 +61 -26
- package/dist/useEveAgentRuntime.js.map +1 -1
- package/package.json +7 -6
- package/src/convertEveMessages.test.ts +698 -6
- package/src/convertEveMessages.ts +185 -14
- 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.refetch.test.tsx +89 -0
- package/src/useEveAgentRuntime.test.tsx +1037 -167
- package/src/useEveAgentRuntime.ts +148 -42
|
@@ -1,6 +1,6 @@
|
|
|
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,
|
|
@@ -26,12 +26,13 @@ import {
|
|
|
26
26
|
type UseEveAgentOptions,
|
|
27
27
|
type UseEveAgentStatus,
|
|
28
28
|
} from "eve/react";
|
|
29
|
-
import type { SendTurnPayload } from "eve/client";
|
|
30
29
|
import {
|
|
31
30
|
convertEveMessages,
|
|
31
|
+
findEveInputRequest,
|
|
32
32
|
getEveMessageContent,
|
|
33
33
|
toEveInputResponse,
|
|
34
34
|
} from "./convertEveMessages";
|
|
35
|
+
import { eveExtras } from "./eveExtras";
|
|
35
36
|
|
|
36
37
|
const USER_STAGED_STATUS = {
|
|
37
38
|
type: "complete",
|
|
@@ -83,22 +84,35 @@ const hasRunConfig = (
|
|
|
83
84
|
): runConfig is NonNullable<AppendMessage["runConfig"]> =>
|
|
84
85
|
runConfig?.custom !== undefined && Object.keys(runConfig.custom).length > 0;
|
|
85
86
|
|
|
87
|
+
type EveJsonValue =
|
|
88
|
+
| string
|
|
89
|
+
| number
|
|
90
|
+
| boolean
|
|
91
|
+
| null
|
|
92
|
+
| readonly EveJsonValue[]
|
|
93
|
+
| { readonly [key: string]: EveJsonValue };
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Structural equivalent of eve's `JsonObject`. Eve demoted its public send
|
|
97
|
+
* payload type once already (`SendTurnPayload`, public through 0.30, internal
|
|
98
|
+
* from 0.31), so the helpers declare the shapes they forward instead of
|
|
99
|
+
* re-exporting eve's option types, and assignability is checked at the send
|
|
100
|
+
* call sites against the installed version.
|
|
101
|
+
*/
|
|
102
|
+
type EveClientContext = { readonly [key: string]: EveJsonValue };
|
|
103
|
+
|
|
86
104
|
/**
|
|
87
105
|
* Only the `custom` bag crosses the wire. Eve reads `clientContext` as its own
|
|
88
106
|
* namespace and serializes it into a model-visible context message, so sending
|
|
89
107
|
* the assistant-ui envelope would surface a literal `"custom"` key in the
|
|
90
108
|
* prompt and to every eve-side handler.
|
|
91
109
|
*/
|
|
92
|
-
const
|
|
110
|
+
const toEveSendOptions = (
|
|
93
111
|
runConfig: AppendMessage["runConfig"],
|
|
94
|
-
):
|
|
112
|
+
): { readonly clientContext: EveClientContext } | undefined =>
|
|
95
113
|
hasRunConfig(runConfig)
|
|
96
|
-
? {
|
|
97
|
-
|
|
98
|
-
SendTurnPayload["clientContext"]
|
|
99
|
-
>,
|
|
100
|
-
}
|
|
101
|
-
: {};
|
|
114
|
+
? { clientContext: runConfig.custom as EveClientContext }
|
|
115
|
+
: undefined;
|
|
102
116
|
|
|
103
117
|
export type UseEveAgentRuntimeOptions = Omit<
|
|
104
118
|
UseEveAgentOptions<EveMessageData>,
|
|
@@ -186,10 +200,9 @@ export const useEveAgentRuntime = (options: UseEveAgentRuntimeOptions = {}) => {
|
|
|
186
200
|
const hasExecutingTools = Object.values(toolStatuses).some(
|
|
187
201
|
(status) => status?.type === "executing",
|
|
188
202
|
);
|
|
189
|
-
const
|
|
190
|
-
agent.status === "submitted" ||
|
|
191
|
-
|
|
192
|
-
hasExecutingTools;
|
|
203
|
+
const providerIsRunning =
|
|
204
|
+
agent.status === "submitted" || agent.status === "streaming";
|
|
205
|
+
const isRunning = providerIsRunning || hasExecutingTools;
|
|
193
206
|
|
|
194
207
|
const convertedMessages = useMemo(() => {
|
|
195
208
|
const createdAtByMessageId = createdAtByMessageIdRef.current;
|
|
@@ -217,21 +230,34 @@ export const useEveAgentRuntime = (options: UseEveAgentRuntimeOptions = {}) => {
|
|
|
217
230
|
const messages = stagedMessages ?? convertedMessages;
|
|
218
231
|
const messagesRef = useRef(messages);
|
|
219
232
|
messagesRef.current = messages;
|
|
233
|
+
const agentRef = useRef(agent);
|
|
234
|
+
agentRef.current = agent;
|
|
220
235
|
|
|
221
|
-
// Upstream `EveAgentStore
|
|
222
|
-
//
|
|
223
|
-
// an active turn; chaining every
|
|
224
|
-
// status.
|
|
236
|
+
// Upstream `EveAgentStore` `send` and `respond` reject while a turn is in
|
|
237
|
+
// flight and only resolve once the turn's stream parks, so a pending chain
|
|
238
|
+
// link is exactly an active turn; chaining every dispatch serializes them
|
|
239
|
+
// without watching status.
|
|
225
240
|
const sendChainRef = useRef<Promise<void>>(Promise.resolve());
|
|
226
241
|
const sendEpochRef = useRef(0);
|
|
242
|
+
// A cancel drops the queued send but keeps its draft for a later promotion;
|
|
243
|
+
// only a reset discards the draft with the session, so the two need separate
|
|
244
|
+
// counters. A queued read has no draft and outlives a cancel, so it waits on
|
|
245
|
+
// the reset counter instead.
|
|
246
|
+
const resetEpochRef = useRef(0);
|
|
227
247
|
const isMountedRef = useRef(true);
|
|
248
|
+
const runtimeRef = useRef<ReturnType<typeof useExternalStoreRuntime> | null>(
|
|
249
|
+
null,
|
|
250
|
+
);
|
|
228
251
|
|
|
229
|
-
const enqueueSend = (
|
|
230
|
-
|
|
252
|
+
const enqueueSend = (
|
|
253
|
+
dispatch: () => Promise<void>,
|
|
254
|
+
epochRef: { current: number } = sendEpochRef,
|
|
255
|
+
) => {
|
|
256
|
+
const epoch = epochRef.current;
|
|
231
257
|
const next = sendChainRef.current.then(() => {
|
|
232
|
-
if (
|
|
233
|
-
|
|
234
|
-
return
|
|
258
|
+
if (!isMountedRef.current) throw sendAbandonedError;
|
|
259
|
+
if (epoch !== epochRef.current) throw sendCancelledError;
|
|
260
|
+
return dispatch();
|
|
235
261
|
});
|
|
236
262
|
sendChainRef.current = next.catch(() => {});
|
|
237
263
|
return next;
|
|
@@ -290,10 +316,37 @@ export const useEveAgentRuntime = (options: UseEveAgentRuntimeOptions = {}) => {
|
|
|
290
316
|
setStagedMessages(nextMessages);
|
|
291
317
|
};
|
|
292
318
|
|
|
293
|
-
|
|
319
|
+
const reset = useCallback(() => {
|
|
320
|
+
runtimeRef.current?.thread.unstable_notifySessionReset();
|
|
321
|
+
// Sends parked behind an active turn captured the pre-reset epoch, so the
|
|
322
|
+
// epoch has to advance before the session is torn down or they dispatch
|
|
323
|
+
// into the new one; `lastFinishStatusRef` is run-scoped for the same
|
|
324
|
+
// reason.
|
|
325
|
+
sendEpochRef.current += 1;
|
|
326
|
+
resetEpochRef.current += 1;
|
|
327
|
+
lastFinishStatusRef.current = null;
|
|
328
|
+
setStagedMessages(null);
|
|
329
|
+
stagedInputsRef.current.clear();
|
|
330
|
+
setToolStatuses({});
|
|
331
|
+
agent.reset();
|
|
332
|
+
}, [agent]);
|
|
333
|
+
|
|
334
|
+
const extras = useMemo(
|
|
335
|
+
() =>
|
|
336
|
+
eveExtras.provide({
|
|
337
|
+
error: agent.error,
|
|
338
|
+
events: agent.events,
|
|
339
|
+
session: agent.session,
|
|
340
|
+
reset,
|
|
341
|
+
}),
|
|
342
|
+
[agent.error, agent.events, agent.session, reset],
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
const runtime = useExternalStoreRuntime({
|
|
294
346
|
...pickExternalStoreSharedOptions(options),
|
|
295
347
|
messages,
|
|
296
|
-
isRunning,
|
|
348
|
+
isRunning: providerIsRunning,
|
|
349
|
+
extras,
|
|
297
350
|
unstable_enableToolInvocations: true,
|
|
298
351
|
setToolStatuses,
|
|
299
352
|
adapters: {
|
|
@@ -309,10 +362,12 @@ export const useEveAgentRuntime = (options: UseEveAgentRuntimeOptions = {}) => {
|
|
|
309
362
|
return;
|
|
310
363
|
}
|
|
311
364
|
try {
|
|
312
|
-
await enqueueSend(
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
365
|
+
await enqueueSend(() =>
|
|
366
|
+
agent.send(
|
|
367
|
+
getEveMessageContent(message),
|
|
368
|
+
toEveSendOptions(message.runConfig),
|
|
369
|
+
),
|
|
370
|
+
);
|
|
316
371
|
} catch (error) {
|
|
317
372
|
// A cancelled send never reached the session, so it rethrows for the
|
|
318
373
|
// composer to take the draft back; an unmounted one has no composer
|
|
@@ -328,6 +383,7 @@ export const useEveAgentRuntime = (options: UseEveAgentRuntimeOptions = {}) => {
|
|
|
328
383
|
if (!stagedRun)
|
|
329
384
|
throw new Error("Runtime does not support reloading messages.");
|
|
330
385
|
const epoch = sendEpochRef.current;
|
|
386
|
+
const resetEpoch = resetEpochRef.current;
|
|
331
387
|
for (const { message: stagedMessage, input } of stagedRun) {
|
|
332
388
|
if (epoch !== sendEpochRef.current) return;
|
|
333
389
|
const previousMessages = messagesRef.current;
|
|
@@ -347,11 +403,17 @@ export const useEveAgentRuntime = (options: UseEveAgentRuntimeOptions = {}) => {
|
|
|
347
403
|
? config.runConfig
|
|
348
404
|
: input.runConfig;
|
|
349
405
|
try {
|
|
350
|
-
await enqueueSend(
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
406
|
+
await enqueueSend(() =>
|
|
407
|
+
agent.send(
|
|
408
|
+
getEveMessageContent(input.message),
|
|
409
|
+
toEveSendOptions(runConfig),
|
|
410
|
+
),
|
|
411
|
+
);
|
|
354
412
|
} catch (error) {
|
|
413
|
+
// A reset discarded the draft along with the session, so
|
|
414
|
+
// restoring it here would refloat a message the thread no
|
|
415
|
+
// longer has. A cancel still restores.
|
|
416
|
+
if (resetEpoch !== resetEpochRef.current) return;
|
|
355
417
|
stagedInputsRef.current.set(stagedMessage.id, input);
|
|
356
418
|
messagesRef.current = previousMessages;
|
|
357
419
|
setStagedMessages(previousMessages);
|
|
@@ -363,17 +425,61 @@ export const useEveAgentRuntime = (options: UseEveAgentRuntimeOptions = {}) => {
|
|
|
363
425
|
},
|
|
364
426
|
}
|
|
365
427
|
: {}),
|
|
366
|
-
onCancel: () => {
|
|
428
|
+
onCancel: async () => {
|
|
367
429
|
sendEpochRef.current += 1;
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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();
|
|
376
440
|
}
|
|
377
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
|
+
},
|
|
378
481
|
});
|
|
482
|
+
runtimeRef.current = runtime;
|
|
483
|
+
|
|
484
|
+
return runtime;
|
|
379
485
|
};
|