@assistant-ui/eve 0.0.16 → 0.0.17

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.
@@ -0,0 +1,23 @@
1
+ import { MessageStreamEvent } from "eve/client";
2
+ //#region src/deriveCreatedAt.d.ts
3
+ type TurnTimestamps = {
4
+ readonly user?: Date;
5
+ readonly assistant?: Date;
6
+ };
7
+ type TurnTimestampCache = {
8
+ lastEvents: readonly MessageStreamEvent[];
9
+ timestamps: ReadonlyMap<string, TurnTimestamps>;
10
+ };
11
+ declare const createTurnTimestampCache: () => TurnTimestampCache;
12
+ /**
13
+ * Eve appends to its log (`[...events, event]`), so a snapshot that keeps the
14
+ * previous scan's last element by identity shares the whole prefix and the
15
+ * scan resumes there. Any other snapshot is re-derived into a fresh map: turn
16
+ * ids are per-session sequence numbers that recur after `reset()`, so entries
17
+ * must not outlive the log that produced them. Re-derivation is also what
18
+ * makes the render-phase cache writes safe when React discards a render.
19
+ */
20
+ declare const collectTurnTimestamps: (events: readonly MessageStreamEvent[], cache: TurnTimestampCache) => ReadonlyMap<string, TurnTimestamps>;
21
+ //#endregion
22
+ export { TurnTimestampCache, TurnTimestamps, collectTurnTimestamps, createTurnTimestampCache };
23
+ //# sourceMappingURL=deriveCreatedAt.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deriveCreatedAt.d.ts","names":[],"sources":["../src/deriveCreatedAt.ts"],"mappings":";;KAEY;WACD,OAAO;WACP,YAAY;;KAGX;EACV,qBAAqB;EACrB,YAAY,oBAAoB;;cAiCrB,gCAA+B;;;;;;;;;cAa/B,wBACX,iBAAiB,sBACjB,OAAO,uBACN,oBAAoB"}
@@ -0,0 +1,79 @@
1
+ //#region src/deriveCreatedAt.ts
2
+ /**
3
+ * The reducer cases eve projects into a message: `message.received` creates
4
+ * `${turnId}:user`, and every assistant entry reaches `updateAssistantMessage`,
5
+ * which creates `${turnId}:assistant` for a turn that has none. It enumerates
6
+ * that reducer across the `>=0.32.0` peer range rather than stating a rule
7
+ * about turn ids, because `turn.started` and the compaction events carry one
8
+ * before the model runs. An event eve adds to the creating set later is absent
9
+ * here and falls back to the wall clock instead of stamping a wrong time.
10
+ */
11
+ const ROLE_BY_EVENT_TYPE = {
12
+ "message.received": "user",
13
+ "step.started": "assistant",
14
+ "reasoning.appended": "assistant",
15
+ "reasoning.completed": "assistant",
16
+ "action.input.appended": "assistant",
17
+ "actions.requested": "assistant",
18
+ "input.requested": "assistant",
19
+ "action.result": "assistant",
20
+ "action.partial": "assistant",
21
+ "authorization.required": "assistant",
22
+ "authorization.completed": "assistant",
23
+ "message.appended": "assistant",
24
+ "message.completed": "assistant",
25
+ "result.completed": "assistant",
26
+ "turn.completed": "assistant",
27
+ "turn.cancelled": "assistant"
28
+ };
29
+ const createTurnTimestampCache = () => ({
30
+ lastEvents: [],
31
+ timestamps: /* @__PURE__ */ new Map()
32
+ });
33
+ /**
34
+ * Eve appends to its log (`[...events, event]`), so a snapshot that keeps the
35
+ * previous scan's last element by identity shares the whole prefix and the
36
+ * scan resumes there. Any other snapshot is re-derived into a fresh map: turn
37
+ * ids are per-session sequence numbers that recur after `reset()`, so entries
38
+ * must not outlive the log that produced them. Re-derivation is also what
39
+ * makes the render-phase cache writes safe when React discards a render.
40
+ */
41
+ const collectTurnTimestamps = (events, cache) => {
42
+ if (events === cache.lastEvents) return cache.timestamps;
43
+ const scanned = cache.lastEvents;
44
+ const resumesScan = scanned.length === 0 || events[scanned.length - 1] === scanned[scanned.length - 1];
45
+ let timestamps = cache.timestamps;
46
+ let draft;
47
+ if (!resumesScan) {
48
+ draft = /* @__PURE__ */ new Map();
49
+ timestamps = draft;
50
+ }
51
+ for (let i = resumesScan ? scanned.length : 0; i < events.length; i++) {
52
+ const event = events[i];
53
+ const role = ROLE_BY_EVENT_TYPE[event.type];
54
+ if (role === void 0) continue;
55
+ const data = event.data;
56
+ if (typeof data !== "object" || data === null) continue;
57
+ const turnId = data.turnId;
58
+ if (typeof turnId !== "string") continue;
59
+ const known = timestamps.get(turnId);
60
+ if (known?.[role] !== void 0) continue;
61
+ const at = event.meta?.at;
62
+ if (typeof at !== "string") continue;
63
+ const date = new Date(at);
64
+ if (Number.isNaN(date.getTime())) continue;
65
+ draft ??= new Map(timestamps);
66
+ draft.set(turnId, {
67
+ ...known,
68
+ [role]: date
69
+ });
70
+ timestamps = draft;
71
+ }
72
+ cache.lastEvents = events;
73
+ cache.timestamps = timestamps;
74
+ return timestamps;
75
+ };
76
+ //#endregion
77
+ export { collectTurnTimestamps, createTurnTimestampCache };
78
+
79
+ //# sourceMappingURL=deriveCreatedAt.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deriveCreatedAt.js","names":[],"sources":["../src/deriveCreatedAt.ts"],"sourcesContent":["import type { MessageStreamEvent } from \"eve/client\";\n\nexport type TurnTimestamps = {\n readonly user?: Date;\n readonly assistant?: Date;\n};\n\nexport type TurnTimestampCache = {\n lastEvents: readonly MessageStreamEvent[];\n timestamps: ReadonlyMap<string, TurnTimestamps>;\n};\n\n/**\n * The reducer cases eve projects into a message: `message.received` creates\n * `${turnId}:user`, and every assistant entry reaches `updateAssistantMessage`,\n * which creates `${turnId}:assistant` for a turn that has none. It enumerates\n * that reducer across the `>=0.32.0` peer range rather than stating a rule\n * about turn ids, because `turn.started` and the compaction events carry one\n * before the model runs. An event eve adds to the creating set later is absent\n * here and falls back to the wall clock instead of stamping a wrong time.\n */\nconst ROLE_BY_EVENT_TYPE: Partial<\n Record<MessageStreamEvent[\"type\"], keyof TurnTimestamps>\n> = {\n \"message.received\": \"user\",\n \"step.started\": \"assistant\",\n \"reasoning.appended\": \"assistant\",\n \"reasoning.completed\": \"assistant\",\n \"action.input.appended\": \"assistant\",\n \"actions.requested\": \"assistant\",\n \"input.requested\": \"assistant\",\n \"action.result\": \"assistant\",\n \"action.partial\": \"assistant\",\n \"authorization.required\": \"assistant\",\n \"authorization.completed\": \"assistant\",\n \"message.appended\": \"assistant\",\n \"message.completed\": \"assistant\",\n \"result.completed\": \"assistant\",\n \"turn.completed\": \"assistant\",\n \"turn.cancelled\": \"assistant\",\n};\n\nexport const createTurnTimestampCache = (): TurnTimestampCache => ({\n lastEvents: [],\n timestamps: new Map(),\n});\n\n/**\n * Eve appends to its log (`[...events, event]`), so a snapshot that keeps the\n * previous scan's last element by identity shares the whole prefix and the\n * scan resumes there. Any other snapshot is re-derived into a fresh map: turn\n * ids are per-session sequence numbers that recur after `reset()`, so entries\n * must not outlive the log that produced them. Re-derivation is also what\n * makes the render-phase cache writes safe when React discards a render.\n */\nexport const collectTurnTimestamps = (\n events: readonly MessageStreamEvent[],\n cache: TurnTimestampCache,\n): ReadonlyMap<string, TurnTimestamps> => {\n if (events === cache.lastEvents) return cache.timestamps;\n\n const scanned = cache.lastEvents;\n const resumesScan =\n scanned.length === 0 ||\n events[scanned.length - 1] === scanned[scanned.length - 1];\n\n let timestamps = cache.timestamps;\n let draft: Map<string, TurnTimestamps> | undefined;\n if (!resumesScan) {\n draft = new Map();\n timestamps = draft;\n }\n for (let i = resumesScan ? scanned.length : 0; i < events.length; i++) {\n const event = events[i]!;\n const role = ROLE_BY_EVENT_TYPE[event.type];\n if (role === undefined) continue;\n const data: unknown = (event as { readonly data?: unknown }).data;\n if (typeof data !== \"object\" || data === null) continue;\n const turnId = (data as { readonly turnId?: unknown }).turnId;\n if (typeof turnId !== \"string\") continue;\n const known = timestamps.get(turnId);\n if (known?.[role] !== undefined) continue;\n const at: unknown = (event as { readonly meta?: { readonly at?: unknown } })\n .meta?.at;\n if (typeof at !== \"string\") continue;\n const date = new Date(at);\n if (Number.isNaN(date.getTime())) continue;\n draft ??= new Map(timestamps);\n draft.set(turnId, { ...known, [role]: date });\n timestamps = draft;\n }\n\n cache.lastEvents = events;\n cache.timestamps = timestamps;\n return timestamps;\n};\n"],"mappings":";;;;;;;;;;AAqBA,MAAM,qBAEF;CACF,oBAAoB;CACpB,gBAAgB;CAChB,sBAAsB;CACtB,uBAAuB;CACvB,yBAAyB;CACzB,qBAAqB;CACrB,mBAAmB;CACnB,iBAAiB;CACjB,kBAAkB;CAClB,0BAA0B;CAC1B,2BAA2B;CAC3B,oBAAoB;CACpB,qBAAqB;CACrB,oBAAoB;CACpB,kBAAkB;CAClB,kBAAkB;AACpB;AAEA,MAAa,kCAAsD;CACjE,YAAY,CAAC;CACb,4BAAY,IAAI,IAAI;AACtB;;;;;;;;;AAUA,MAAa,yBACX,QACA,UACwC;CACxC,IAAI,WAAW,MAAM,YAAY,OAAO,MAAM;CAE9C,MAAM,UAAU,MAAM;CACtB,MAAM,cACJ,QAAQ,WAAW,KACnB,OAAO,QAAQ,SAAS,OAAO,QAAQ,QAAQ,SAAS;CAE1D,IAAI,aAAa,MAAM;CACvB,IAAI;CACJ,IAAI,CAAC,aAAa;EAChB,wBAAQ,IAAI,IAAI;EAChB,aAAa;CACf;CACA,KAAK,IAAI,IAAI,cAAc,QAAQ,SAAS,GAAG,IAAI,OAAO,QAAQ,KAAK;EACrE,MAAM,QAAQ,OAAO;EACrB,MAAM,OAAO,mBAAmB,MAAM;EACtC,IAAI,SAAS,KAAA,GAAW;EACxB,MAAM,OAAiB,MAAsC;EAC7D,IAAI,OAAO,SAAS,YAAY,SAAS,MAAM;EAC/C,MAAM,SAAU,KAAuC;EACvD,IAAI,OAAO,WAAW,UAAU;EAChC,MAAM,QAAQ,WAAW,IAAI,MAAM;EACnC,IAAI,QAAQ,UAAU,KAAA,GAAW;EACjC,MAAM,KAAe,MAClB,MAAM;EACT,IAAI,OAAO,OAAO,UAAU;EAC5B,MAAM,OAAO,IAAI,KAAK,EAAE;EACxB,IAAI,OAAO,MAAM,KAAK,QAAQ,CAAC,GAAG;EAClC,UAAU,IAAI,IAAI,UAAU;EAC5B,MAAM,IAAI,QAAQ;GAAE,GAAG;IAAQ,OAAO;EAAK,CAAC;EAC5C,aAAa;CACf;CAEA,MAAM,aAAa;CACnB,MAAM,aAAa;CACnB,OAAO;AACT"}
@@ -1 +1 @@
1
- {"version":3,"file":"useEveAgentRuntime.d.ts","names":[],"sources":["../src/useEveAgentRuntime.ts"],"mappings":";;;KA2HY,4BAA4B,KACtC,mBAAmB,8BAGnB;WACW;aAEM,cAAc;aACd,SAAS;aACT,YAAY;aACZ,QAAQ;aACR,WAAW;;;;;;;;;;cAYjB,qBAAsB,UAAS,2DAA8B"}
1
+ {"version":3,"file":"useEveAgentRuntime.d.ts","names":[],"sources":["../src/useEveAgentRuntime.ts"],"mappings":";;;KA+HY,4BAA4B,KACtC,mBAAmB,8BAGnB;WACW;aAEM,cAAc;aACd,SAAS;aACT,YAAY;aACZ,QAAQ;aACR,WAAW;;;;;;;;;;cAYjB,qBAAsB,UAAS,2DAA8B"}
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
  import { convertEveMessages, findEveInputRequest, getEveMessageContent, toEveInputResponse } from "./convertEveMessages.js";
