@assistant-ui/eve 0.0.14 → 0.0.16

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.
@@ -1,6 +1,13 @@
1
1
  "use client";
2
2
 
3
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
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,
@@ -28,6 +35,7 @@ import {
28
35
  } from "eve/react";
29
36
  import {
30
37
  convertEveMessages,
38
+ findEveInputRequest,
31
39
  getEveMessageContent,
32
40
  toEveInputResponse,
33
41
  } from "./convertEveMessages";
@@ -199,10 +207,9 @@ export const useEveAgentRuntime = (options: UseEveAgentRuntimeOptions = {}) => {
199
207
  const hasExecutingTools = Object.values(toolStatuses).some(
200
208
  (status) => status?.type === "executing",
201
209
  );
202
- const isRunning =
203
- agent.status === "submitted" ||
204
- agent.status === "streaming" ||
205
- hasExecutingTools;
210
+ const providerIsRunning =
211
+ agent.status === "submitted" || agent.status === "streaming";
212
+ const isRunning = providerIsRunning || hasExecutingTools;
206
213
 
207
214
  const convertedMessages = useMemo(() => {
208
215
  const createdAtByMessageId = createdAtByMessageIdRef.current;
@@ -229,7 +236,13 @@ export const useEveAgentRuntime = (options: UseEveAgentRuntimeOptions = {}) => {
229
236
 
230
237
  const messages = stagedMessages ?? convertedMessages;
231
238
  const messagesRef = useRef(messages);
232
- messagesRef.current = messages;
239
+ const agentRef = useRef(agent);
240
+ // Descendant layout effects can dispatch against these refs synchronously;
241
+ // this matches useA2ARuntime's commit-scoped ref publication.
242
+ useInsertionEffect(() => {
243
+ messagesRef.current = messages;
244
+ agentRef.current = agent;
245
+ }, [agent, messages]);
233
246
 
234
247
  // Upstream `EveAgentStore` `send` and `respond` reject while a turn is in
235
248
  // flight and only resolve once the turn's stream parks, so a pending chain
@@ -239,18 +252,22 @@ export const useEveAgentRuntime = (options: UseEveAgentRuntimeOptions = {}) => {
239
252
  const sendEpochRef = useRef(0);
240
253
  // A cancel drops the queued send but keeps its draft for a later promotion;
241
254
  // only a reset discards the draft with the session, so the two need separate
242
- // counters.
255
+ // counters. A queued read has no draft and outlives a cancel, so it waits on
256
+ // the reset counter instead.
243
257
  const resetEpochRef = useRef(0);
244
258
  const isMountedRef = useRef(true);
245
259
  const runtimeRef = useRef<ReturnType<typeof useExternalStoreRuntime> | null>(
246
260
  null,
247
261
  );
248
262
 
249
- const enqueueSend = (dispatch: () => Promise<void>) => {
250
- const epoch = sendEpochRef.current;
263
+ const enqueueSend = (
264
+ dispatch: () => Promise<void>,
265
+ epochRef: { current: number } = sendEpochRef,
266
+ ) => {
267
+ const epoch = epochRef.current;
251
268
  const next = sendChainRef.current.then(() => {
252
- if (epoch !== sendEpochRef.current)
253
- throw isMountedRef.current ? sendCancelledError : sendAbandonedError;
269
+ if (!isMountedRef.current) throw sendAbandonedError;
270
+ if (epoch !== epochRef.current) throw sendCancelledError;
254
271
  return dispatch();
255
272
  });
256
273
  sendChainRef.current = next.catch(() => {});
@@ -339,7 +356,7 @@ export const useEveAgentRuntime = (options: UseEveAgentRuntimeOptions = {}) => {
339
356
  const runtime = useExternalStoreRuntime({
340
357
  ...pickExternalStoreSharedOptions(options),
341
358
  messages,
342
- isRunning,
359
+ isRunning: providerIsRunning,
343
360
  extras,
344
361
  unstable_enableToolInvocations: true,
345
362
  setToolStatuses,
@@ -419,18 +436,60 @@ export const useEveAgentRuntime = (options: UseEveAgentRuntimeOptions = {}) => {
419
436
  },
420
437
  }
421
438
  : {}),
422
- onCancel: () => {
439
+ onCancel: async () => {
423
440
  sendEpochRef.current += 1;
424
- agent.stop();
425
- return Promise.resolve();
426
- },
427
- onRespondToToolApproval: async (response) => {
428
- try {
429
- await enqueueSend(() => agent.respond([toEveInputResponse(response)]));
430
- } catch (error) {
431
- if (!isDroppedSend(error)) throw error;
441
+ // Eve 0.38 replaced the binding's local-abort `stop()` with the durable
442
+ // `cancel()`, so the adapter detects which side of that break the host's
443
+ // eve provides instead of pinning the peer range to one of them.
444
+ const controls = agent as
445
+ | { readonly cancel: () => Promise<unknown> }
446
+ | { readonly stop: () => void };
447
+ if ("cancel" in controls) {
448
+ await controls.cancel();
449
+ } else {
450
+ controls.stop();
432
451
  }
433
452
  },
453
+ // Hosts below eve 0.44.1 expose no `resume`; leaving the capability absent
454
+ // keeps `threads.reloadMainThread()` on core's no-capability no-op.
455
+ ...("resume" in agent
456
+ ? {
457
+ onRefetchThread: () =>
458
+ // `resume()` rejects during a turn, so the replay rides the send
459
+ // chain like every other dispatch and runs once the turn parks; a
460
+ // cancel of that turn does not drop it, or the caller would be told
461
+ // the thread was refetched when it was not. Upstream shares one
462
+ // replay across concurrent `resume()` calls, so a refetch
463
+ // dispatched while `resume: true` is replaying on mount joins that
464
+ // read instead of taking a fresh one. The session is read at
465
+ // dispatch time because nothing durable exists before the first
466
+ // send lands.
467
+ enqueueSend(async () => {
468
+ const live = agentRef.current;
469
+ if (live.session === undefined) return;
470
+ await live.resume();
471
+ }, resetEpochRef).catch((error) => {
472
+ if (isDroppedSend(error)) return;
473
+ throw error;
474
+ }),
475
+ }
476
+ : {}),
477
+ onRespondToToolApproval: (response) => {
478
+ // Eve resolves a request the moment any response for it arrives, and an
479
+ // empty one is recorded as an answer with no content. Mapping before the
480
+ // send is enqueued keeps an unmappable response unsent, so the request
481
+ // stays answerable; the mapper's error reaches the caller as the seam's
482
+ // rejection.
483
+ const inputResponse = toEveInputResponse(
484
+ response,
485
+ findEveInputRequest(agent.data, response.approvalId),
486
+ );
487
+ return enqueueSend(() => agent.respond([inputResponse])).catch(
488
+ (error) => {
489
+ if (!isDroppedSend(error)) throw error;
490
+ },
491
+ );
492
+ },
434
493
  });
435
494
  runtimeRef.current = runtime;
436
495