@assistant-ui/eve 0.0.11 → 0.0.13
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":";;;
|
|
1
|
+
{"version":3,"file":"useEveAgentRuntime.d.ts","names":[],"sources":["../src/useEveAgentRuntime.ts"],"mappings":";;;KAsGY,4BAA4B,KACtC,mBAAmB,8BAGnB;WACW;aAEM,cAAc;aACd,SAAS;aACT,YAAY;aACZ,QAAQ;aACR,WAAW;;;;;;;;;;cAYjB,qBAAsB,UAAS,2DAA8B"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { convertEveMessages, getEveMessageContent, toEveInputResponse } from "./convertEveMessages.js";
|
|
3
|
-
import { fromThreadMessageLike, generateId, pickExternalStoreSharedOptions } from "@assistant-ui/core";
|
|
3
|
+
import { MessageNotSentError, fromThreadMessageLike, generateId, pickExternalStoreSharedOptions } from "@assistant-ui/core";
|
|
4
4
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
5
5
|
import { useExternalStoreRuntime, useRuntimeAdapters } from "@assistant-ui/core/react";
|
|
6
6
|
import { useEveAgent } from "eve/react";
|
|
@@ -9,7 +9,22 @@ const USER_STAGED_STATUS = {
|
|
|
9
9
|
type: "complete",
|
|
10
10
|
reason: "unknown"
|
|
11
11
|
};
|
|
12
|
-
const sendCancelledError =
|
|
12
|
+
const sendCancelledError = new MessageNotSentError("eve send was dropped because the run was cancelled.");
|
|
13
|
+
const sendAbandonedError = /* @__PURE__ */ new Error("eve send was dropped because the runtime unmounted.");
|
|
14
|
+
const isDroppedSend = (error) => error === sendCancelledError || error === sendAbandonedError;
|
|
15
|
+
const reportEveLifecycleCallbackError = (name, error) => {
|
|
16
|
+
console.error(`[assistant-ui/eve] ${name} callback threw an error`, error);
|
|
17
|
+
};
|
|
18
|
+
const invokeEveLifecycleCallback = (name, callback, value) => {
|
|
19
|
+
if (!callback) return;
|
|
20
|
+
try {
|
|
21
|
+
Promise.resolve(callback(value)).catch((error) => {
|
|
22
|
+
reportEveLifecycleCallbackError(name, error);
|
|
23
|
+
});
|
|
24
|
+
} catch (error) {
|
|
25
|
+
reportEveLifecycleCallbackError(name, error);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
13
28
|
const hasRunConfig = (runConfig) => runConfig?.custom !== void 0 && Object.keys(runConfig.custom).length > 0;
|
|
14
29
|
/**
|
|
15
30
|
* Only the `custom` bag crosses the wire. Eve reads `clientContext` as its own
|
|
@@ -27,14 +42,17 @@ const toEveClientContext = (runConfig) => hasRunConfig(runConfig) ? { clientCont
|
|
|
27
42
|
*/
|
|
28
43
|
const useEveAgentRuntime = (options = {}) => {
|
|
29
44
|
const { adapters, isDisabled: _isDisabled, isSendDisabled: _isSendDisabled, suggestions: _suggestions, unstable_capabilities: _unstable_capabilities, ...agentOptions } = options;
|
|
30
|
-
const { onFinish } = agentOptions;
|
|
45
|
+
const { onError, onEvent, onFinish, onSessionChange } = agentOptions;
|
|
31
46
|
const lastFinishStatusRef = useRef(null);
|
|
32
47
|
const agent = useEveAgent({
|
|
33
48
|
...agentOptions,
|
|
49
|
+
...onError ? { onError: (error) => invokeEveLifecycleCallback("onError", onError, error) } : {},
|
|
50
|
+
...onEvent ? { onEvent: (event) => invokeEveLifecycleCallback("onEvent", onEvent, event) } : {},
|
|
34
51
|
onFinish: (snapshot) => {
|
|
35
52
|
lastFinishStatusRef.current = snapshot.status;
|
|
36
|
-
onFinish
|
|
37
|
-
}
|
|
53
|
+
invokeEveLifecycleCallback("onFinish", onFinish, snapshot);
|
|
54
|
+
},
|
|
55
|
+
...onSessionChange ? { onSessionChange: (session) => invokeEveLifecycleCallback("onSessionChange", onSessionChange, session) } : {}
|
|
38
56
|
});
|
|
39
57
|
const runtimeAdapters = useRuntimeAdapters();
|
|
40
58
|
const [toolStatuses, setToolStatuses] = useState({});
|
|
@@ -68,17 +86,20 @@ const useEveAgentRuntime = (options = {}) => {
|
|
|
68
86
|
messagesRef.current = messages;
|
|
69
87
|
const sendChainRef = useRef(Promise.resolve());
|
|
70
88
|
const sendEpochRef = useRef(0);
|
|
89
|
+
const isMountedRef = useRef(true);
|
|
71
90
|
const enqueueSend = (payload) => {
|
|
72
91
|
const epoch = sendEpochRef.current;
|
|
73
92
|
const next = sendChainRef.current.then(() => {
|
|
74
|
-
if (epoch !== sendEpochRef.current) throw sendCancelledError;
|
|
93
|
+
if (epoch !== sendEpochRef.current) throw isMountedRef.current ? sendCancelledError : sendAbandonedError;
|
|
75
94
|
return agent.send(payload);
|
|
76
95
|
});
|
|
77
96
|
sendChainRef.current = next.catch(() => {});
|
|
78
97
|
return next;
|
|
79
98
|
};
|
|
80
99
|
useEffect(() => {
|
|
100
|
+
isMountedRef.current = true;
|
|
81
101
|
return () => {
|
|
102
|
+
isMountedRef.current = false;
|
|
82
103
|
sendEpochRef.current += 1;
|
|
83
104
|
};
|
|
84
105
|
}, []);
|
|
@@ -135,7 +156,8 @@ const useEveAgentRuntime = (options = {}) => {
|
|
|
135
156
|
...toEveClientContext(message.runConfig)
|
|
136
157
|
});
|
|
137
158
|
} catch (error) {
|
|
138
|
-
if (error
|
|
159
|
+
if (error === sendAbandonedError) return;
|
|
160
|
+
throw error;
|
|
139
161
|
}
|
|
140
162
|
},
|
|
141
163
|
...stagedMessages ? { onReload: async (parentId, config) => {
|
|
@@ -159,7 +181,7 @@ const useEveAgentRuntime = (options = {}) => {
|
|
|
159
181
|
stagedInputsRef.current.set(stagedMessage.id, input);
|
|
160
182
|
messagesRef.current = previousMessages;
|
|
161
183
|
setStagedMessages(previousMessages);
|
|
162
|
-
if (error
|
|
184
|
+
if (isDroppedSend(error)) return;
|
|
163
185
|
throw error;
|
|
164
186
|
}
|
|
165
187
|
if (lastFinishStatusRef.current === "error") return;
|
|
@@ -174,7 +196,7 @@ const useEveAgentRuntime = (options = {}) => {
|
|
|
174
196
|
try {
|
|
175
197
|
await enqueueSend({ inputResponses: [toEveInputResponse(response)] });
|
|
176
198
|
} catch (error) {
|
|
177
|
-
if (error
|
|
199
|
+
if (!isDroppedSend(error)) throw error;
|
|
178
200
|
}
|
|
179
201
|
}
|
|
180
202
|
});
|
|
@@ -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":"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"}
|
|
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 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 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 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\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 const isMountedRef = useRef(true);\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)\n throw isMountedRef.current ? sendCancelledError : sendAbandonedError;\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. 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 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 // 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 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 (isDroppedSend(error)) 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 (!isDroppedSend(error)) 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.
|
|
3
|
+
"version": "0.0.13",
|
|
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.
|
|
31
|
+
"@assistant-ui/core": "^0.3.13"
|
|
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,
|
|
@@ -858,6 +911,142 @@ describe("useEveAgentRuntime concurrent sends", () => {
|
|
|
858
911
|
expect(send).toHaveBeenCalledTimes(1);
|
|
859
912
|
});
|
|
860
913
|
|
|
914
|
+
it("returns a cancelled queued send to the composer", async () => {
|
|
915
|
+
let resolveFirstSend!: () => void;
|
|
916
|
+
const send = vi
|
|
917
|
+
.fn()
|
|
918
|
+
.mockImplementationOnce(
|
|
919
|
+
() =>
|
|
920
|
+
new Promise<void>((resolve) => {
|
|
921
|
+
resolveFirstSend = resolve;
|
|
922
|
+
}),
|
|
923
|
+
)
|
|
924
|
+
.mockResolvedValue(undefined);
|
|
925
|
+
const agent = createAgent({ data: settledData, send });
|
|
926
|
+
mockUseEveAgent.mockReturnValue(agent as never);
|
|
927
|
+
const { result } = renderHook(() => useEveAgentRuntime());
|
|
928
|
+
|
|
929
|
+
await act(async () => {
|
|
930
|
+
result.current.thread.append({
|
|
931
|
+
role: "user",
|
|
932
|
+
content: [{ type: "text", text: "first" }],
|
|
933
|
+
});
|
|
934
|
+
});
|
|
935
|
+
await waitFor(() => expect(send).toHaveBeenCalledTimes(1));
|
|
936
|
+
|
|
937
|
+
await act(async () => {
|
|
938
|
+
result.current.thread.composer.setText("queued");
|
|
939
|
+
result.current.thread.composer.send();
|
|
940
|
+
});
|
|
941
|
+
expect(result.current.thread.composer.getState().text).toBe("");
|
|
942
|
+
|
|
943
|
+
act(() => {
|
|
944
|
+
result.current.thread.cancelRun();
|
|
945
|
+
});
|
|
946
|
+
await act(async () => {
|
|
947
|
+
resolveFirstSend();
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
await waitFor(() => {
|
|
951
|
+
expect(result.current.thread.composer.getState().text).toBe("queued");
|
|
952
|
+
});
|
|
953
|
+
expect(send).toHaveBeenCalledTimes(1);
|
|
954
|
+
expect(getText(result.current)).toEqual(["earlier", "earlier answer"]);
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
it("returns the queued send when cancel keeps the trailing message", async () => {
|
|
958
|
+
let resolveFirstSend!: () => void;
|
|
959
|
+
const send = vi
|
|
960
|
+
.fn()
|
|
961
|
+
.mockImplementationOnce(
|
|
962
|
+
() =>
|
|
963
|
+
new Promise<void>((resolve) => {
|
|
964
|
+
resolveFirstSend = resolve;
|
|
965
|
+
}),
|
|
966
|
+
)
|
|
967
|
+
.mockResolvedValue(undefined);
|
|
968
|
+
// Before the turn streams, the dispatched message is the thread's trailing
|
|
969
|
+
// user leaf. Eve owns that message and cannot remove it on cancel.
|
|
970
|
+
const agent = createAgent({
|
|
971
|
+
data: {
|
|
972
|
+
messages: [
|
|
973
|
+
...settledData.messages,
|
|
974
|
+
{ id: "u2", role: "user", parts: [{ type: "text", text: "first" }] },
|
|
975
|
+
],
|
|
976
|
+
} satisfies EveMessageData,
|
|
977
|
+
status: "submitted",
|
|
978
|
+
send,
|
|
979
|
+
});
|
|
980
|
+
mockUseEveAgent.mockReturnValue(agent as never);
|
|
981
|
+
const { result } = renderHook(() => useEveAgentRuntime());
|
|
982
|
+
|
|
983
|
+
await act(async () => {
|
|
984
|
+
result.current.thread.composer.setText("first");
|
|
985
|
+
result.current.thread.composer.send();
|
|
986
|
+
});
|
|
987
|
+
await waitFor(() => expect(send).toHaveBeenCalledTimes(1));
|
|
988
|
+
|
|
989
|
+
await act(async () => {
|
|
990
|
+
result.current.thread.composer.setText("queued");
|
|
991
|
+
result.current.thread.composer.send();
|
|
992
|
+
});
|
|
993
|
+
act(() => {
|
|
994
|
+
result.current.thread.cancelRun();
|
|
995
|
+
});
|
|
996
|
+
expect(result.current.thread.composer.getState().text).toBe("");
|
|
997
|
+
|
|
998
|
+
await act(async () => {
|
|
999
|
+
resolveFirstSend();
|
|
1000
|
+
});
|
|
1001
|
+
|
|
1002
|
+
await waitFor(() => {
|
|
1003
|
+
expect(result.current.thread.composer.getState().text).toBe("queued");
|
|
1004
|
+
});
|
|
1005
|
+
expect(send).toHaveBeenCalledTimes(1);
|
|
1006
|
+
});
|
|
1007
|
+
|
|
1008
|
+
it("keeps a cancelled tool approval discarded", async () => {
|
|
1009
|
+
let resolveFirstSend!: () => void;
|
|
1010
|
+
const send = vi
|
|
1011
|
+
.fn()
|
|
1012
|
+
.mockImplementationOnce(
|
|
1013
|
+
() =>
|
|
1014
|
+
new Promise<void>((resolve) => {
|
|
1015
|
+
resolveFirstSend = resolve;
|
|
1016
|
+
}),
|
|
1017
|
+
)
|
|
1018
|
+
.mockResolvedValue(undefined);
|
|
1019
|
+
const agent = createAgent({ data: approvalData, send });
|
|
1020
|
+
mockUseEveAgent.mockReturnValue(agent as never);
|
|
1021
|
+
const { result } = renderHook(() => useEveAgentRuntime());
|
|
1022
|
+
const before = getText(result.current);
|
|
1023
|
+
|
|
1024
|
+
await act(async () => {
|
|
1025
|
+
result.current.thread.append({
|
|
1026
|
+
role: "user",
|
|
1027
|
+
content: [{ type: "text", text: "go" }],
|
|
1028
|
+
});
|
|
1029
|
+
});
|
|
1030
|
+
await waitFor(() => expect(send).toHaveBeenCalledTimes(1));
|
|
1031
|
+
|
|
1032
|
+
act(() => {
|
|
1033
|
+
result.current.thread
|
|
1034
|
+
.getMessageByIndex(1)
|
|
1035
|
+
.getMessagePartByToolCallId("call_1")
|
|
1036
|
+
.respondToToolApproval({ optionId: "approve" });
|
|
1037
|
+
});
|
|
1038
|
+
act(() => {
|
|
1039
|
+
result.current.thread.cancelRun();
|
|
1040
|
+
});
|
|
1041
|
+
await act(async () => {
|
|
1042
|
+
resolveFirstSend();
|
|
1043
|
+
});
|
|
1044
|
+
|
|
1045
|
+
expect(send).toHaveBeenCalledTimes(1);
|
|
1046
|
+
expect(result.current.thread.composer.getState().text).toBe("");
|
|
1047
|
+
expect(getText(result.current)).toEqual(before);
|
|
1048
|
+
});
|
|
1049
|
+
|
|
861
1050
|
it("drops a queued send when the hook unmounts before it dispatches", async () => {
|
|
862
1051
|
let resolveFirstSend!: () => void;
|
|
863
1052
|
const send = vi
|
|
@@ -4,6 +4,7 @@ import { useEffect, useMemo, useRef, useState } from "react";
|
|
|
4
4
|
import {
|
|
5
5
|
fromThreadMessageLike,
|
|
6
6
|
generateId,
|
|
7
|
+
MessageNotSentError,
|
|
7
8
|
pickExternalStoreSharedOptions,
|
|
8
9
|
type AppendMessage,
|
|
9
10
|
type AttachmentAdapter,
|
|
@@ -37,10 +38,46 @@ const USER_STAGED_STATUS = {
|
|
|
37
38
|
reason: "unknown",
|
|
38
39
|
} as const;
|
|
39
40
|
|
|
40
|
-
const sendCancelledError = new
|
|
41
|
+
const sendCancelledError = new MessageNotSentError(
|
|
41
42
|
"eve send was dropped because the run was cancelled.",
|
|
42
43
|
);
|
|
43
44
|
|
|
45
|
+
const sendAbandonedError = new Error(
|
|
46
|
+
"eve send was dropped because the runtime unmounted.",
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const isDroppedSend = (error: unknown) =>
|
|
50
|
+
error === sendCancelledError || error === sendAbandonedError;
|
|
51
|
+
|
|
52
|
+
type EveLifecycleCallbackName =
|
|
53
|
+
| "onError"
|
|
54
|
+
| "onEvent"
|
|
55
|
+
| "onFinish"
|
|
56
|
+
| "onSessionChange";
|
|
57
|
+
|
|
58
|
+
const reportEveLifecycleCallbackError = (
|
|
59
|
+
name: EveLifecycleCallbackName,
|
|
60
|
+
error: unknown,
|
|
61
|
+
) => {
|
|
62
|
+
console.error(`[assistant-ui/eve] ${name} callback threw an error`, error);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const invokeEveLifecycleCallback = <T>(
|
|
66
|
+
name: EveLifecycleCallbackName,
|
|
67
|
+
callback: ((value: T) => unknown) | undefined,
|
|
68
|
+
value: T,
|
|
69
|
+
) => {
|
|
70
|
+
if (!callback) return;
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
void Promise.resolve(callback(value)).catch((error) => {
|
|
74
|
+
reportEveLifecycleCallbackError(name, error);
|
|
75
|
+
});
|
|
76
|
+
} catch (error) {
|
|
77
|
+
reportEveLifecycleCallbackError(name, error);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
44
81
|
const hasRunConfig = (
|
|
45
82
|
runConfig: AppendMessage["runConfig"],
|
|
46
83
|
): runConfig is NonNullable<AppendMessage["runConfig"]> =>
|
|
@@ -100,14 +137,36 @@ export const useEveAgentRuntime = (options: UseEveAgentRuntimeOptions = {}) => {
|
|
|
100
137
|
? true
|
|
101
138
|
: never;
|
|
102
139
|
|
|
103
|
-
const { onFinish } = agentOptions;
|
|
140
|
+
const { onError, onEvent, onFinish, onSessionChange } = agentOptions;
|
|
104
141
|
const lastFinishStatusRef = useRef<UseEveAgentStatus | null>(null);
|
|
105
142
|
const agent = useEveAgent({
|
|
106
143
|
...agentOptions,
|
|
144
|
+
...(onError
|
|
145
|
+
? {
|
|
146
|
+
onError: (error) =>
|
|
147
|
+
invokeEveLifecycleCallback("onError", onError, error),
|
|
148
|
+
}
|
|
149
|
+
: {}),
|
|
150
|
+
...(onEvent
|
|
151
|
+
? {
|
|
152
|
+
onEvent: (event) =>
|
|
153
|
+
invokeEveLifecycleCallback("onEvent", onEvent, event),
|
|
154
|
+
}
|
|
155
|
+
: {}),
|
|
107
156
|
onFinish: (snapshot) => {
|
|
108
157
|
lastFinishStatusRef.current = snapshot.status;
|
|
109
|
-
onFinish
|
|
158
|
+
invokeEveLifecycleCallback("onFinish", onFinish, snapshot);
|
|
110
159
|
},
|
|
160
|
+
...(onSessionChange
|
|
161
|
+
? {
|
|
162
|
+
onSessionChange: (session) =>
|
|
163
|
+
invokeEveLifecycleCallback(
|
|
164
|
+
"onSessionChange",
|
|
165
|
+
onSessionChange,
|
|
166
|
+
session,
|
|
167
|
+
),
|
|
168
|
+
}
|
|
169
|
+
: {}),
|
|
111
170
|
});
|
|
112
171
|
const runtimeAdapters = useRuntimeAdapters();
|
|
113
172
|
const [toolStatuses, setToolStatuses] = useState<
|
|
@@ -165,11 +224,13 @@ export const useEveAgentRuntime = (options: UseEveAgentRuntimeOptions = {}) => {
|
|
|
165
224
|
// status.
|
|
166
225
|
const sendChainRef = useRef<Promise<void>>(Promise.resolve());
|
|
167
226
|
const sendEpochRef = useRef(0);
|
|
227
|
+
const isMountedRef = useRef(true);
|
|
168
228
|
|
|
169
229
|
const enqueueSend = (payload: Parameters<typeof agent.send>[0]) => {
|
|
170
230
|
const epoch = sendEpochRef.current;
|
|
171
231
|
const next = sendChainRef.current.then(() => {
|
|
172
|
-
if (epoch !== sendEpochRef.current)
|
|
232
|
+
if (epoch !== sendEpochRef.current)
|
|
233
|
+
throw isMountedRef.current ? sendCancelledError : sendAbandonedError;
|
|
173
234
|
return agent.send(payload);
|
|
174
235
|
});
|
|
175
236
|
sendChainRef.current = next.catch(() => {});
|
|
@@ -177,9 +238,13 @@ export const useEveAgentRuntime = (options: UseEveAgentRuntimeOptions = {}) => {
|
|
|
177
238
|
};
|
|
178
239
|
|
|
179
240
|
// The store outlives the component (useEveAgent holds it in a ref with no
|
|
180
|
-
// cleanup), so queued sends must not fire server turns after unmount.
|
|
241
|
+
// cleanup), so queued sends must not fire server turns after unmount. The
|
|
242
|
+
// flag separates that teardown from a user cancel, and is re-armed in setup
|
|
243
|
+
// for a remounted tree.
|
|
181
244
|
useEffect(() => {
|
|
245
|
+
isMountedRef.current = true;
|
|
182
246
|
return () => {
|
|
247
|
+
isMountedRef.current = false;
|
|
183
248
|
sendEpochRef.current += 1;
|
|
184
249
|
};
|
|
185
250
|
}, []);
|
|
@@ -249,7 +314,11 @@ export const useEveAgentRuntime = (options: UseEveAgentRuntimeOptions = {}) => {
|
|
|
249
314
|
...toEveClientContext(message.runConfig),
|
|
250
315
|
});
|
|
251
316
|
} catch (error) {
|
|
252
|
-
|
|
317
|
+
// A cancelled send never reached the session, so it rethrows for the
|
|
318
|
+
// composer to take the draft back; an unmounted one has no composer
|
|
319
|
+
// left to restore.
|
|
320
|
+
if (error === sendAbandonedError) return;
|
|
321
|
+
throw error;
|
|
253
322
|
}
|
|
254
323
|
},
|
|
255
324
|
...(stagedMessages
|
|
@@ -286,7 +355,7 @@ export const useEveAgentRuntime = (options: UseEveAgentRuntimeOptions = {}) => {
|
|
|
286
355
|
stagedInputsRef.current.set(stagedMessage.id, input);
|
|
287
356
|
messagesRef.current = previousMessages;
|
|
288
357
|
setStagedMessages(previousMessages);
|
|
289
|
-
if (error
|
|
358
|
+
if (isDroppedSend(error)) return;
|
|
290
359
|
throw error;
|
|
291
360
|
}
|
|
292
361
|
if (lastFinishStatusRef.current === "error") return;
|
|
@@ -303,7 +372,7 @@ export const useEveAgentRuntime = (options: UseEveAgentRuntimeOptions = {}) => {
|
|
|
303
372
|
try {
|
|
304
373
|
await enqueueSend({ inputResponses: [toEveInputResponse(response)] });
|
|
305
374
|
} catch (error) {
|
|
306
|
-
if (error
|
|
375
|
+
if (!isDroppedSend(error)) throw error;
|
|
307
376
|
}
|
|
308
377
|
},
|
|
309
378
|
});
|