3
+ import { collectTurnTimestamps, createTurnTimestampCache } from "./deriveCreatedAt.js";
3
4
  import { eveExtras } from "./eveExtras.js";
4
5
  import { MessageNotSentError, fromThreadMessageLike, generateId, pickExternalStoreSharedOptions } from "@assistant-ui/core";
5
6
  import { useExternalStoreRuntime, useRuntimeAdapters } from "@assistant-ui/core/react";
@@ -63,6 +64,8 @@ const useEveAgentRuntime = (options = {}) => {
63
64
  const hasExecutingTools = Object.values(toolStatuses).some((status) => status?.type === "executing");
64
65
  const providerIsRunning = agent.status === "submitted" || agent.status === "streaming";
65
66
  const isRunning = providerIsRunning || hasExecutingTools;
67
+ const turnTimestampCacheRef = useRef(createTurnTimestampCache());
68
+ const turnTimestamps = useMemo(() => collectTurnTimestamps(agent.events, turnTimestampCacheRef.current), [agent.events]);
66
69
  const convertedMessages = useMemo(() => {
67
70
  const createdAtByMessageId = createdAtByMessageIdRef.current;
68
71
  const messageIds = new Set(agent.data.messages.map((message) => message.id));
@@ -71,6 +74,9 @@ const useEveAgentRuntime = (options = {}) => {
71
74
  isRunning,
72
75
  error: agent.error,
73
76
  getCreatedAt: (message) => {
77
+ const turnId = message.metadata?.turnId;
78
+ const durable = turnId === void 0 ? void 0 : turnTimestamps.get(turnId)?.[message.role];
79
+ if (durable !== void 0) return durable;
74
80
  const existing = createdAtByMessageId.get(message.id);
75
81
  if (existing) return existing;
76
82
  const createdAt = /* @__PURE__ */ new Date();
@@ -81,7 +87,8 @@ const useEveAgentRuntime = (options = {}) => {
81
87
  }, [
82
88
  agent.data,
83
89
  agent.error,
84
- isRunning
90
+ isRunning,
91
+ turnTimestamps
85
92
  ]);
86
93
  const messages = stagedMessages ?? convertedMessages;
87
94
  const messagesRef = useRef(messages);
@@ -1 +1 @@
1
- {"version":3,"file":"useEveAgentRuntime.js","names":[],"sources":["../src/useEveAgentRuntime.ts"],"sourcesContent":["\"use client\";\n\nimport {\n useCallback,\n useEffect,\n useInsertionEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport {\n fromThreadMessageLike,\n generateId,\n MessageNotSentError,\n pickExternalStoreSharedOptions,\n type AppendMessage,\n type AttachmentAdapter,\n type DictationAdapter,\n type ExternalStoreSharedOptions,\n type FeedbackAdapter,\n type RealtimeVoiceAdapter,\n type SpeechSynthesisAdapter,\n type ThreadMessage,\n type ToolExecutionStatus,\n} from \"@assistant-ui/core\";\nimport {\n useExternalStoreRuntime,\n useRuntimeAdapters,\n} from \"@assistant-ui/core/react\";\nimport {\n useEveAgent,\n type EveMessageData,\n type UseEveAgentOptions,\n type UseEveAgentStatus,\n} from \"eve/react\";\nimport {\n convertEveMessages,\n findEveInputRequest,\n getEveMessageContent,\n toEveInputResponse,\n} from \"./convertEveMessages\";\nimport { eveExtras } from \"./eveExtras\";\n\nconst USER_STAGED_STATUS = {\n type: \"complete\",\n reason: \"unknown\",\n} as const;\n\nconst sendCancelledError = new MessageNotSentError(\n \"eve send was dropped because the run was cancelled.\",\n);\n\nconst sendAbandonedError = new Error(\n \"eve send was dropped because the runtime unmounted.\",\n);\n\nconst isDroppedSend = (error: unknown) =>\n error === sendCancelledError || error === sendAbandonedError;\n\ntype EveLifecycleCallbackName =\n | \"onError\"\n | \"onEvent\"\n | \"onFinish\"\n | \"onSessionChange\";\n\nconst reportEveLifecycleCallbackError = (\n name: EveLifecycleCallbackName,\n error: unknown,\n) => {\n console.error(`[assistant-ui/eve] ${name} callback threw an error`, error);\n};\n\nconst invokeEveLifecycleCallback = <T>(\n name: EveLifecycleCallbackName,\n callback: ((value: T) => unknown) | undefined,\n value: T,\n) => {\n if (!callback) return;\n\n try {\n void Promise.resolve(callback(value)).catch((error) => {\n reportEveLifecycleCallbackError(name, error);\n });\n } catch (error) {\n reportEveLifecycleCallbackError(name, error);\n }\n};\n\nconst hasRunConfig = (\n runConfig: AppendMessage[\"runConfig\"],\n): runConfig is NonNullable<AppendMessage[\"runConfig\"]> =>\n runConfig?.custom !== undefined && Object.keys(runConfig.custom).length > 0;\n\ntype EveJsonValue =\n | string\n | number\n | boolean\n | null\n | readonly EveJsonValue[]\n | { readonly [key: string]: EveJsonValue };\n\n/**\n * Structural equivalent of eve's `JsonObject`. Eve demoted its public send\n * payload type once already (`SendTurnPayload`, public through 0.30, internal\n * from 0.31), so the helpers declare the shapes they forward instead of\n * re-exporting eve's option types, and assignability is checked at the send\n * call sites against the installed version.\n */\ntype EveClientContext = { readonly [key: string]: EveJsonValue };\n\n/**\n * Only the `custom` bag crosses the wire. Eve reads `clientContext` as its own\n * namespace and serializes it into a model-visible context message, so sending\n * the assistant-ui envelope would surface a literal `\"custom\"` key in the\n * prompt and to every eve-side handler.\n */\nconst toEveSendOptions = (\n runConfig: AppendMessage[\"runConfig\"],\n): { readonly clientContext: EveClientContext } | undefined =>\n hasRunConfig(runConfig)\n ? { clientContext: runConfig.custom as EveClientContext }\n : undefined;\n\nexport type UseEveAgentRuntimeOptions = Omit<\n UseEveAgentOptions<EveMessageData>,\n \"reducer\"\n> &\n ExternalStoreSharedOptions & {\n readonly adapters?:\n | {\n readonly attachments?: AttachmentAdapter | undefined;\n readonly speech?: SpeechSynthesisAdapter | undefined;\n readonly dictation?: DictationAdapter | undefined;\n readonly voice?: RealtimeVoiceAdapter | undefined;\n readonly feedback?: FeedbackAdapter | undefined;\n }\n | undefined;\n };\n\n/**\n * Connects Eve's `useEveAgent` hook to assistant-ui's runtime contract.\n *\n * The runtime renders Eve messages, forwards new user messages to the Eve\n * session, supports cancellation, and maps Eve input requests to assistant-ui\n * tool approval UI.\n */\nexport const useEveAgentRuntime = (options: UseEveAgentRuntimeOptions = {}) => {\n const {\n adapters,\n isDisabled: _isDisabled,\n isSendDisabled: _isSendDisabled,\n suggestions: _suggestions,\n unstable_capabilities: _unstable_capabilities,\n ...agentOptions\n } = options;\n true satisfies keyof typeof agentOptions &\n keyof ExternalStoreSharedOptions extends never\n ? true\n : never;\n\n const { onError, onEvent, onFinish, onSessionChange } = agentOptions;\n const lastFinishStatusRef = useRef<UseEveAgentStatus | null>(null);\n const agent = useEveAgent({\n ...agentOptions,\n ...(onError\n ? {\n onError: (error) =>\n invokeEveLifecycleCallback(\"onError\", onError, error),\n }\n : {}),\n ...(onEvent\n ? {\n onEvent: (event) =>\n invokeEveLifecycleCallback(\"onEvent\", onEvent, event),\n }\n : {}),\n onFinish: (snapshot) => {\n lastFinishStatusRef.current = snapshot.status;\n invokeEveLifecycleCallback(\"onFinish\", onFinish, snapshot);\n },\n ...(onSessionChange\n ? {\n onSessionChange: (session) =>\n invokeEveLifecycleCallback(\n \"onSessionChange\",\n onSessionChange,\n session,\n ),\n }\n : {}),\n });\n const runtimeAdapters = useRuntimeAdapters();\n const [toolStatuses, setToolStatuses] = useState<\n Record<string, ToolExecutionStatus>\n >({});\n const [stagedMessages, setStagedMessages] = useState<ThreadMessage[] | null>(\n null,\n );\n const createdAtByMessageIdRef = useRef(new Map<string, Date>());\n const stagedInputsRef = useRef(\n new Map<\n string,\n { message: AppendMessage; runConfig: AppendMessage[\"runConfig\"] }\n >(),\n );\n\n const hasExecutingTools = Object.values(toolStatuses).some(\n (status) => status?.type === \"executing\",\n );\n const providerIsRunning =\n agent.status === \"submitted\" || agent.status === \"streaming\";\n const isRunning = providerIsRunning || hasExecutingTools;\n\n const convertedMessages = useMemo(() => {\n const createdAtByMessageId = createdAtByMessageIdRef.current;\n const messageIds = new Set(\n agent.data.messages.map((message) => message.id),\n );\n for (const messageId of createdAtByMessageId.keys()) {\n if (!messageIds.has(messageId)) createdAtByMessageId.delete(messageId);\n }\n\n return convertEveMessages(agent.data, {\n isRunning,\n error: agent.error,\n getCreatedAt: (message) => {\n const existing = createdAtByMessageId.get(message.id);\n if (existing) return existing;\n\n const createdAt = new Date();\n createdAtByMessageId.set(message.id, createdAt);\n return createdAt;\n },\n });\n }, [agent.data, agent.error, isRunning]);\n\n const messages = stagedMessages ?? convertedMessages;\n const messagesRef = useRef(messages);\n const agentRef = useRef(agent);\n // Descendant layout effects can dispatch against these refs synchronously;\n // this matches useA2ARuntime's commit-scoped ref publication.\n useInsertionEffect(() => {\n messagesRef.current = messages;\n agentRef.current = agent;\n }, [agent, messages]);\n\n // Upstream `EveAgentStore` `send` and `respond` reject while a turn is in\n // flight and only resolve once the turn's stream parks, so a pending chain\n // link is exactly an active turn; chaining every dispatch serializes them\n // without watching status.\n const sendChainRef = useRef<Promise<void>>(Promise.resolve());\n const sendEpochRef = useRef(0);\n // A cancel drops the queued send but keeps its draft for a later promotion;\n // only a reset discards the draft with the session, so the two need separate\n // counters. A queued read has no draft and outlives a cancel, so it waits on\n // the reset counter instead.\n const resetEpochRef = useRef(0);\n const isMountedRef = useRef(true);\n const runtimeRef = useRef<ReturnType<typeof useExternalStoreRuntime> | null>(\n null,\n );\n\n const enqueueSend = (\n dispatch: () => Promise<void>,\n epochRef: { current: number } = sendEpochRef,\n ) => {\n const epoch = epochRef.current;\n const next = sendChainRef.current.then(() => {\n if (!isMountedRef.current) throw sendAbandonedError;\n if (epoch !== epochRef.current) throw sendCancelledError;\n return dispatch();\n });\n sendChainRef.current = next.catch(() => {});\n return next;\n };\n\n // The store outlives the component (useEveAgent holds it in a ref with no\n // cleanup), so queued sends must not fire server turns after unmount. The\n // flag separates that teardown from a user cancel, and is re-armed in setup\n // for a remounted tree.\n useEffect(() => {\n isMountedRef.current = true;\n return () => {\n isMountedRef.current = false;\n sendEpochRef.current += 1;\n };\n }, []);\n\n useEffect(() => {\n if (stagedInputsRef.current.size === 0) return;\n const baseIds = new Set(convertedMessages.map((message) => message.id));\n const remaining = messagesRef.current.filter(\n (message) =>\n stagedInputsRef.current.has(message.id) && !baseIds.has(message.id),\n );\n setStagedMessages(\n remaining.length > 0 ? [...convertedMessages, ...remaining] : null,\n );\n }, [convertedMessages]);\n\n const getStagedRun = (parentId: string | null) => {\n if (!parentId || !stagedInputsRef.current.has(parentId)) return null;\n const staged: {\n message: ThreadMessage;\n input: { message: AppendMessage; runConfig: AppendMessage[\"runConfig\"] };\n }[] = [];\n for (const message of messagesRef.current) {\n const input = stagedInputsRef.current.get(message.id);\n if (input) staged.push({ message, input });\n if (message.id === parentId) break;\n }\n return staged;\n };\n\n const stageUserMessage = (message: AppendMessage) => {\n const threadMessage = fromThreadMessageLike(\n message,\n generateId(),\n USER_STAGED_STATUS,\n );\n stagedInputsRef.current.set(threadMessage.id, {\n message,\n runConfig: message.runConfig,\n });\n const nextMessages = [...messagesRef.current, threadMessage];\n messagesRef.current = nextMessages;\n setStagedMessages(nextMessages);\n };\n\n const reset = useCallback(() => {\n runtimeRef.current?.thread.unstable_notifySessionReset();\n // Sends parked behind an active turn captured the pre-reset epoch, so the\n // epoch has to advance before the session is torn down or they dispatch\n // into the new one; `lastFinishStatusRef` is run-scoped for the same\n // reason.\n sendEpochRef.current += 1;\n resetEpochRef.current += 1;\n lastFinishStatusRef.current = null;\n setStagedMessages(null);\n stagedInputsRef.current.clear();\n setToolStatuses({});\n agent.reset();\n }, [agent]);\n\n const extras = useMemo(\n () =>\n eveExtras.provide({\n error: agent.error,\n events: agent.events,\n session: agent.session,\n reset,\n }),\n [agent.error, agent.events, agent.session, reset],\n );\n\n const runtime = useExternalStoreRuntime({\n ...pickExternalStoreSharedOptions(options),\n messages,\n isRunning: providerIsRunning,\n extras,\n unstable_enableToolInvocations: true,\n setToolStatuses,\n adapters: {\n attachments: adapters?.attachments ?? runtimeAdapters?.attachments,\n speech: adapters?.speech,\n dictation: adapters?.dictation,\n voice: adapters?.voice,\n feedback: adapters?.feedback,\n },\n onNew: async (message) => {\n if (!(message.startRun ?? message.role === \"user\")) {\n stageUserMessage(message);\n return;\n }\n try {\n await enqueueSend(() =>\n agent.send(\n getEveMessageContent(message),\n toEveSendOptions(message.runConfig),\n ),\n );\n } catch (error) {\n // A cancelled send never reached the session, so it rethrows for the\n // composer to take the draft back; an unmounted one has no composer\n // left to restore.\n if (error === sendAbandonedError) return;\n throw error;\n }\n },\n ...(stagedMessages\n ? {\n onReload: async (parentId: string | null, config) => {\n const stagedRun = getStagedRun(parentId);\n if (!stagedRun)\n throw new Error(\"Runtime does not support reloading messages.\");\n const epoch = sendEpochRef.current;\n const resetEpoch = resetEpochRef.current;\n for (const { message: stagedMessage, input } of stagedRun) {\n if (epoch !== sendEpochRef.current) return;\n const previousMessages = messagesRef.current;\n stagedInputsRef.current.delete(stagedMessage.id);\n const nextMessages = previousMessages.filter(\n (message) => message.id !== stagedMessage.id,\n );\n messagesRef.current = nextMessages;\n setStagedMessages(\n stagedInputsRef.current.size > 0 ? nextMessages : null,\n );\n // The reload config belongs to the message being reloaded; the\n // drafts promoted ahead of it keep the config they were staged\n // with, or reloading the tail would rewrite their context too.\n const runConfig =\n stagedMessage.id === parentId && hasRunConfig(config.runConfig)\n ? config.runConfig\n : input.runConfig;\n try {\n await enqueueSend(() =>\n agent.send(\n getEveMessageContent(input.message),\n toEveSendOptions(runConfig),\n ),\n );\n } catch (error) {\n // A reset discarded the draft along with the session, so\n // restoring it here would refloat a message the thread no\n // longer has. A cancel still restores.\n if (resetEpoch !== resetEpochRef.current) return;\n stagedInputsRef.current.set(stagedMessage.id, input);\n messagesRef.current = previousMessages;\n setStagedMessages(previousMessages);\n if (isDroppedSend(error)) return;\n throw error;\n }\n if (lastFinishStatusRef.current === \"error\") return;\n }\n },\n }\n : {}),\n onCancel: async () => {\n sendEpochRef.current += 1;\n // Eve 0.38 replaced the binding's local-abort `stop()` with the durable\n // `cancel()`, so the adapter detects which side of that break the host's\n // eve provides instead of pinning the peer range to one of them.\n const controls = agent as\n | { readonly cancel: () => Promise<unknown> }\n | { readonly stop: () => void };\n if (\"cancel\" in controls) {\n await controls.cancel();\n } else {\n controls.stop();\n }\n },\n // Hosts below eve 0.44.1 expose no `resume`; leaving the capability absent\n // keeps `threads.reloadMainThread()` on core's no-capability no-op.\n ...(\"resume\" in agent\n ? {\n onRefetchThread: () =>\n // `resume()` rejects during a turn, so the replay rides the send\n // chain like every other dispatch and runs once the turn parks; a\n // cancel of that turn does not drop it, or the caller would be told\n // the thread was refetched when it was not. Upstream shares one\n // replay across concurrent `resume()` calls, so a refetch\n // dispatched while `resume: true` is replaying on mount joins that\n // read instead of taking a fresh one. The session is read at\n // dispatch time because nothing durable exists before the first\n // send lands.\n enqueueSend(async () => {\n const live = agentRef.current;\n if (live.session === undefined) return;\n await live.resume();\n }, resetEpochRef).catch((error) => {\n if (isDroppedSend(error)) return;\n throw error;\n }),\n }\n : {}),\n onRespondToToolApproval: (response) => {\n // Eve resolves a request the moment any response for it arrives, and an\n // empty one is recorded as an answer with no content. Mapping before the\n // send is enqueued keeps an unmappable response unsent, so the request\n // stays answerable; the mapper's error reaches the caller as the seam's\n // rejection.\n const inputResponse = toEveInputResponse(\n response,\n findEveInputRequest(agent.data, response.approvalId),\n );\n return enqueueSend(() => agent.respond([inputResponse])).catch(\n (error) => {\n if (!isDroppedSend(error)) throw error;\n },\n );\n },\n });\n runtimeRef.current = runtime;\n\n return runtime;\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"useEveAgentRuntime.js","names":[],"sources":["../src/useEveAgentRuntime.ts"],"sourcesContent":["\"use client\";\n\nimport {\n useCallback,\n useEffect,\n useInsertionEffect,\n useMemo,\n useRef,\n useState,\n} from \"react\";\nimport {\n fromThreadMessageLike,\n generateId,\n MessageNotSentError,\n pickExternalStoreSharedOptions,\n type AppendMessage,\n type AttachmentAdapter,\n type DictationAdapter,\n type ExternalStoreSharedOptions,\n type FeedbackAdapter,\n type RealtimeVoiceAdapter,\n type SpeechSynthesisAdapter,\n type ThreadMessage,\n type ToolExecutionStatus,\n} from \"@assistant-ui/core\";\nimport {\n useExternalStoreRuntime,\n useRuntimeAdapters,\n} from \"@assistant-ui/core/react\";\nimport {\n useEveAgent,\n type EveMessageData,\n type UseEveAgentOptions,\n type UseEveAgentStatus,\n} from \"eve/react\";\nimport {\n convertEveMessages,\n findEveInputRequest,\n getEveMessageContent,\n toEveInputResponse,\n} from \"./convertEveMessages\";\nimport {\n collectTurnTimestamps,\n createTurnTimestampCache,\n} from \"./deriveCreatedAt\";\nimport { eveExtras } from \"./eveExtras\";\n\nconst USER_STAGED_STATUS = {\n type: \"complete\",\n reason: \"unknown\",\n} as const;\n\nconst sendCancelledError = new MessageNotSentError(\n \"eve send was dropped because the run was cancelled.\",\n);\n\nconst sendAbandonedError = new Error(\n \"eve send was dropped because the runtime unmounted.\",\n);\n\nconst isDroppedSend = (error: unknown) =>\n error === sendCancelledError || error === sendAbandonedError;\n\ntype EveLifecycleCallbackName =\n | \"onError\"\n | \"onEvent\"\n | \"onFinish\"\n | \"onSessionChange\";\n\nconst reportEveLifecycleCallbackError = (\n name: EveLifecycleCallbackName,\n error: unknown,\n) => {\n console.error(`[assistant-ui/eve] ${name} callback threw an error`, error);\n};\n\nconst invokeEveLifecycleCallback = <T>(\n name: EveLifecycleCallbackName,\n callback: ((value: T) => unknown) | undefined,\n value: T,\n) => {\n if (!callback) return;\n\n try {\n void Promise.resolve(callback(value)).catch((error) => {\n reportEveLifecycleCallbackError(name, error);\n });\n } catch (error) {\n reportEveLifecycleCallbackError(name, error);\n }\n};\n\nconst hasRunConfig = (\n runConfig: AppendMessage[\"runConfig\"],\n): runConfig is NonNullable<AppendMessage[\"runConfig\"]> =>\n runConfig?.custom !== undefined && Object.keys(runConfig.custom).length > 0;\n\ntype EveJsonValue =\n | string\n | number\n | boolean\n | null\n | readonly EveJsonValue[]\n | { readonly [key: string]: EveJsonValue };\n\n/**\n * Structural equivalent of eve's `JsonObject`. Eve demoted its public send\n * payload type once already (`SendTurnPayload`, public through 0.30, internal\n * from 0.31), so the helpers declare the shapes they forward instead of\n * re-exporting eve's option types, and assignability is checked at the send\n * call sites against the installed version.\n */\ntype EveClientContext = { readonly [key: string]: EveJsonValue };\n\n/**\n * Only the `custom` bag crosses the wire. Eve reads `clientContext` as its own\n * namespace and serializes it into a model-visible context message, so sending\n * the assistant-ui envelope would surface a literal `\"custom\"` key in the\n * prompt and to every eve-side handler.\n */\nconst toEveSendOptions = (\n runConfig: AppendMessage[\"runConfig\"],\n): { readonly clientContext: EveClientContext } | undefined =>\n hasRunConfig(runConfig)\n ? { clientContext: runConfig.custom as EveClientContext }\n : undefined;\n\nexport type UseEveAgentRuntimeOptions = Omit<\n UseEveAgentOptions<EveMessageData>,\n \"reducer\"\n> &\n ExternalStoreSharedOptions & {\n readonly adapters?:\n | {\n readonly attachments?: AttachmentAdapter | undefined;\n readonly speech?: SpeechSynthesisAdapter | undefined;\n readonly dictation?: DictationAdapter | undefined;\n readonly voice?: RealtimeVoiceAdapter | undefined;\n readonly feedback?: FeedbackAdapter | undefined;\n }\n | undefined;\n };\n\n/**\n * Connects Eve's `useEveAgent` hook to assistant-ui's runtime contract.\n *\n * The runtime renders Eve messages, forwards new user messages to the Eve\n * session, supports cancellation, and maps Eve input requests to assistant-ui\n * tool approval UI.\n */\nexport const useEveAgentRuntime = (options: UseEveAgentRuntimeOptions = {}) => {\n const {\n adapters,\n isDisabled: _isDisabled,\n isSendDisabled: _isSendDisabled,\n suggestions: _suggestions,\n unstable_capabilities: _unstable_capabilities,\n ...agentOptions\n } = options;\n true satisfies keyof typeof agentOptions &\n keyof ExternalStoreSharedOptions extends never\n ? true\n : never;\n\n const { onError, onEvent, onFinish, onSessionChange } = agentOptions;\n const lastFinishStatusRef = useRef<UseEveAgentStatus | null>(null);\n const agent = useEveAgent({\n ...agentOptions,\n ...(onError\n ? {\n onError: (error) =>\n invokeEveLifecycleCallback(\"onError\", onError, error),\n }\n : {}),\n ...(onEvent\n ? {\n onEvent: (event) =>\n invokeEveLifecycleCallback(\"onEvent\", onEvent, event),\n }\n : {}),\n onFinish: (snapshot) => {\n lastFinishStatusRef.current = snapshot.status;\n invokeEveLifecycleCallback(\"onFinish\", onFinish, snapshot);\n },\n ...(onSessionChange\n ? {\n onSessionChange: (session) =>\n invokeEveLifecycleCallback(\n \"onSessionChange\",\n onSessionChange,\n session,\n ),\n }\n : {}),\n });\n const runtimeAdapters = useRuntimeAdapters();\n const [toolStatuses, setToolStatuses] = useState<\n Record<string, ToolExecutionStatus>\n >({});\n const [stagedMessages, setStagedMessages] = useState<ThreadMessage[] | null>(\n null,\n );\n const createdAtByMessageIdRef = useRef(new Map<string, Date>());\n const stagedInputsRef = useRef(\n new Map<\n string,\n { message: AppendMessage; runConfig: AppendMessage[\"runConfig\"] }\n >(),\n );\n\n const hasExecutingTools = Object.values(toolStatuses).some(\n (status) => status?.type === \"executing\",\n );\n const providerIsRunning =\n agent.status === \"submitted\" || agent.status === \"streaming\";\n const isRunning = providerIsRunning || hasExecutingTools;\n\n // Kept apart from the message memo so events that teach no timestamp keep\n // the map identity and skip rebuilding every ThreadMessage.\n const turnTimestampCacheRef = useRef(createTurnTimestampCache());\n const turnTimestamps = useMemo(\n () => collectTurnTimestamps(agent.events, turnTimestampCacheRef.current),\n [agent.events],\n );\n\n const convertedMessages = useMemo(() => {\n const createdAtByMessageId = createdAtByMessageIdRef.current;\n const messageIds = new Set(\n agent.data.messages.map((message) => message.id),\n );\n for (const messageId of createdAtByMessageId.keys()) {\n if (!messageIds.has(messageId)) createdAtByMessageId.delete(messageId);\n }\n\n return convertEveMessages(agent.data, {\n isRunning,\n error: agent.error,\n getCreatedAt: (message) => {\n const turnId = message.metadata?.turnId;\n const durable =\n turnId === undefined\n ? undefined\n : turnTimestamps.get(turnId)?.[message.role];\n if (durable !== undefined) return durable;\n\n const existing = createdAtByMessageId.get(message.id);\n if (existing) return existing;\n\n const createdAt = new Date();\n createdAtByMessageId.set(message.id, createdAt);\n return createdAt;\n },\n });\n }, [agent.data, agent.error, isRunning, turnTimestamps]);\n\n const messages = stagedMessages ?? convertedMessages;\n const messagesRef = useRef(messages);\n const agentRef = useRef(agent);\n // Descendant layout effects can dispatch against these refs synchronously;\n // this matches useA2ARuntime's commit-scoped ref publication.\n useInsertionEffect(() => {\n messagesRef.current = messages;\n agentRef.current = agent;\n }, [agent, messages]);\n\n // Upstream `EveAgentStore` `send` and `respond` reject while a turn is in\n // flight and only resolve once the turn's stream parks, so a pending chain\n // link is exactly an active turn; chaining every dispatch serializes them\n // without watching status.\n const sendChainRef = useRef<Promise<void>>(Promise.resolve());\n const sendEpochRef = useRef(0);\n // A cancel drops the queued send but keeps its draft for a later promotion;\n // only a reset discards the draft with the session, so the two need separate\n // counters. A queued read has no draft and outlives a cancel, so it waits on\n // the reset counter instead.\n const resetEpochRef = useRef(0);\n const isMountedRef = useRef(true);\n const runtimeRef = useRef<ReturnType<typeof useExternalStoreRuntime> | null>(\n null,\n );\n\n const enqueueSend = (\n dispatch: () => Promise<void>,\n epochRef: { current: number } = sendEpochRef,\n ) => {\n const epoch = epochRef.current;\n const next = sendChainRef.current.then(() => {\n if (!isMountedRef.current) throw sendAbandonedError;\n if (epoch !== epochRef.current) throw sendCancelledError;\n return dispatch();\n });\n sendChainRef.current = next.catch(() => {});\n return next;\n };\n\n // The store outlives the component (useEveAgent holds it in a ref with no\n // cleanup), so queued sends must not fire server turns after unmount. The\n // flag separates that teardown from a user cancel, and is re-armed in setup\n // for a remounted tree.\n useEffect(() => {\n isMountedRef.current = true;\n return () => {\n isMountedRef.current = false;\n sendEpochRef.current += 1;\n };\n }, []);\n\n useEffect(() => {\n if (stagedInputsRef.current.size === 0) return;\n const baseIds = new Set(convertedMessages.map((message) => message.id));\n const remaining = messagesRef.current.filter(\n (message) =>\n stagedInputsRef.current.has(message.id) && !baseIds.has(message.id),\n );\n setStagedMessages(\n remaining.length > 0 ? [...convertedMessages, ...remaining] : null,\n );\n }, [convertedMessages]);\n\n const getStagedRun = (parentId: string | null) => {\n if (!parentId || !stagedInputsRef.current.has(parentId)) return null;\n const staged: {\n message: ThreadMessage;\n input: { message: AppendMessage; runConfig: AppendMessage[\"runConfig\"] };\n }[] = [];\n for (const message of messagesRef.current) {\n const input = stagedInputsRef.current.get(message.id);\n if (input) staged.push({ message, input });\n if (message.id === parentId) break;\n }\n return staged;\n };\n\n const stageUserMessage = (message: AppendMessage) => {\n const threadMessage = fromThreadMessageLike(\n message,\n generateId(),\n USER_STAGED_STATUS,\n );\n stagedInputsRef.current.set(threadMessage.id, {\n message,\n runConfig: message.runConfig,\n });\n const nextMessages = [...messagesRef.current, threadMessage];\n messagesRef.current = nextMessages;\n setStagedMessages(nextMessages);\n };\n\n const reset = useCallback(() => {\n runtimeRef.current?.thread.unstable_notifySessionReset();\n // Sends parked behind an active turn captured the pre-reset epoch, so the\n // epoch has to advance before the session is torn down or they dispatch\n // into the new one; `lastFinishStatusRef` is run-scoped for the same\n // reason.\n sendEpochRef.current += 1;\n resetEpochRef.current += 1;\n lastFinishStatusRef.current = null;\n setStagedMessages(null);\n stagedInputsRef.current.clear();\n setToolStatuses({});\n agent.reset();\n }, [agent]);\n\n const extras = useMemo(\n () =>\n eveExtras.provide({\n error: agent.error,\n events: agent.events,\n session: agent.session,\n reset,\n }),\n [agent.error, agent.events, agent.session, reset],\n );\n\n const runtime = useExternalStoreRuntime({\n ...pickExternalStoreSharedOptions(options),\n messages,\n isRunning: providerIsRunning,\n extras,\n unstable_enableToolInvocations: true,\n setToolStatuses,\n adapters: {\n attachments: adapters?.attachments ?? runtimeAdapters?.attachments,\n speech: adapters?.speech,\n dictation: adapters?.dictation,\n voice: adapters?.voice,\n feedback: adapters?.feedback,\n },\n onNew: async (message) => {\n if (!(message.startRun ?? message.role === \"user\")) {\n stageUserMessage(message);\n return;\n }\n try {\n await enqueueSend(() =>\n agent.send(\n getEveMessageContent(message),\n toEveSendOptions(message.runConfig),\n ),\n );\n } catch (error) {\n // A cancelled send never reached the session, so it rethrows for the\n // composer to take the draft back; an unmounted one has no composer\n // left to restore.\n if (error === sendAbandonedError) return;\n throw error;\n }\n },\n ...(stagedMessages\n ? {\n onReload: async (parentId: string | null, config) => {\n const stagedRun = getStagedRun(parentId);\n if (!stagedRun)\n throw new Error(\"Runtime does not support reloading messages.\");\n const epoch = sendEpochRef.current;\n const resetEpoch = resetEpochRef.current;\n for (const { message: stagedMessage, input } of stagedRun) {\n if (epoch !== sendEpochRef.current) return;\n const previousMessages = messagesRef.current;\n stagedInputsRef.current.delete(stagedMessage.id);\n const nextMessages = previousMessages.filter(\n (message) => message.id !== stagedMessage.id,\n );\n messagesRef.current = nextMessages;\n setStagedMessages(\n stagedInputsRef.current.size > 0 ? nextMessages : null,\n );\n // The reload config belongs to the message being reloaded; the\n // drafts promoted ahead of it keep the config they were staged\n // with, or reloading the tail would rewrite their context too.\n const runConfig =\n stagedMessage.id === parentId && hasRunConfig(config.runConfig)\n ? config.runConfig\n : input.runConfig;\n try {\n await enqueueSend(() =>\n agent.send(\n getEveMessageContent(input.message),\n toEveSendOptions(runConfig),\n ),\n );\n } catch (error) {\n // A reset discarded the draft along with the session, so\n // restoring it here would refloat a message the thread no\n // longer has. A cancel still restores.\n if (resetEpoch !== resetEpochRef.current) return;\n stagedInputsRef.current.set(stagedMessage.id, input);\n messagesRef.current = previousMessages;\n setStagedMessages(previousMessages);\n if (isDroppedSend(error)) return;\n throw error;\n }\n if (lastFinishStatusRef.current === \"error\") return;\n }\n },\n }\n : {}),\n onCancel: async () => {\n sendEpochRef.current += 1;\n // Eve 0.38 replaced the binding's local-abort `stop()` with the durable\n // `cancel()`, so the adapter detects which side of that break the host's\n // eve provides instead of pinning the peer range to one of them.\n const controls = agent as\n | { readonly cancel: () => Promise<unknown> }\n | { readonly stop: () => void };\n if (\"cancel\" in controls) {\n await controls.cancel();\n } else {\n controls.stop();\n }\n },\n // Hosts below eve 0.44.1 expose no `resume`; leaving the capability absent\n // keeps `threads.reloadMainThread()` on core's no-capability no-op.\n ...(\"resume\" in agent\n ? {\n onRefetchThread: () =>\n // `resume()` rejects during a turn, so the replay rides the send\n // chain like every other dispatch and runs once the turn parks; a\n // cancel of that turn does not drop it, or the caller would be told\n // the thread was refetched when it was not. Upstream shares one\n // replay across concurrent `resume()` calls, so a refetch\n // dispatched while `resume: true` is replaying on mount joins that\n // read instead of taking a fresh one. The session is read at\n // dispatch time because nothing durable exists before the first\n // send lands.\n enqueueSend(async () => {\n const live = agentRef.current;\n if (live.session === undefined) return;\n await live.resume();\n }, resetEpochRef).catch((error) => {\n if (isDroppedSend(error)) return;\n throw error;\n }),\n }\n : {}),\n onRespondToToolApproval: (response) => {\n // Eve resolves a request the moment any response for it arrives, and an\n // empty one is recorded as an answer with no content. Mapping before the\n // send is enqueued keeps an unmappable response unsent, so the request\n // stays answerable; the mapper's error reaches the caller as the seam's\n // rejection.\n const inputResponse = toEveInputResponse(\n response,\n findEveInputRequest(agent.data, response.approvalId),\n );\n return enqueueSend(() => agent.respond([inputResponse])).catch(\n (error) => {\n if (!isDroppedSend(error)) throw error;\n },\n );\n },\n });\n runtimeRef.current = runtime;\n\n return runtime;\n};\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@assistant-ui/eve",
3
- "version": "0.0.16",
3
+ "version": "0.0.17",
4
4
  "description": "Eve runtime adapter for assistant-ui",
5
5
  "keywords": [
6
6
  "eve",
@@ -28,8 +28,8 @@
28
28
  ],
29
29
  "sideEffects": false,
30
30
  "dependencies": {
31
- "@assistant-ui/core": "^0.3.17",
32
- "@assistant-ui/store": "^0.3.12"
31
+ "@assistant-ui/core": "^0.3.18",
32
+ "@assistant-ui/store": "^0.3.13"
33
33
  },
34
34
  "peerDependencies": {
35
35
  "@types/react": "*",
@@ -42,11 +42,11 @@
42
42
  }
43
43
  },
44
44
  "devDependencies": {
45
- "@assistant-ui/x-buildutils": "0.0.25",
45
+ "@assistant-ui/x-buildutils": "0.0.26",
46
46
  "@testing-library/dom": "^10.4.1",
47
47
  "@testing-library/react": "^16.3.3",
48
48
  "@types/react": "^19.2.18",
49
- "eve": "^0.47.6",
49
+ "eve": "^0.52.2",
50
50
  "jsdom": "^30.0.1",
51
51
  "react": "^19.2.8",
52
52
  "react-dom": "^19.2.8",
@@ -0,0 +1,190 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import type { MessageStreamEvent } from "eve/client";
3
+
4
+ import {
5
+ collectTurnTimestamps,
6
+ createTurnTimestampCache,
7
+ } from "./deriveCreatedAt";
8
+
9
+ const TURN = "turn_1";
10
+
11
+ const turnStarted = (at: string, turnId = TURN) =>
12
+ ({
13
+ type: "turn.started",
14
+ data: { sequence: 1, turnId },
15
+ meta: { at, id: `evt_${at}` },
16
+ }) as const satisfies MessageStreamEvent;
17
+
18
+ const messageReceived = (at: string, turnId = TURN) =>
19
+ ({
20
+ type: "message.received",
21
+ data: { message: "hi", sequence: 2, turnId },
22
+ meta: { at, id: `evt_${at}` },
23
+ }) as const satisfies MessageStreamEvent;
24
+
25
+ const compactionRequested = (at: string, turnId = TURN) =>
26
+ ({
27
+ type: "compaction.requested",
28
+ data: {
29
+ modelId: "m",
30
+ sequence: 3,
31
+ sessionId: "s",
32
+ turnId,
33
+ usageInputTokens: null,
34
+ },
35
+ meta: { at, id: `evt_${at}` },
36
+ }) as const satisfies MessageStreamEvent;
37
+
38
+ const messageAppended = (at: string, turnId = TURN) =>
39
+ ({
40
+ type: "message.appended",
41
+ data: {
42
+ messageDelta: "he",
43
+ messageSoFar: "he",
44
+ sequence: 4,
45
+ stepIndex: 0,
46
+ turnId,
47
+ },
48
+ meta: { at, id: `evt_${at}` },
49
+ }) as const satisfies MessageStreamEvent;
50
+
51
+ const stepStarted = (at: string, turnId = TURN) =>
52
+ ({
53
+ type: "step.started",
54
+ data: { modelId: "m", sequence: 4, stepIndex: 0, turnId },
55
+ meta: { at, id: `evt_${at}` },
56
+ }) as const satisfies MessageStreamEvent;
57
+
58
+ describe("collectTurnTimestamps", () => {
59
+ it("stamps the user at message.received and the assistant at step.started, skipping the turn's other events", () => {
60
+ const timestamps = collectTurnTimestamps(
61
+ [
62
+ turnStarted("2026-01-02T10:00:00.000Z"),
63
+ messageReceived("2026-01-02T10:00:01.000Z"),
64
+ compactionRequested("2026-01-02T10:00:02.000Z"),
65
+ stepStarted("2026-01-02T10:02:00.000Z"),
66
+ stepStarted("2026-01-02T10:03:00.000Z"),
67
+ ],
68
+ createTurnTimestampCache(),
69
+ );
70
+
71
+ expect(timestamps.get(TURN)?.user).toEqual(
72
+ new Date("2026-01-02T10:00:01.000Z"),
73
+ );
74
+ expect(timestamps.get(TURN)?.assistant).toEqual(
75
+ new Date("2026-01-02T10:02:00.000Z"),
76
+ );
77
+ expect(timestamps.get("turn_other")?.user).toBe(undefined);
78
+ });
79
+
80
+ it("resumes the scan at appended events and keeps the map identity when they teach nothing", () => {
81
+ const cache = createTurnTimestampCache();
82
+ const events = [
83
+ turnStarted("2026-01-02T10:00:00.000Z"),
84
+ messageReceived("2026-01-02T10:00:01.000Z"),
85
+ ];
86
+ const first = collectTurnTimestamps(events, cache);
87
+ expect(first.get(TURN)?.assistant).toBe(undefined);
88
+
89
+ const quiet = [
90
+ ...events,
91
+ turnStarted("2026-01-02T10:00:02.000Z", "turn_2"),
92
+ ];
93
+ expect(collectTurnTimestamps(quiet, cache)).toBe(first);
94
+
95
+ const timestamps = collectTurnTimestamps(
96
+ [...quiet, stepStarted("2026-01-02T10:02:00.000Z")],
97
+ cache,
98
+ );
99
+ expect(timestamps.get(TURN)?.user).toEqual(
100
+ new Date("2026-01-02T10:00:01.000Z"),
101
+ );
102
+ expect(timestamps.get(TURN)?.assistant).toEqual(
103
+ new Date("2026-01-02T10:02:00.000Z"),
104
+ );
105
+ });
106
+
107
+ it("stamps an assistant created by a turn ending before its first step", () => {
108
+ const timestamps = collectTurnTimestamps(
109
+ [
110
+ messageReceived("2026-01-02T10:00:01.000Z"),
111
+ {
112
+ type: "turn.cancelled",
113
+ data: { sequence: 3, turnId: TURN },
114
+ meta: { at: "2026-01-02T10:00:05.000Z", id: "evt_cancel" },
115
+ },
116
+ ],
117
+ createTurnTimestampCache(),
118
+ );
119
+
120
+ expect(timestamps.get(TURN)?.assistant).toEqual(
121
+ new Date("2026-01-02T10:00:05.000Z"),
122
+ );
123
+ });
124
+
125
+ it("stamps the assistant from the turn's first message-creating event when no step.started precedes it", () => {
126
+ const timestamps = collectTurnTimestamps(
127
+ [
128
+ turnStarted("2026-01-02T10:00:00.000Z"),
129
+ messageReceived("2026-01-02T10:00:01.000Z"),
130
+ compactionRequested("2026-01-02T10:00:02.000Z"),
131
+ messageAppended("2026-01-02T10:02:00.000Z"),
132
+ ],
133
+ createTurnTimestampCache(),
134
+ );
135
+
136
+ expect(timestamps.get(TURN)?.assistant).toEqual(
137
+ new Date("2026-01-02T10:02:00.000Z"),
138
+ );
139
+ });
140
+
141
+ it("skips malformed events without throwing", () => {
142
+ const events = [
143
+ { type: "message.received", data: null, meta: { at: "x", id: "e1" } },
144
+ { type: "step.started", meta: { at: "2026-01-01T00:00:00Z", id: "e2" } },
145
+ {
146
+ type: "message.received",
147
+ data: { turnId: 42 },
148
+ meta: { at: "x", id: "e3" },
149
+ },
150
+ {
151
+ type: "message.received",
152
+ data: { message: "hi", sequence: 2, turnId: TURN },
153
+ meta: { at: "not a date", id: "e4" },
154
+ },
155
+ { type: "step.started", data: { stepIndex: 0, turnId: TURN } },
156
+ stepStarted("2026-01-02T10:02:00.000Z"),
157
+ ] as never as readonly MessageStreamEvent[];
158
+
159
+ const timestamps = collectTurnTimestamps(
160
+ events,
161
+ createTurnTimestampCache(),
162
+ );
163
+
164
+ expect(timestamps.get(TURN)?.user).toBe(undefined);
165
+ expect(timestamps.get(TURN)?.assistant).toEqual(
166
+ new Date("2026-01-02T10:02:00.000Z"),
167
+ );
168
+ });
169
+
170
+ it("re-derives from scratch when the log is replaced, so a recurring turn id serves the new session's stamps", () => {
171
+ const cache = createTurnTimestampCache();
172
+ collectTurnTimestamps(
173
+ [
174
+ messageReceived("2026-01-02T10:00:01.000Z"),
175
+ stepStarted("2026-01-02T10:02:00.000Z"),
176
+ ],
177
+ cache,
178
+ );
179
+
180
+ const timestamps = collectTurnTimestamps(
181
+ [messageReceived("2026-03-01T09:00:01.000Z")],
182
+ cache,
183
+ );
184
+
185
+ expect(timestamps.get(TURN)?.user).toEqual(
186
+ new Date("2026-03-01T09:00:01.000Z"),
187
+ );
188
+ expect(timestamps.get(TURN)?.assistant).toBe(undefined);
189
+ });
190
+ });
@@ -0,0 +1,96 @@
1
+ import type { MessageStreamEvent } from "eve/client";
2
+
3
+ export type TurnTimestamps = {
4
+ readonly user?: Date;
5
+ readonly assistant?: Date;
6
+ };
7
+
8
+ export type TurnTimestampCache = {
9
+ lastEvents: readonly MessageStreamEvent[];
10
+ timestamps: ReadonlyMap<string, TurnTimestamps>;
11
+ };
12
+
13
+ /**
14
+ * The reducer cases eve projects into a message: `message.received` creates
15
+ * `${turnId}:user`, and every assistant entry reaches `updateAssistantMessage`,
16
+ * which creates `${turnId}:assistant` for a turn that has none. It enumerates
17
+ * that reducer across the `>=0.32.0` peer range rather than stating a rule
18
+ * about turn ids, because `turn.started` and the compaction events carry one
19
+ * before the model runs. An event eve adds to the creating set later is absent
20
+ * here and falls back to the wall clock instead of stamping a wrong time.
21
+ */
22
+ const ROLE_BY_EVENT_TYPE: Partial<
23
+ Record<MessageStreamEvent["type"], keyof TurnTimestamps>
24
+ > = {
25
+ "message.received": "user",
26
+ "step.started": "assistant",
27
+ "reasoning.appended": "assistant",
28
+ "reasoning.completed": "assistant",
29
+ "action.input.appended": "assistant",
30
+ "actions.requested": "assistant",
31
+ "input.requested": "assistant",
32
+ "action.result": "assistant",
33
+ "action.partial": "assistant",
34
+ "authorization.required": "assistant",
35
+ "authorization.completed": "assistant",
36
+ "message.appended": "assistant",
37
+ "message.completed": "assistant",
38
+ "result.completed": "assistant",
39
+ "turn.completed": "assistant",
40
+ "turn.cancelled": "assistant",
41
+ };
42
+
43
+ export const createTurnTimestampCache = (): TurnTimestampCache => ({
44
+ lastEvents: [],
45
+ timestamps: new Map(),
46
+ });
47
+
48
+ /**
49
+ * Eve appends to its log (`[...events, event]`), so a snapshot that keeps the
50
+ * previous scan's last element by identity shares the whole prefix and the
51
+ * scan resumes there. Any other snapshot is re-derived into a fresh map: turn
52
+ * ids are per-session sequence numbers that recur after `reset()`, so entries
53
+ * must not outlive the log that produced them. Re-derivation is also what
54
+ * makes the render-phase cache writes safe when React discards a render.
55
+ */
56
+ export const collectTurnTimestamps = (
57
+ events: readonly MessageStreamEvent[],
58
+ cache: TurnTimestampCache,
59
+ ): ReadonlyMap<string, TurnTimestamps> => {
60
+ if (events === cache.lastEvents) return cache.timestamps;
61
+
62
+ const scanned = cache.lastEvents;
63
+ const resumesScan =
64
+ scanned.length === 0 ||
65
+ events[scanned.length - 1] === scanned[scanned.length - 1];
66
+
67
+ let timestamps = cache.timestamps;
68
+ let draft: Map<string, TurnTimestamps> | undefined;
69
+ if (!resumesScan) {
70
+ draft = new Map();
71
+ timestamps = draft;
72
+ }
73
+ for (let i = resumesScan ? scanned.length : 0; i < events.length; i++) {
74
+ const event = events[i]!;
75
+ const role = ROLE_BY_EVENT_TYPE[event.type];
76
+ if (role === undefined) continue;
77
+ const data: unknown = (event as { readonly data?: unknown }).data;
78
+ if (typeof data !== "object" || data === null) continue;
79
+ const turnId = (data as { readonly turnId?: unknown }).turnId;
80
+ if (typeof turnId !== "string") continue;
81
+ const known = timestamps.get(turnId);
82
+ if (known?.[role] !== undefined) continue;
83
+ const at: unknown = (event as { readonly meta?: { readonly at?: unknown } })
84
+ .meta?.at;
85
+ if (typeof at !== "string") continue;
86
+ const date = new Date(at);
87
+ if (Number.isNaN(date.getTime())) continue;
88
+ draft ??= new Map(timestamps);
89
+ draft.set(turnId, { ...known, [role]: date });
90
+ timestamps = draft;
91
+ }
92
+
93
+ cache.lastEvents = events;
94
+ cache.timestamps = timestamps;
95
+ return timestamps;
96
+ };
@@ -0,0 +1,80 @@
1
+ // @vitest-environment jsdom
2
+
3
+ // Deliberately does not mock `eve/react`: the real `useEveAgent` resumes an
4
+ // offline session so the durable `meta.at` values travel through eve's own
5
+ // store and reducer before the adapter reads them. Supplying `session` keeps
6
+ // the store from constructing a network client.
7
+
8
+ import { renderHook, waitFor } from "@testing-library/react";
9
+ import { describe, expect, it } from "vitest";
10
+ import type { MessageStreamEvent } from "eve/client";
11
+
12
+ import { useEveAgentRuntime } from "./useEveAgentRuntime";
13
+
14
+ const TURN = "turn_resumed";
15
+
16
+ const USER_AT = "2026-01-02T10:00:01.000Z";
17
+ const ASSISTANT_AT = "2026-01-02T10:02:00.000Z";
18
+
19
+ const resumedEvents = [
20
+ {
21
+ type: "turn.started",
22
+ data: { sequence: 1, turnId: TURN },
23
+ meta: { at: "2026-01-02T10:00:00.000Z", id: "evt_001" },
24
+ },
25
+ {
26
+ type: "message.received",
27
+ data: { message: "hi", sequence: 2, turnId: TURN },
28
+ meta: { at: USER_AT, id: "evt_101" },
29
+ },
30
+ {
31
+ type: "step.started",
32
+ data: { modelId: "test-model", sequence: 3, stepIndex: 0, turnId: TURN },
33
+ meta: { at: ASSISTANT_AT, id: "evt_102" },
34
+ },
35
+ {
36
+ type: "message.completed",
37
+ data: {
38
+ finishReason: "stop",
39
+ message: "hello",
40
+ sequence: 5,
41
+ stepIndex: 0,
42
+ turnId: TURN,
43
+ },
44
+ meta: { at: "2026-01-02T10:02:06.000Z", id: "evt_003" },
45
+ },
46
+ {
47
+ type: "turn.completed",
48
+ data: { sequence: 6, turnId: TURN },
49
+ meta: { at: "2026-01-02T10:02:07.000Z", id: "evt_004" },
50
+ },
51
+ ] as const satisfies readonly MessageStreamEvent[];
52
+
53
+ const offlineSession = {
54
+ state: { sessionId: "session_resumed", streamIndex: 0 },
55
+ cancel: async () => ({ status: "no_active_turn" }),
56
+ send: () => {
57
+ throw new Error("the resume test must not reach the network");
58
+ },
59
+ stream: async function* () {
60
+ yield* JSON.parse(
61
+ JSON.stringify(resumedEvents),
62
+ ) as readonly MessageStreamEvent[];
63
+ },
64
+ } as never;
65
+
66
+ describe("useEveAgentRuntime against a resumed eve session", () => {
67
+ it("renders resumed history at its durable event times, not the current time", async () => {
68
+ const { result } = renderHook(() =>
69
+ useEveAgentRuntime({ resume: true, session: offlineSession }),
70
+ );
71
+
72
+ await waitFor(() =>
73
+ expect(result.current.thread.getState().messages).toHaveLength(2),
74
+ );
75
+
76
+ const messages = result.current.thread.getState().messages;
77
+ expect(messages[0]?.createdAt).toEqual(new Date(USER_AT));
78
+ expect(messages[1]?.createdAt).toEqual(new Date(ASSISTANT_AT));
79
+ });
80
+ });
@@ -18,6 +18,7 @@ vi.mock("eve/react", async (importOriginal) => ({
18
18
  useEveAgent: mockUseEveAgent,
19
19
  }));
20
20
 
21
+ import type { MessageStreamEvent } from "eve/client";
21
22
  import type { EveMessageData } from "eve/react";
22
23
  import { useEveAgentRuntime } from "./useEveAgentRuntime";
23
24
  import { eveExtras } from "./eveExtras";
@@ -2152,3 +2153,88 @@ describe("useEveAgentRuntime thread refetch", () => {
2152
2153
  });
2153
2154
  });
2154
2155
  });
2156
+
2157
+ describe("useEveAgentRuntime createdAt derivation", () => {
2158
+ const TURN = "turn_ts";
2159
+ const USER_AT = "2026-01-02T10:00:01.000Z";
2160
+ const ASSISTANT_AT = "2026-01-02T10:02:00.000Z";
2161
+
2162
+ const turnEvents = [
2163
+ {
2164
+ type: "turn.started",
2165
+ data: { sequence: 1, turnId: TURN },
2166
+ meta: { at: "2026-01-02T10:00:00.000Z", id: "evt_1" },
2167
+ },
2168
+ {
2169
+ type: "message.received",
2170
+ data: { message: "hi", sequence: 2, turnId: TURN },
2171
+ meta: { at: USER_AT, id: "evt_2" },
2172
+ },
2173
+ {
2174
+ type: "step.started",
2175
+ data: { modelId: "m", sequence: 3, stepIndex: 0, turnId: TURN },
2176
+ meta: { at: ASSISTANT_AT, id: "evt_3" },
2177
+ },
2178
+ ] satisfies readonly MessageStreamEvent[];
2179
+
2180
+ const turnData: EveMessageData = {
2181
+ messages: [
2182
+ {
2183
+ id: `${TURN}:user`,
2184
+ role: "user",
2185
+ metadata: { turnId: TURN },
2186
+ parts: [{ type: "text", text: "hi" }],
2187
+ },
2188
+ {
2189
+ id: `${TURN}:assistant`,
2190
+ role: "assistant",
2191
+ metadata: { turnId: TURN },
2192
+ parts: [{ type: "text", text: "hello" }],
2193
+ },
2194
+ ],
2195
+ };
2196
+
2197
+ it("stamps a message with the wall clock when no event covers its turn, and keeps that stamp across renders", () => {
2198
+ const before = Date.now();
2199
+ const agent = createAgent({ data: turnData, events: [] });
2200
+ mockUseEveAgent.mockReturnValue(agent as never);
2201
+
2202
+ const { result, rerender } = renderHook(() => useEveAgentRuntime());
2203
+
2204
+ const stamped = result.current.thread.getState().messages[0]?.createdAt;
2205
+ expect(stamped?.getTime()).toBeGreaterThanOrEqual(before);
2206
+
2207
+ mockUseEveAgent.mockReturnValue({
2208
+ ...agent,
2209
+ data: { messages: [...turnData.messages] },
2210
+ } as never);
2211
+ rerender();
2212
+
2213
+ expect(result.current.thread.getState().messages[0]?.createdAt).toBe(
2214
+ stamped,
2215
+ );
2216
+ });
2217
+
2218
+ it("keeps the message list identity when appended events stamp no message", () => {
2219
+ const agent = createAgent({ data: turnData, events: turnEvents });
2220
+ mockUseEveAgent.mockReturnValue(agent as never);
2221
+
2222
+ const { result, rerender } = renderHook(() => useEveAgentRuntime());
2223
+ const first = result.current.thread.getState().messages;
2224
+
2225
+ mockUseEveAgent.mockReturnValue({
2226
+ ...agent,
2227
+ events: [
2228
+ ...turnEvents,
2229
+ {
2230
+ type: "turn.started",
2231
+ data: { sequence: 4, turnId: "turn_next" },
2232
+ meta: { at: "2026-01-02T10:05:00.000Z", id: "evt_4" },
2233
+ },
2234
+ ],
2235
+ } as never);
2236
+ rerender();
2237
+
2238
+ expect(result.current.thread.getState().messages).toBe(first);
2239
+ });
2240
+ });
@@ -39,6 +39,10 @@ import {
39
39
  getEveMessageContent,
40
40
  toEveInputResponse,
41
41
  } from "./convertEveMessages";
42
+ import {
43
+ collectTurnTimestamps,
44
+ createTurnTimestampCache,
45
+ } from "./deriveCreatedAt";
42
46
  import { eveExtras } from "./eveExtras";
43
47
 
44
48
  const USER_STAGED_STATUS = {
@@ -211,6 +215,14 @@ export const useEveAgentRuntime = (options: UseEveAgentRuntimeOptions = {}) => {
211
215
  agent.status === "submitted" || agent.status === "streaming";
212
216
  const isRunning = providerIsRunning || hasExecutingTools;
213
217
 
218
+ // Kept apart from the message memo so events that teach no timestamp keep
219
+ // the map identity and skip rebuilding every ThreadMessage.
220
+ const turnTimestampCacheRef = useRef(createTurnTimestampCache());
221
+ const turnTimestamps = useMemo(
222
+ () => collectTurnTimestamps(agent.events, turnTimestampCacheRef.current),
223
+ [agent.events],
224
+ );
225
+
214
226
  const convertedMessages = useMemo(() => {
215
227
  const createdAtByMessageId = createdAtByMessageIdRef.current;
216
228
  const messageIds = new Set(
@@ -224,6 +236,13 @@ export const useEveAgentRuntime = (options: UseEveAgentRuntimeOptions = {}) => {
224
236
  isRunning,
225
237
  error: agent.error,
226
238
  getCreatedAt: (message) => {
239
+ const turnId = message.metadata?.turnId;
240
+ const durable =
241
+ turnId === undefined
242
+ ? undefined
243
+ : turnTimestamps.get(turnId)?.[message.role];
244
+ if (durable !== undefined) return durable;
245
+
227
246
  const existing = createdAtByMessageId.get(message.id);
228
247
  if (existing) return existing;
229
248
 
@@ -232,7 +251,7 @@ export const useEveAgentRuntime = (options: UseEveAgentRuntimeOptions = {}) => {
232
251
  return createdAt;
233
252
  },
234
253
  });
235
- }, [agent.data, agent.error, isRunning]);
254
+ }, [agent.data, agent.error, isRunning, turnTimestamps]);
236
255
 
237
256
  const messages = stagedMessages ?? convertedMessages;
238
257
  const messagesRef = useRef(messages);