@assistant-ui/eve 0.0.23 → 0.0.25
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 +40 -51
- package/dist/convertEveMessages.d.ts.map +1 -1
- package/dist/convertEveMessages.js +151 -122
- package/dist/convertEveMessages.js.map +1 -1
- package/dist/deriveCreatedAt.d.ts +9 -9
- package/dist/deriveCreatedAt.d.ts.map +1 -1
- package/dist/deriveCreatedAt.js +25 -34
- package/dist/deriveCreatedAt.js.map +1 -1
- package/dist/eveCloudSessions.js +1 -2
- package/dist/eveExtras.d.ts +4 -2
- package/dist/eveExtras.d.ts.map +1 -1
- package/dist/eveExtras.js +1 -2
- package/dist/eveExtras.js.map +1 -1
- package/dist/hooks.d.ts +4 -3
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +5 -5
- package/dist/hooks.js.map +1 -1
- package/dist/sdkIdentity.js +2 -3
- package/dist/useEveAgentRuntime.d.ts +3 -5
- package/dist/useEveAgentRuntime.d.ts.map +1 -1
- package/dist/useEveAgentRuntime.js +34 -23
- package/dist/useEveAgentRuntime.js.map +1 -1
- package/package.json +10 -10
- package/src/convertEveMessages.test.ts +940 -775
- package/src/convertEveMessages.ts +290 -233
- package/src/deriveCreatedAt.test.ts +90 -88
- package/src/deriveCreatedAt.ts +32 -36
- package/src/eveExtras.ts +4 -4
- package/src/hooks.ts +4 -3
- package/src/testUtils.ts +189 -8
- package/src/useEveAgentRuntime.cloud.test.tsx +105 -136
- package/src/useEveAgentRuntime.eveResume.test.tsx +22 -104
- package/src/useEveAgentRuntime.fast-refresh.test.tsx +195 -37
- package/src/useEveAgentRuntime.interruptedTurn.test.tsx +86 -40
- package/src/useEveAgentRuntime.refetch.test.tsx +19 -58
- package/src/useEveAgentRuntime.test.tsx +372 -291
- package/src/useEveAgentRuntime.ts +100 -113
|
@@ -33,12 +33,9 @@ import { invokeUserCallback } from "@assistant-ui/core/internal";
|
|
|
33
33
|
import { useReplaySafeEffect } from "@assistant-ui/store/internal";
|
|
34
34
|
import { useAui } from "@assistant-ui/store";
|
|
35
35
|
import type { AssistantCloud } from "assistant-cloud";
|
|
36
|
-
import {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
type UseEveAgentOptions,
|
|
40
|
-
type UseEveAgentStatus,
|
|
41
|
-
} from "eve/react";
|
|
36
|
+
import { useEveAgent, type UseEveAgentOptions } from "eve/react";
|
|
37
|
+
import { activeTurn, idle, type JsonObject } from "eve/events";
|
|
38
|
+
import type { Delivery } from "eve/sessions";
|
|
42
39
|
import {
|
|
43
40
|
collectInterruptedTurnEvents,
|
|
44
41
|
convertEveMessages,
|
|
@@ -79,23 +76,6 @@ const hasRunConfig = (
|
|
|
79
76
|
): runConfig is NonNullable<AppendMessage["runConfig"]> =>
|
|
80
77
|
runConfig?.custom !== undefined && Object.keys(runConfig.custom).length > 0;
|
|
81
78
|
|
|
82
|
-
type EveJsonValue =
|
|
83
|
-
| string
|
|
84
|
-
| number
|
|
85
|
-
| boolean
|
|
86
|
-
| null
|
|
87
|
-
| readonly EveJsonValue[]
|
|
88
|
-
| { readonly [key: string]: EveJsonValue };
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Structural equivalent of eve's `JsonObject`. Eve demoted its public send
|
|
92
|
-
* payload type once already (`SendTurnPayload`, public through 0.30, internal
|
|
93
|
-
* from 0.31), so the helpers declare the shapes they forward instead of
|
|
94
|
-
* re-exporting eve's option types, and assignability is checked at the send
|
|
95
|
-
* call sites against the installed version.
|
|
96
|
-
*/
|
|
97
|
-
type EveClientContext = { readonly [key: string]: EveJsonValue };
|
|
98
|
-
|
|
99
79
|
/**
|
|
100
80
|
* Only the `custom` bag crosses the wire. Eve reads `clientContext` as its own
|
|
101
81
|
* namespace and serializes it into a model-visible context message, so sending
|
|
@@ -104,14 +84,12 @@ type EveClientContext = { readonly [key: string]: EveJsonValue };
|
|
|
104
84
|
*/
|
|
105
85
|
const toEveSendOptions = (
|
|
106
86
|
runConfig: AppendMessage["runConfig"],
|
|
107
|
-
): { readonly clientContext:
|
|
87
|
+
): { readonly clientContext: JsonObject } | undefined =>
|
|
108
88
|
hasRunConfig(runConfig)
|
|
109
|
-
? { clientContext: runConfig.custom as
|
|
89
|
+
? { clientContext: runConfig.custom as JsonObject }
|
|
110
90
|
: undefined;
|
|
111
91
|
|
|
112
|
-
type
|
|
113
|
-
|
|
114
|
-
export type UseEveAgentRuntimeOptions = EveAgentOptions &
|
|
92
|
+
export type UseEveAgentRuntimeOptions = UseEveAgentOptions &
|
|
115
93
|
ExternalStoreSharedOptions & {
|
|
116
94
|
readonly adapters?:
|
|
117
95
|
| {
|
|
@@ -142,9 +120,9 @@ const withCloudThreadSession = (
|
|
|
142
120
|
resume: _resume,
|
|
143
121
|
session: _session,
|
|
144
122
|
...options
|
|
145
|
-
}:
|
|
123
|
+
}: UseEveAgentOptions,
|
|
146
124
|
sessionId: string | undefined,
|
|
147
|
-
):
|
|
125
|
+
): UseEveAgentOptions =>
|
|
148
126
|
sessionId === undefined
|
|
149
127
|
? options
|
|
150
128
|
: {
|
|
@@ -193,7 +171,6 @@ const useEveThreadRuntime = (
|
|
|
193
171
|
cloudThread.sessionId === undefined;
|
|
194
172
|
|
|
195
173
|
const { onError, onEvent, onFinish, onSessionChange } = agentOptions;
|
|
196
|
-
const lastFinishStatusRef = useRef<UseEveAgentStatus | null>(null);
|
|
197
174
|
const agent = useEveAgent({
|
|
198
175
|
...(cloudThread
|
|
199
176
|
? withCloudThreadSession(agentOptions, cloudThread.sessionId)
|
|
@@ -221,7 +198,6 @@ const useEveThreadRuntime = (
|
|
|
221
198
|
}
|
|
222
199
|
: {}),
|
|
223
200
|
onFinish: (snapshot) => {
|
|
224
|
-
lastFinishStatusRef.current = snapshot.status;
|
|
225
201
|
void invokeUserCallback(
|
|
226
202
|
"assistant-ui/eve",
|
|
227
203
|
"onFinish",
|
|
@@ -253,11 +229,6 @@ const useEveThreadRuntime = (
|
|
|
253
229
|
}
|
|
254
230
|
: {}),
|
|
255
231
|
});
|
|
256
|
-
if (cloudThread && !("resume" in agent)) {
|
|
257
|
-
throw new Error(
|
|
258
|
-
"useEveAgentRuntime needs eve 0.44.1 or later for `cloud`, because opening a cloud thread resumes its session.",
|
|
259
|
-
);
|
|
260
|
-
}
|
|
261
232
|
const runtimeAdapters = useRuntimeAdapters();
|
|
262
233
|
const [toolStatuses, setToolStatuses] = useState<
|
|
263
234
|
Record<string, ToolExecutionStatus>
|
|
@@ -277,7 +248,10 @@ const useEveThreadRuntime = (
|
|
|
277
248
|
(status) => status?.type === "executing",
|
|
278
249
|
);
|
|
279
250
|
const providerIsRunning =
|
|
280
|
-
agent.status === "
|
|
251
|
+
agent.status === "busy" ||
|
|
252
|
+
(agent.status !== "resuming" &&
|
|
253
|
+
activeTurn(agent.view) !== undefined &&
|
|
254
|
+
!idle(agent.view));
|
|
281
255
|
const isRunning = providerIsRunning || hasExecutingTools;
|
|
282
256
|
|
|
283
257
|
// Kept apart from the message memo so events that teach no timestamp keep
|
|
@@ -299,34 +273,41 @@ const useEveThreadRuntime = (
|
|
|
299
273
|
|
|
300
274
|
const convertedMessages = useMemo(() => {
|
|
301
275
|
const createdAtByMessageId = createdAtByMessageIdRef.current;
|
|
302
|
-
const messageIds = new Set(
|
|
303
|
-
agent.data.messages.map((message) => message.id),
|
|
304
|
-
);
|
|
276
|
+
const messageIds = new Set(agent.messages.map((message) => message.id));
|
|
305
277
|
for (const messageId of createdAtByMessageId.keys()) {
|
|
306
278
|
if (!messageIds.has(messageId)) createdAtByMessageId.delete(messageId);
|
|
307
279
|
}
|
|
308
280
|
|
|
309
|
-
return convertEveMessages(
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
turnId
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
281
|
+
return convertEveMessages(
|
|
282
|
+
{ messages: agent.messages },
|
|
283
|
+
{
|
|
284
|
+
isRunning,
|
|
285
|
+
error: agent.error,
|
|
286
|
+
events: interruptionEvents,
|
|
287
|
+
getCreatedAt: (message) => {
|
|
288
|
+
const turnId = message.metadata?.turnId;
|
|
289
|
+
const durable =
|
|
290
|
+
turnId === undefined
|
|
291
|
+
? undefined
|
|
292
|
+
: turnTimestamps.get(turnId)?.[message.role];
|
|
293
|
+
if (durable !== undefined) return durable;
|
|
294
|
+
|
|
295
|
+
const existing = createdAtByMessageId.get(message.id);
|
|
296
|
+
if (existing) return existing;
|
|
297
|
+
|
|
298
|
+
const createdAt = new Date();
|
|
299
|
+
createdAtByMessageId.set(message.id, createdAt);
|
|
300
|
+
return createdAt;
|
|
301
|
+
},
|
|
327
302
|
},
|
|
328
|
-
|
|
329
|
-
}, [
|
|
303
|
+
);
|
|
304
|
+
}, [
|
|
305
|
+
agent.messages,
|
|
306
|
+
agent.error,
|
|
307
|
+
interruptionEvents,
|
|
308
|
+
isRunning,
|
|
309
|
+
turnTimestamps,
|
|
310
|
+
]);
|
|
330
311
|
|
|
331
312
|
const messages = stagedMessages ?? convertedMessages;
|
|
332
313
|
const messagesRef = useRef(messages);
|
|
@@ -338,10 +319,11 @@ const useEveThreadRuntime = (
|
|
|
338
319
|
agentRef.current = agent;
|
|
339
320
|
}, [agent, messages]);
|
|
340
321
|
|
|
341
|
-
//
|
|
342
|
-
//
|
|
343
|
-
//
|
|
344
|
-
|
|
322
|
+
// Eve commands resolve on acceptance, so each dispatch also waits for its
|
|
323
|
+
// delivery to settle; a pending chain link is exactly an unsettled turn, and
|
|
324
|
+
// chaining every dispatch serializes them without watching status.
|
|
325
|
+
const deliveryReadsRef = useRef(new AbortController());
|
|
326
|
+
const lastDeliveryFailedRef = useRef(false);
|
|
345
327
|
const sendChainRef = useRef<Promise<void>>(Promise.resolve());
|
|
346
328
|
const sendEpochRef = useRef(0);
|
|
347
329
|
// A cancel drops the queued send but keeps its draft for a later promotion;
|
|
@@ -354,6 +336,22 @@ const useEveThreadRuntime = (
|
|
|
354
336
|
null,
|
|
355
337
|
);
|
|
356
338
|
|
|
339
|
+
const settleDelivery = async (pending: Promise<Delivery>) => {
|
|
340
|
+
const signal = deliveryReadsRef.current.signal;
|
|
341
|
+
const receipt = await pending;
|
|
342
|
+
try {
|
|
343
|
+
const result = await receipt.result({ signal });
|
|
344
|
+
if (!signal.aborted) {
|
|
345
|
+
lastDeliveryFailedRef.current = result.status === "failed";
|
|
346
|
+
}
|
|
347
|
+
} catch (error) {
|
|
348
|
+
// Only a reset aborts these reads, and the delivery already reached the
|
|
349
|
+
// old session, so it settles instead of handing its draft back.
|
|
350
|
+
if (signal.aborted) return;
|
|
351
|
+
throw error;
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
|
|
357
355
|
const enqueueSend = useCallback(
|
|
358
356
|
(
|
|
359
357
|
dispatch: () => Promise<void>,
|
|
@@ -371,10 +369,9 @@ const useEveThreadRuntime = (
|
|
|
371
369
|
[],
|
|
372
370
|
);
|
|
373
371
|
|
|
374
|
-
//
|
|
375
|
-
//
|
|
376
|
-
//
|
|
377
|
-
// for a remounted tree.
|
|
372
|
+
// Queued sends must not fire server turns after unmount. The flag separates
|
|
373
|
+
// that teardown from a user cancel, and is re-armed in setup for a remounted
|
|
374
|
+
// tree.
|
|
378
375
|
useEffect(() => {
|
|
379
376
|
isMountedRef.current = true;
|
|
380
377
|
return () => {
|
|
@@ -390,12 +387,12 @@ const useEveThreadRuntime = (
|
|
|
390
387
|
[],
|
|
391
388
|
);
|
|
392
389
|
|
|
393
|
-
// A replay rides the send chain
|
|
390
|
+
// A replay rides the send chain so it waits for the active turn to settle; the reset counter keeps a cancel from dropping a refetch, and the hook's own replay on mount joins this one since upstream shares concurrent `resume()` calls.
|
|
394
391
|
const enqueueResume = useCallback(
|
|
395
392
|
() =>
|
|
396
393
|
enqueueSend(async () => {
|
|
397
394
|
const live = agentRef.current;
|
|
398
|
-
if (live.session === undefined
|
|
395
|
+
if (live.session === undefined) return;
|
|
399
396
|
await live.resume();
|
|
400
397
|
}, resetEpochRef),
|
|
401
398
|
[enqueueSend],
|
|
@@ -468,15 +465,17 @@ const useEveThreadRuntime = (
|
|
|
468
465
|
runtimeRef.current?.thread.unstable_notifySessionReset();
|
|
469
466
|
// Sends parked behind an active turn captured the pre-reset epoch, so the
|
|
470
467
|
// epoch has to advance before the session is torn down or they dispatch
|
|
471
|
-
// into the new one
|
|
472
|
-
// reason.
|
|
468
|
+
// into the new one.
|
|
473
469
|
sendEpochRef.current += 1;
|
|
474
470
|
resetEpochRef.current += 1;
|
|
475
|
-
|
|
471
|
+
lastDeliveryFailedRef.current = false;
|
|
472
|
+
deliveryReadsRef.current.abort();
|
|
473
|
+
deliveryReadsRef.current = new AbortController();
|
|
474
|
+
sendChainRef.current = Promise.resolve();
|
|
476
475
|
setStagedMessages(null);
|
|
477
476
|
stagedInputsRef.current.clear();
|
|
478
477
|
setToolStatuses({});
|
|
479
|
-
agent.
|
|
478
|
+
agent.newSession();
|
|
480
479
|
}, [agent, aui, cloudThread]);
|
|
481
480
|
|
|
482
481
|
const extras = useMemo(
|
|
@@ -522,9 +521,11 @@ const useEveThreadRuntime = (
|
|
|
522
521
|
}
|
|
523
522
|
try {
|
|
524
523
|
await enqueueSend(() =>
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
524
|
+
settleDelivery(
|
|
525
|
+
agent.send(
|
|
526
|
+
getEveMessageContent(message),
|
|
527
|
+
toEveSendOptions(message.runConfig),
|
|
528
|
+
),
|
|
528
529
|
),
|
|
529
530
|
);
|
|
530
531
|
} catch (error) {
|
|
@@ -563,9 +564,11 @@ const useEveThreadRuntime = (
|
|
|
563
564
|
: input.runConfig;
|
|
564
565
|
try {
|
|
565
566
|
await enqueueSend(() =>
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
567
|
+
settleDelivery(
|
|
568
|
+
agent.send(
|
|
569
|
+
getEveMessageContent(input.message),
|
|
570
|
+
toEveSendOptions(runConfig),
|
|
571
|
+
),
|
|
569
572
|
),
|
|
570
573
|
);
|
|
571
574
|
} catch (error) {
|
|
@@ -579,47 +582,31 @@ const useEveThreadRuntime = (
|
|
|
579
582
|
if (isDroppedSend(error)) return;
|
|
580
583
|
throw error;
|
|
581
584
|
}
|
|
582
|
-
if (
|
|
585
|
+
if (lastDeliveryFailedRef.current) return;
|
|
583
586
|
}
|
|
584
587
|
},
|
|
585
588
|
}
|
|
586
589
|
: {}),
|
|
587
590
|
onCancel: async () => {
|
|
588
591
|
sendEpochRef.current += 1;
|
|
589
|
-
|
|
590
|
-
// `cancel()`, so the adapter detects which side of that break the host's
|
|
591
|
-
// eve provides instead of pinning the peer range to one of them.
|
|
592
|
-
const controls = agent as
|
|
593
|
-
| { readonly cancel: () => Promise<unknown> }
|
|
594
|
-
| { readonly stop: () => void };
|
|
595
|
-
if ("cancel" in controls) {
|
|
596
|
-
await controls.cancel();
|
|
597
|
-
} else {
|
|
598
|
-
controls.stop();
|
|
599
|
-
}
|
|
592
|
+
await agent.cancel();
|
|
600
593
|
},
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
enqueueResume().catch((error) => {
|
|
607
|
-
if (isDroppedSend(error)) return;
|
|
608
|
-
throw error;
|
|
609
|
-
}),
|
|
610
|
-
}
|
|
611
|
-
: {}),
|
|
594
|
+
onRefetchThread: () =>
|
|
595
|
+
enqueueResume().catch((error) => {
|
|
596
|
+
if (isDroppedSend(error)) return;
|
|
597
|
+
throw error;
|
|
598
|
+
}),
|
|
612
599
|
onRespondToToolApproval: (response) => {
|
|
613
|
-
// Eve resolves
|
|
614
|
-
// empty one is recorded as an answer with no content. Mapping before
|
|
615
|
-
// send is enqueued keeps an unmappable response unsent, so the
|
|
616
|
-
// stays answerable; the mapper's error reaches the caller as
|
|
617
|
-
// rejection.
|
|
618
|
-
const
|
|
600
|
+
// Eve resolves an interaction the moment any answer for it arrives, and
|
|
601
|
+
// an empty one is recorded as an answer with no content. Mapping before
|
|
602
|
+
// the send is enqueued keeps an unmappable response unsent, so the
|
|
603
|
+
// interaction stays answerable; the mapper's error reaches the caller as
|
|
604
|
+
// the seam's rejection.
|
|
605
|
+
const answer = toEveInputResponse(
|
|
619
606
|
response,
|
|
620
|
-
findEveInputRequest(agent
|
|
607
|
+
findEveInputRequest(agent, response.approvalId),
|
|
621
608
|
);
|
|
622
|
-
return enqueueSend(() => agent.respond([
|
|
609
|
+
return enqueueSend(() => settleDelivery(agent.respond([answer]))).catch(
|
|
623
610
|
(error) => {
|
|
624
611
|
if (!isDroppedSend(error)) throw error;
|
|
625
612
|
},
|
|
@@ -655,7 +642,7 @@ const useEveCloudRuntime = (
|
|
|
655
642
|
* Connects Eve's `useEveAgent` hook to assistant-ui's runtime contract.
|
|
656
643
|
*
|
|
657
644
|
* The runtime renders Eve messages, forwards new user messages to the Eve
|
|
658
|
-
* session, supports cancellation, and maps Eve
|
|
645
|
+
* session, supports cancellation, and maps Eve interactions to assistant-ui
|
|
659
646
|
* tool approval UI.
|
|
660
647
|
*/
|
|
661
648
|
export const useEveAgentRuntime = (options: UseEveAgentRuntimeOptions = {}) => {
|