@assistant-ui/eve 0.0.24 → 0.0.26

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.
Files changed (37) hide show
  1. package/dist/convertEveMessages.d.ts +40 -51
  2. package/dist/convertEveMessages.d.ts.map +1 -1
  3. package/dist/convertEveMessages.js +146 -117
  4. package/dist/convertEveMessages.js.map +1 -1
  5. package/dist/deriveCreatedAt.d.ts +9 -9
  6. package/dist/deriveCreatedAt.d.ts.map +1 -1
  7. package/dist/deriveCreatedAt.js +25 -34
  8. package/dist/deriveCreatedAt.js.map +1 -1
  9. package/dist/eveCloudSessions.js +1 -2
  10. package/dist/eveExtras.d.ts +4 -2
  11. package/dist/eveExtras.d.ts.map +1 -1
  12. package/dist/eveExtras.js +1 -2
  13. package/dist/eveExtras.js.map +1 -1
  14. package/dist/hooks.d.ts +4 -3
  15. package/dist/hooks.d.ts.map +1 -1
  16. package/dist/hooks.js +5 -5
  17. package/dist/hooks.js.map +1 -1
  18. package/dist/sdkIdentity.js +2 -3
  19. package/dist/useEveAgentRuntime.d.ts +3 -5
  20. package/dist/useEveAgentRuntime.d.ts.map +1 -1
  21. package/dist/useEveAgentRuntime.js +34 -23
  22. package/dist/useEveAgentRuntime.js.map +1 -1
  23. package/package.json +10 -10
  24. package/src/convertEveMessages.test.ts +925 -729
  25. package/src/convertEveMessages.ts +282 -227
  26. package/src/deriveCreatedAt.test.ts +90 -88
  27. package/src/deriveCreatedAt.ts +32 -36
  28. package/src/eveExtras.ts +4 -4
  29. package/src/hooks.ts +4 -3
  30. package/src/testUtils.ts +189 -8
  31. package/src/useEveAgentRuntime.cloud.test.tsx +105 -136
  32. package/src/useEveAgentRuntime.eveResume.test.tsx +22 -104
  33. package/src/useEveAgentRuntime.fast-refresh.test.tsx +195 -37
  34. package/src/useEveAgentRuntime.interruptedTurn.test.tsx +86 -40
  35. package/src/useEveAgentRuntime.refetch.test.tsx +19 -58
  36. package/src/useEveAgentRuntime.test.tsx +372 -291
  37. 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
- useEveAgent,
38
- type EveMessageData,
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: EveClientContext } | undefined =>
87
+ ): { readonly clientContext: JsonObject } | undefined =>
108
88
  hasRunConfig(runConfig)
109
- ? { clientContext: runConfig.custom as EveClientContext }
89
+ ? { clientContext: runConfig.custom as JsonObject }
110
90
  : undefined;
111
91
 
112
- type EveAgentOptions = Omit<UseEveAgentOptions<EveMessageData>, "reducer">;
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
- }: EveAgentOptions,
123
+ }: UseEveAgentOptions,
146
124
  sessionId: string | undefined,
147
- ): EveAgentOptions =>
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 === "submitted" || agent.status === "streaming";
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(agent.data, {
310
- isRunning,
311
- error: agent.error,
312
- events: interruptionEvents,
313
- getCreatedAt: (message) => {
314
- const turnId = message.metadata?.turnId;
315
- const durable =
316
- turnId === undefined
317
- ? undefined
318
- : turnTimestamps.get(turnId)?.[message.role];
319
- if (durable !== undefined) return durable;
320
-
321
- const existing = createdAtByMessageId.get(message.id);
322
- if (existing) return existing;
323
-
324
- const createdAt = new Date();
325
- createdAtByMessageId.set(message.id, createdAt);
326
- return createdAt;
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
- }, [agent.data, agent.error, interruptionEvents, isRunning, turnTimestamps]);
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
- // Upstream `EveAgentStore` `send` and `respond` reject while a turn is in
342
- // flight and only resolve once the turn's stream parks, so a pending chain
343
- // link is exactly an active turn; chaining every dispatch serializes them
344
- // without watching status.
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
- // The store outlives the component (useEveAgent holds it in a ref with no
375
- // cleanup), so queued sends must not fire server turns after unmount. The
376
- // flag separates that teardown from a user cancel, and is re-armed in setup
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 because `resume()` rejects during a turn and upstream refuses sends while it runs; 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.
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 || !("resume" in live)) return;
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; `lastFinishStatusRef` is run-scoped for the same
472
- // reason.
468
+ // into the new one.
473
469
  sendEpochRef.current += 1;
474
470
  resetEpochRef.current += 1;
475
- lastFinishStatusRef.current = null;
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.reset();
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
- agent.send(
526
- getEveMessageContent(message),
527
- toEveSendOptions(message.runConfig),
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
- agent.send(
567
- getEveMessageContent(input.message),
568
- toEveSendOptions(runConfig),
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 (lastFinishStatusRef.current === "error") return;
585
+ if (lastDeliveryFailedRef.current) return;
583
586
  }
584
587
  },
585
588
  }
586
589
  : {}),
587
590
  onCancel: async () => {
588
591
  sendEpochRef.current += 1;
589
- // Eve 0.38 replaced the binding's local-abort `stop()` with the durable
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
- // Hosts below eve 0.44.1 expose no `resume`; leaving the capability absent
602
- // keeps `threads.reloadMainThread()` on core's no-capability no-op.
603
- ...("resume" in agent
604
- ? {
605
- onRefetchThread: () =>
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 a request the moment any response for it arrives, and an
614
- // empty one is recorded as an answer with no content. Mapping before the
615
- // send is enqueued keeps an unmappable response unsent, so the request
616
- // stays answerable; the mapper's error reaches the caller as the seam's
617
- // rejection.
618
- const inputResponse = toEveInputResponse(
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.data, response.approvalId),
607
+ findEveInputRequest(agent, response.approvalId),
621
608
  );
622
- return enqueueSend(() => agent.respond([inputResponse])).catch(
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 input requests to assistant-ui
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 = {}) => {