@assistant-ui/eve 0.0.11 → 0.0.12

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 +1 @@
1
- {"version":3,"file":"useEveAgentRuntime.d.ts","names":[],"sources":["../src/useEveAgentRuntime.ts"],"mappings":";;;KAiEY,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":";;;KA8FY,4BAA4B,KACtC,mBAAmB,8BAGnB;WACW;aAEM,cAAc;aACd,SAAS;aACT,YAAY;aACZ,QAAQ;aACR,WAAW;;;;;;;;;;cAYjB,qBAAsB,UAAS,2DAA8B"}
@@ -10,6 +10,19 @@ const USER_STAGED_STATUS = {
10
10
  reason: "unknown"
11
11
  };
12
12
  const sendCancelledError = /* @__PURE__ */ new Error("eve send was dropped because the run was cancelled.");
13
+ const reportEveLifecycleCallbackError = (name, error) => {
14
+ console.error(`[assistant-ui/eve] ${name} callback threw an error`, error);
15
+ };
16
+ const invokeEveLifecycleCallback = (name, callback, value) => {
17
+ if (!callback) return;
18
+ try {
19
+ Promise.resolve(callback(value)).catch((error) => {
20
+ reportEveLifecycleCallbackError(name, error);
21
+ });
22
+ } catch (error) {
23
+ reportEveLifecycleCallbackError(name, error);
24
+ }
25
+ };
13
26
  const hasRunConfig = (runConfig) => runConfig?.custom !== void 0 && Object.keys(runConfig.custom).length > 0;
14
27
  /**
15
28
  * Only the `custom` bag crosses the wire. Eve reads `clientContext` as its own
@@ -27,14 +40,17 @@ const toEveClientContext = (runConfig) => hasRunConfig(runConfig) ? { clientCont
27
40
  */
28
41
  const useEveAgentRuntime = (options = {}) => {
29
42
  const { adapters, isDisabled: _isDisabled, isSendDisabled: _isSendDisabled, suggestions: _suggestions, unstable_capabilities: _unstable_capabilities, ...agentOptions } = options;
30
- const { onFinish } = agentOptions;
43
+ const { onError, onEvent, onFinish, onSessionChange } = agentOptions;
31
44
  const lastFinishStatusRef = useRef(null);
32
45
  const agent = useEveAgent({
33
46
  ...agentOptions,
47
+ ...onError ? { onError: (error) => invokeEveLifecycleCallback("onError", onError, error) } : {},
48
+ ...onEvent ? { onEvent: (event) => invokeEveLifecycleCallback("onEvent", onEvent, event) } : {},
34
49
  onFinish: (snapshot) => {
35
50
  lastFinishStatusRef.current = snapshot.status;
36
- onFinish?.(snapshot);
37
- }
51
+ invokeEveLifecycleCallback("onFinish", onFinish, snapshot);
52
+ },
53
+ ...onSessionChange ? { onSessionChange: (session) => invokeEveLifecycleCallback("onSessionChange", onSessionChange, session) } : {}
38
54
  });
39
55
  const runtimeAdapters = useRuntimeAdapters();
40
56
  const [toolStatuses, setToolStatuses] = useState({});
