@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.
@@ -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 toEveClientContext = (
110
+ const toEveSendOptions = (
93
111
  runConfig: AppendMessage["runConfig"],
94
- ): Pick<SendTurnPayload, "clientContext"> =>
112
+ ): { readonly clientContext: EveClientContext } | undefined =>
95
113
  hasRunConfig(runConfig)
96
- ? {
97
- clientContext: runConfig.custom as NonNullable<
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 isRunning =
190
- agent.status === "submitted" ||
191
- agent.status === "streaming" ||
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.send` rejects while a turn is in flight and only
222
- // resolves once the turn's stream parks, so a pending chain link is exactly
223
- // an active turn; chaining every send serializes them without watching
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 = (payload: Parameters<typeof agent.send>[0]) => {
230
- const epoch = sendEpochRef.current;
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 (epoch !== sendEpochRef.current)
233
- throw isMountedRef.current ? sendCancelledError : sendAbandonedError;
234
- return agent.send(payload);
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
- return useExternalStoreRuntime({
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
- message: getEveMessageContent(message),
314
- ...toEveClientContext(message.runConfig),
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
- message: getEveMessageContent(input.message),
352
- ...toEveClientContext(runConfig),
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
- agent.stop();
369
- return Promise.resolve();
370
- },
371
- onRespondToToolApproval: async (response) => {
372
- try {
373
- await enqueueSend({ inputResponses: [toEveInputResponse(response)] });
374
- } catch (error) {
375
- if (!isDroppedSend(error)) throw error;
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
  };