@assistant-ui/eve 0.0.13 → 0.0.14

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,12 @@ 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,
32
31
  getEveMessageContent,
33
32
  toEveInputResponse,
34
33
  } from "./convertEveMessages";
34
+ import { eveExtras } from "./eveExtras";
35
35
 
36
36
  const USER_STAGED_STATUS = {
37
37
  type: "complete",
@@ -83,22 +83,35 @@ const hasRunConfig = (
83
83
  ): runConfig is NonNullable<AppendMessage["runConfig"]> =>
84
84
  runConfig?.custom !== undefined && Object.keys(runConfig.custom).length > 0;
85
85
 
86
+ type EveJsonValue =
87
+ | string
88
+ | number
89
+ | boolean
90
+ | null
91
+ | readonly EveJsonValue[]
92
+ | { readonly [key: string]: EveJsonValue };
93
+
94
+ /**
95
+ * Structural equivalent of eve's `JsonObject`. Eve demoted its public send
96
+ * payload type once already (`SendTurnPayload`, public through 0.30, internal
97
+ * from 0.31), so the helpers declare the shapes they forward instead of
98
+ * re-exporting eve's option types, and assignability is checked at the send
99
+ * call sites against the installed version.
100
+ */
101
+ type EveClientContext = { readonly [key: string]: EveJsonValue };
102
+
86
103
  /**
87
104
  * Only the `custom` bag crosses the wire. Eve reads `clientContext` as its own
88
105
  * namespace and serializes it into a model-visible context message, so sending
89
106
  * the assistant-ui envelope would surface a literal `"custom"` key in the
90
107
  * prompt and to every eve-side handler.
91
108
  */
92
- const toEveClientContext = (
109
+ const toEveSendOptions = (
93
110
  runConfig: AppendMessage["runConfig"],
94
- ): Pick<SendTurnPayload, "clientContext"> =>
111
+ ): { readonly clientContext: EveClientContext } | undefined =>
95
112
  hasRunConfig(runConfig)
96
- ? {
97
- clientContext: runConfig.custom as NonNullable<
98
- SendTurnPayload["clientContext"]
99
- >,
100
- }
101
- : {};
113
+ ? { clientContext: runConfig.custom as EveClientContext }
114
+ : undefined;
102
115
 
103
116
  export type UseEveAgentRuntimeOptions = Omit<
104
117
  UseEveAgentOptions<EveMessageData>,
@@ -218,20 +231,27 @@ export const useEveAgentRuntime = (options: UseEveAgentRuntimeOptions = {}) => {
218
231
  const messagesRef = useRef(messages);
219
232
  messagesRef.current = messages;
220
233
 
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.
234
+ // Upstream `EveAgentStore` `send` and `respond` reject while a turn is in
235
+ // flight and only resolve once the turn's stream parks, so a pending chain
236
+ // link is exactly an active turn; chaining every dispatch serializes them
237
+ // without watching status.
225
238
  const sendChainRef = useRef<Promise<void>>(Promise.resolve());
226
239
  const sendEpochRef = useRef(0);
240
+ // A cancel drops the queued send but keeps its draft for a later promotion;
241
+ // only a reset discards the draft with the session, so the two need separate
242
+ // counters.
243
+ const resetEpochRef = useRef(0);
227
244
  const isMountedRef = useRef(true);
245
+ const runtimeRef = useRef<ReturnType<typeof useExternalStoreRuntime> | null>(
246
+ null,
247
+ );
228
248
 
229
- const enqueueSend = (payload: Parameters<typeof agent.send>[0]) => {
249
+ const enqueueSend = (dispatch: () => Promise<void>) => {
230
250
  const epoch = sendEpochRef.current;
231
251
  const next = sendChainRef.current.then(() => {
232
252
  if (epoch !== sendEpochRef.current)
233
253
  throw isMountedRef.current ? sendCancelledError : sendAbandonedError;
234
- return agent.send(payload);
254
+ return dispatch();
235
255
  });
236
256
  sendChainRef.current = next.catch(() => {});
237
257
  return next;
@@ -290,10 +310,37 @@ export const useEveAgentRuntime = (options: UseEveAgentRuntimeOptions = {}) => {
290
310
  setStagedMessages(nextMessages);
291
311
  };
292
312
 
293
- return useExternalStoreRuntime({
313
+ const reset = useCallback(() => {
314
+ runtimeRef.current?.thread.unstable_notifySessionReset();
315
+ // Sends parked behind an active turn captured the pre-reset epoch, so the
316
+ // epoch has to advance before the session is torn down or they dispatch
317
+ // into the new one; `lastFinishStatusRef` is run-scoped for the same
318
+ // reason.
319
+ sendEpochRef.current += 1;
320
+ resetEpochRef.current += 1;
321
+ lastFinishStatusRef.current = null;
322
+ setStagedMessages(null);
323
+ stagedInputsRef.current.clear();
324
+ setToolStatuses({});
325
+ agent.reset();
326
+ }, [agent]);
327
+
328
+ const extras = useMemo(
329
+ () =>
330
+ eveExtras.provide({
331
+ error: agent.error,
332
+ events: agent.events,
333
+ session: agent.session,
334
+ reset,
335
+ }),
336
+ [agent.error, agent.events, agent.session, reset],
337
+ );
338
+
339
+ const runtime = useExternalStoreRuntime({
294
340
  ...pickExternalStoreSharedOptions(options),
295
341
  messages,
296
342
  isRunning,
343
+ extras,
297
344
  unstable_enableToolInvocations: true,
298
345
  setToolStatuses,
299
346
  adapters: {
@@ -309,10 +356,12 @@ export const useEveAgentRuntime = (options: UseEveAgentRuntimeOptions = {}) => {
309
356
  return;
310
357
  }
311
358
  try {
312
- await enqueueSend({
313
- message: getEveMessageContent(message),
314
- ...toEveClientContext(message.runConfig),
315
- });
359
+ await enqueueSend(() =>
360
+ agent.send(
361
+ getEveMessageContent(message),
362
+ toEveSendOptions(message.runConfig),
363
+ ),
364
+ );
316
365
  } catch (error) {
317
366
  // A cancelled send never reached the session, so it rethrows for the
318
367
  // composer to take the draft back; an unmounted one has no composer
@@ -328,6 +377,7 @@ export const useEveAgentRuntime = (options: UseEveAgentRuntimeOptions = {}) => {
328
377
  if (!stagedRun)
329
378
  throw new Error("Runtime does not support reloading messages.");
330
379
  const epoch = sendEpochRef.current;
380
+ const resetEpoch = resetEpochRef.current;
331
381
  for (const { message: stagedMessage, input } of stagedRun) {
332
382
  if (epoch !== sendEpochRef.current) return;
333
383
  const previousMessages = messagesRef.current;
@@ -347,11 +397,17 @@ export const useEveAgentRuntime = (options: UseEveAgentRuntimeOptions = {}) => {
347
397
  ? config.runConfig
348
398
  : input.runConfig;
349
399
  try {
350
- await enqueueSend({
351
- message: getEveMessageContent(input.message),
352
- ...toEveClientContext(runConfig),
353
- });
400
+ await enqueueSend(() =>
401
+ agent.send(
402
+ getEveMessageContent(input.message),
403
+ toEveSendOptions(runConfig),
404
+ ),
405
+ );
354
406
  } catch (error) {
407
+ // A reset discarded the draft along with the session, so
408
+ // restoring it here would refloat a message the thread no
409
+ // longer has. A cancel still restores.
410
+ if (resetEpoch !== resetEpochRef.current) return;
355
411
  stagedInputsRef.current.set(stagedMessage.id, input);
356
412
  messagesRef.current = previousMessages;
357
413
  setStagedMessages(previousMessages);
@@ -370,10 +426,13 @@ export const useEveAgentRuntime = (options: UseEveAgentRuntimeOptions = {}) => {
370
426
  },
371
427
  onRespondToToolApproval: async (response) => {
372
428
  try {
373
- await enqueueSend({ inputResponses: [toEveInputResponse(response)] });
429
+ await enqueueSend(() => agent.respond([toEveInputResponse(response)]));
374
430
  } catch (error) {
375
431
  if (!isDroppedSend(error)) throw error;
376
432
  }
377
433
  },
378
434
  });
435
+ runtimeRef.current = runtime;
436
+
437
+ return runtime;
379
438
  };