@@ -1 +1 @@
1
- {"version":3,"file":"useEveAgentRuntime.js","names":[],"sources":["../src/useEveAgentRuntime.ts"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport {\n fromThreadMessageLike,\n generateId,\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 type { SendTurnPayload } from \"eve/client\";\nimport {\n convertEveMessages,\n getEveMessageContent,\n toEveInputResponse,\n} from \"./convertEveMessages\";\n\nconst USER_STAGED_STATUS = {\n type: \"complete\",\n reason: \"unknown\",\n} as const;\n\nconst sendCancelledError = new Error(\n \"eve send was dropped because the run was cancelled.\",\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\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 toEveClientContext = (\n runConfig: AppendMessage[\"runConfig\"],\n): Pick<SendTurnPayload, \"clientContext\"> =>\n hasRunConfig(runConfig)\n ? {\n clientContext: runConfig.custom as NonNullable<\n SendTurnPayload[\"clientContext\"]\n >,\n }\n : {};\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 { onFinish } = agentOptions;\n const lastFinishStatusRef = useRef<UseEveAgentStatus | null>(null);\n const agent = useEveAgent({\n ...agentOptions,\n onFinish: (snapshot) => {\n lastFinishStatusRef.current = snapshot.status;\n onFinish?.(snapshot);\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 isRunning =\n agent.status === \"submitted\" ||\n agent.status === \"streaming\" ||\n 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 messagesRef.current = messages;\n\n // Upstream `EveAgentStore.send` rejects while a turn is in flight and only\n // resolves once the turn's stream parks, so a pending chain link is exactly\n // an active turn; chaining every send serializes them without watching\n // status.\n const sendChainRef = useRef<Promise<void>>(Promise.resolve());\n const sendEpochRef = useRef(0);\n\n const enqueueSend = (payload: Parameters<typeof agent.send>[0]) => {\n const epoch = sendEpochRef.current;\n const next = sendChainRef.current.then(() => {\n if (epoch !== sendEpochRef.current) throw sendCancelledError;\n return agent.send(payload);\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.\n useEffect(() => {\n return () => {\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 return useExternalStoreRuntime({\n ...pickExternalStoreSharedOptions(options),\n messages,\n isRunning,\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 message: getEveMessageContent(message),\n ...toEveClientContext(message.runConfig),\n });\n } catch (error) {\n if (error !== sendCancelledError) 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 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 message: getEveMessageContent(input.message),\n ...toEveClientContext(runConfig),\n });\n } catch (error) {\n stagedInputsRef.current.set(stagedMessage.id, input);\n messagesRef.current = previousMessages;\n setStagedMessages(previousMessages);\n if (error === sendCancelledError) return;\n throw error;\n }\n if (lastFinishStatusRef.current === \"error\") return;\n }\n },\n }\n : {}),\n onCancel: () => {\n sendEpochRef.current += 1;\n agent.stop();\n return Promise.resolve();\n },\n onRespondToToolApproval: async (response) => {\n try {\n await enqueueSend({ inputResponses: [toEveInputResponse(response)] });\n } catch (error) {\n if (error !== sendCancelledError) throw error;\n }\n },\n });\n};\n"],"mappings":";;;;;;;AAkCA,MAAM,qBAAqB;CACzB,MAAM;CACN,QAAQ;AACV;AAEA,MAAM,qCAAqB,IAAI,MAC7B,qDACF;AAEA,MAAM,gBACJ,cAEA,WAAW,WAAW,KAAA,KAAa,OAAO,KAAK,UAAU,MAAM,CAAC,CAAC,SAAS;;;;;;;AAQ5E,MAAM,sBACJ,cAEA,aAAa,SAAS,IAClB,EACE,eAAe,UAAU,OAG3B,IACA,CAAC;;;;;;;;AAyBP,MAAa,sBAAsB,UAAqC,CAAC,MAAM;CAC7E,MAAM,EACJ,UACA,YAAY,aACZ,gBAAgB,iBAChB,aAAa,cACb,uBAAuB,wBACvB,GAAG,iBACD;CAMJ,MAAM,EAAE,aAAa;CACrB,MAAM,sBAAsB,OAAiC,IAAI;CACjE,MAAM,QAAQ,YAAY;EACxB,GAAG;EACH,WAAW,aAAa;GACtB,oBAAoB,UAAU,SAAS;GACvC,WAAW,QAAQ;EACrB;CACF,CAAC;CACD,MAAM,kBAAkB,mBAAmB;CAC3C,MAAM,CAAC,cAAc,mBAAmB,SAEtC,CAAC,CAAC;CACJ,MAAM,CAAC,gBAAgB,qBAAqB,SAC1C,IACF;CACA,MAAM,0BAA0B,uBAAO,IAAI,IAAkB,CAAC;CAC9D,MAAM,kBAAkB,uBACtB,IAAI,IAGF,CACJ;CAEA,MAAM,oBAAoB,OAAO,OAAO,YAAY,CAAC,CAAC,MACnD,WAAW,QAAQ,SAAS,WAC/B;CACA,MAAM,YACJ,MAAM,WAAW,eACjB,MAAM,WAAW,eACjB;CAEF,MAAM,oBAAoB,cAAc;EACtC,MAAM,uBAAuB,wBAAwB;EACrD,MAAM,aAAa,IAAI,IACrB,MAAM,KAAK,SAAS,KAAK,YAAY,QAAQ,EAAE,CACjD;EACA,KAAK,MAAM,aAAa,qBAAqB,KAAK,GAChD,IAAI,CAAC,WAAW,IAAI,SAAS,GAAG,qBAAqB,OAAO,SAAS;EAGvE,OAAO,mBAAmB,MAAM,MAAM;GACpC;GACA,OAAO,MAAM;GACb,eAAe,YAAY;IACzB,MAAM,WAAW,qBAAqB,IAAI,QAAQ,EAAE;IACpD,IAAI,UAAU,OAAO;IAErB,MAAM,4BAAY,IAAI,KAAK;IAC3B,qBAAqB,IAAI,QAAQ,IAAI,SAAS;IAC9C,OAAO;GACT;EACF,CAAC;CACH,GAAG;EAAC,MAAM;EAAM,MAAM;EAAO;CAAS,CAAC;CAEvC,MAAM,WAAW,kBAAkB;CACnC,MAAM,cAAc,OAAO,QAAQ;CACnC,YAAY,UAAU;CAMtB,MAAM,eAAe,OAAsB,QAAQ,QAAQ,CAAC;CAC5D,MAAM,eAAe,OAAO,CAAC;CAE7B,MAAM,eAAe,YAA8C;EACjE,MAAM,QAAQ,aAAa;EAC3B,MAAM,OAAO,aAAa,QAAQ,WAAW;GAC3C,IAAI,UAAU,aAAa,SAAS,MAAM;GAC1C,OAAO,MAAM,KAAK,OAAO;EAC3B,CAAC;EACD,aAAa,UAAU,KAAK,YAAY,CAAC,CAAC;EAC1C,OAAO;CACT;CAIA,gBAAgB;EACd,aAAa;GACX,aAAa,WAAW;EAC1B;CACF,GAAG,CAAC,CAAC;CAEL,gBAAgB;EACd,IAAI,gBAAgB,QAAQ,SAAS,GAAG;EACxC,MAAM,UAAU,IAAI,IAAI,kBAAkB,KAAK,YAAY,QAAQ,EAAE,CAAC;EACtE,MAAM,YAAY,YAAY,QAAQ,QACnC,YACC,gBAAgB,QAAQ,IAAI,QAAQ,EAAE,KAAK,CAAC,QAAQ,IAAI,QAAQ,EAAE,CACtE;EACA,kBACE,UAAU,SAAS,IAAI,CAAC,GAAG,mBAAmB,GAAG,SAAS,IAAI,IAChE;CACF,GAAG,CAAC,iBAAiB,CAAC;CAEtB,MAAM,gBAAgB,aAA4B;EAChD,IAAI,CAAC,YAAY,CAAC,gBAAgB,QAAQ,IAAI,QAAQ,GAAG,OAAO;EAChE,MAAM,SAGA,CAAC;EACP,KAAK,MAAM,WAAW,YAAY,SAAS;GACzC,MAAM,QAAQ,gBAAgB,QAAQ,IAAI,QAAQ,EAAE;GACpD,IAAI,OAAO,OAAO,KAAK;IAAE;IAAS;GAAM,CAAC;GACzC,IAAI,QAAQ,OAAO,UAAU;EAC/B;EACA,OAAO;CACT;CAEA,MAAM,oBAAoB,YAA2B;EACnD,MAAM,gBAAgB,sBACpB,SACA,WAAW,GACX,kBACF;EACA,gBAAgB,QAAQ,IAAI,cAAc,IAAI;GAC5C;GACA,WAAW,QAAQ;EACrB,CAAC;EACD,MAAM,eAAe,CAAC,GAAG,YAAY,SAAS,aAAa;EAC3D,YAAY,UAAU;EACtB,kBAAkB,YAAY;CAChC;CAEA,OAAO,wBAAwB;EAC7B,GAAG,+BAA+B,OAAO;EACzC;EACA;EACA,gCAAgC;EAChC;EACA,UAAU;GACR,aAAa,UAAU,eAAe,iBAAiB;GACvD,QAAQ,UAAU;GAClB,WAAW,UAAU;GACrB,OAAO,UAAU;GACjB,UAAU,UAAU;EACtB;EACA,OAAO,OAAO,YAAY;GACxB,IAAI,EAAE,QAAQ,YAAY,QAAQ,SAAS,SAAS;IAClD,iBAAiB,OAAO;IACxB;GACF;GACA,IAAI;IACF,MAAM,YAAY;KAChB,SAAS,qBAAqB,OAAO;KACrC,GAAG,mBAAmB,QAAQ,SAAS;IACzC,CAAC;GACH,SAAS,OAAO;IACd,IAAI,UAAU,oBAAoB,MAAM;GAC1C;EACF;EACA,GAAI,iBACA,EACE,UAAU,OAAO,UAAyB,WAAW;GACnD,MAAM,YAAY,aAAa,QAAQ;GACvC,IAAI,CAAC,WACH,MAAM,IAAI,MAAM,8CAA8C;GAChE,MAAM,QAAQ,aAAa;GAC3B,KAAK,MAAM,EAAE,SAAS,eAAe,WAAW,WAAW;IACzD,IAAI,UAAU,aAAa,SAAS;IACpC,MAAM,mBAAmB,YAAY;IACrC,gBAAgB,QAAQ,OAAO,cAAc,EAAE;IAC/C,MAAM,eAAe,iBAAiB,QACnC,YAAY,QAAQ,OAAO,cAAc,EAC5C;IACA,YAAY,UAAU;IACtB,kBACE,gBAAgB,QAAQ,OAAO,IAAI,eAAe,IACpD;IAIA,MAAM,YACJ,cAAc,OAAO,YAAY,aAAa,OAAO,SAAS,IAC1D,OAAO,YACP,MAAM;IACZ,IAAI;KACF,MAAM,YAAY;MAChB,SAAS,qBAAqB,MAAM,OAAO;MAC3C,GAAG,mBAAmB,SAAS;KACjC,CAAC;IACH,SAAS,OAAO;KACd,gBAAgB,QAAQ,IAAI,cAAc,IAAI,KAAK;KACnD,YAAY,UAAU;KACtB,kBAAkB,gBAAgB;KAClC,IAAI,UAAU,oBAAoB;KAClC,MAAM;IACR;IACA,IAAI,oBAAoB,YAAY,SAAS;GAC/C;EACF,EACF,IACA,CAAC;EACL,gBAAgB;GACd,aAAa,WAAW;GACxB,MAAM,KAAK;GACX,OAAO,QAAQ,QAAQ;EACzB;EACA,yBAAyB,OAAO,aAAa;GAC3C,IAAI;IACF,MAAM,YAAY,EAAE,gBAAgB,CAAC,mBAAmB,QAAQ,CAAC,EAAE,CAAC;GACtE,SAAS,OAAO;IACd,IAAI,UAAU,oBAAoB,MAAM;GAC1C;EACF;CACF,CAAC;AACH"}
1
+ {"version":3,"file":"useEveAgentRuntime.js","names":[],"sources":["../src/useEveAgentRuntime.ts"],"sourcesContent":["\"use client\";\n\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport {\n fromThreadMessageLike,\n generateId,\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 type { SendTurnPayload } from \"eve/client\";\nimport {\n convertEveMessages,\n getEveMessageContent,\n toEveInputResponse,\n} from \"./convertEveMessages\";\n\nconst USER_STAGED_STATUS = {\n type: \"complete\",\n reason: \"unknown\",\n} as const;\n\nconst sendCancelledError = new Error(\n \"eve send was dropped because the run was cancelled.\",\n);\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\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 toEveClientContext = (\n runConfig: AppendMessage[\"runConfig\"],\n): Pick<SendTurnPayload, \"clientContext\"> =>\n hasRunConfig(runConfig)\n ? {\n clientContext: runConfig.custom as NonNullable<\n SendTurnPayload[\"clientContext\"]\n >,\n }\n : {};\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 isRunning =\n agent.status === \"submitted\" ||\n agent.status === \"streaming\" ||\n 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 messagesRef.current = messages;\n\n // Upstream `EveAgentStore.send` rejects while a turn is in flight and only\n // resolves once the turn's stream parks, so a pending chain link is exactly\n // an active turn; chaining every send serializes them without watching\n // status.\n const sendChainRef = useRef<Promise<void>>(Promise.resolve());\n const sendEpochRef = useRef(0);\n\n const enqueueSend = (payload: Parameters<typeof agent.send>[0]) => {\n const epoch = sendEpochRef.current;\n const next = sendChainRef.current.then(() => {\n if (epoch !== sendEpochRef.current) throw sendCancelledError;\n return agent.send(payload);\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.\n useEffect(() => {\n return () => {\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 return useExternalStoreRuntime({\n ...pickExternalStoreSharedOptions(options),\n messages,\n isRunning,\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 message: getEveMessageContent(message),\n ...toEveClientContext(message.runConfig),\n });\n } catch (error) {\n if (error !== sendCancelledError) 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 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 message: getEveMessageContent(input.message),\n ...toEveClientContext(runConfig),\n });\n } catch (error) {\n stagedInputsRef.current.set(stagedMessage.id, input);\n messagesRef.current = previousMessages;\n setStagedMessages(previousMessages);\n if (error === sendCancelledError) return;\n throw error;\n }\n if (lastFinishStatusRef.current === \"error\") return;\n }\n },\n }\n : {}),\n onCancel: () => {\n sendEpochRef.current += 1;\n agent.stop();\n return Promise.resolve();\n },\n onRespondToToolApproval: async (response) => {\n try {\n await enqueueSend({ inputResponses: [toEveInputResponse(response)] });\n } catch (error) {\n if (error !== sendCancelledError) throw error;\n }\n },\n });\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.11",
3
+ "version": "0.0.12",
4
4
  "description": "Eve runtime adapter for assistant-ui",
5
5
  "keywords": [
6
6
  "eve",
@@ -28,7 +28,7 @@
28
28
  ],
29
29
  "sideEffects": false,
30
30
  "dependencies": {
31
- "@assistant-ui/core": "^0.3.10"
31
+ "@assistant-ui/core": "^0.3.11"
32
32
  },
33
33
  "peerDependencies": {
34
34
  "@types/react": "*",
@@ -1,7 +1,7 @@
1
1
  // @vitest-environment jsdom
2
2
 
3
3
  import { act, renderHook, waitFor } from "@testing-library/react";
4
- import { describe, expect, it, vi } from "vitest";
4
+ import { afterEach, describe, expect, it, vi } from "vitest";
5
5
 
6
6
  const { mockUseEveAgent } = vi.hoisted(() => ({
7
7
  mockUseEveAgent: vi.fn(),
@@ -39,7 +39,60 @@ const createAgent = (overrides: Record<string, unknown>) => ({
39
39
  ...overrides,
40
40
  });
41
41
 
42
+ afterEach(() => {
43
+ vi.restoreAllMocks();
44
+ });
45
+
42
46
  describe("useEveAgentRuntime status forwarding", () => {
47
+ it.each(
48
+ (["onError", "onEvent", "onFinish", "onSessionChange"] as const).flatMap(
49
+ (callbackName) =>
50
+ (["throws", "rejects"] as const).map(
51
+ (failureMode) => [callbackName, failureMode] as const,
52
+ ),
53
+ ),
54
+ )(
55
+ "isolates %s callback errors when it %s",
56
+ async (callbackName, failureMode) => {
57
+ const callbackError = new Error(`${callbackName} failed`);
58
+ const callback = vi.fn(() => {
59
+ if (failureMode === "throws") throw callbackError;
60
+ return Promise.reject(callbackError);
61
+ });
62
+ const agent = createAgent({ data: { messages: [] } });
63
+ let capturedOptions: Record<
64
+ string,
65
+ ((value: unknown) => void) | undefined
66
+ > = {};
67
+ mockUseEveAgent.mockImplementation((options) => {
68
+ capturedOptions = options as typeof capturedOptions;
69
+ return agent as never;
70
+ });
71
+ const consoleError = vi
72
+ .spyOn(console, "error")
73
+ .mockImplementation(() => {});
74
+
75
+ renderHook(() =>
76
+ useEveAgentRuntime({ [callbackName]: callback } as never),
77
+ );
78
+
79
+ const value =
80
+ callbackName === "onFinish"
81
+ ? { status: "ready" }
82
+ : callbackName === "onError"
83
+ ? new Error("run failed")
84
+ : {};
85
+ expect(() => capturedOptions[callbackName]?.(value)).not.toThrow();
86
+ expect(callback).toHaveBeenCalledWith(value);
87
+ await waitFor(() => {
88
+ expect(consoleError).toHaveBeenCalledWith(
89
+ `[assistant-ui/eve] ${callbackName} callback threw an error`,
90
+ callbackError,
91
+ );
92
+ });
93
+ },
94
+ );
95
+
43
96
  it("maps the session error onto the interrupted assistant message", () => {
44
97
  mockUseEveAgent.mockReturnValue(
45
98
  createAgent({ status: "error", error: new Error("boom") }) as never,
@@ -41,6 +41,35 @@ const sendCancelledError = new Error(
41
41
  "eve send was dropped because the run was cancelled.",
42
42
  );
43
43
 
44
+ type EveLifecycleCallbackName =
45
+ | "onError"
46
+ | "onEvent"
47
+ | "onFinish"
48
+ | "onSessionChange";
49
+
50
+ const reportEveLifecycleCallbackError = (
51
+ name: EveLifecycleCallbackName,
52
+ error: unknown,
53
+ ) => {
54
+ console.error(`[assistant-ui/eve] ${name} callback threw an error`, error);
55
+ };
56
+
57
+ const invokeEveLifecycleCallback = <T>(
58
+ name: EveLifecycleCallbackName,
59
+ callback: ((value: T) => unknown) | undefined,
60
+ value: T,
61
+ ) => {
62
+ if (!callback) return;
63
+
64
+ try {
65
+ void Promise.resolve(callback(value)).catch((error) => {
66
+ reportEveLifecycleCallbackError(name, error);
67
+ });
68
+ } catch (error) {
69
+ reportEveLifecycleCallbackError(name, error);
70
+ }
71
+ };
72
+
44
73
  const hasRunConfig = (
45
74
  runConfig: AppendMessage["runConfig"],
46
75
  ): runConfig is NonNullable<AppendMessage["runConfig"]> =>
@@ -100,14 +129,36 @@ export const useEveAgentRuntime = (options: UseEveAgentRuntimeOptions = {}) => {
100
129
  ? true
101
130
  : never;
102
131
 
103
- const { onFinish } = agentOptions;
132
+ const { onError, onEvent, onFinish, onSessionChange } = agentOptions;
104
133
  const lastFinishStatusRef = useRef<UseEveAgentStatus | null>(null);
105
134
  const agent = useEveAgent({
106
135
  ...agentOptions,
136
+ ...(onError
137
+ ? {
138
+ onError: (error) =>
139
+ invokeEveLifecycleCallback("onError", onError, error),
140
+ }
141
+ : {}),
142
+ ...(onEvent
143
+ ? {
144
+ onEvent: (event) =>
145
+ invokeEveLifecycleCallback("onEvent", onEvent, event),
146
+ }
147
+ : {}),
107
148
  onFinish: (snapshot) => {
108
149
  lastFinishStatusRef.current = snapshot.status;
109
- onFinish?.(snapshot);
150
+ invokeEveLifecycleCallback("onFinish", onFinish, snapshot);
110
151
  },
152
+ ...(onSessionChange
153
+ ? {
154
+ onSessionChange: (session) =>
155
+ invokeEveLifecycleCallback(
156
+ "onSessionChange",
157
+ onSessionChange,
158
+ session,
159
+ ),
160
+ }
161
+ : {}),
111
162
  });
112
163
  const runtimeAdapters = useRuntimeAdapters();
113
164
  const [toolStatuses, setToolStatuses] = useState<