@assistant-ui/eve 0.0.8 → 0.0.10
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":";;;KAiEY,4BAA4B,KACtC,mBAAmB,8BAGnB;WACW;aAEM,cAAc;aACd,SAAS;aACT,YAAY;aACZ,QAAQ;aACR,WAAW;;;;;;;;;;cAYjB,qBAAsB,UAAS,2DAA8B"}
|
|
@@ -9,6 +9,15 @@ const USER_STAGED_STATUS = {
|
|
|
9
9
|
type: "complete",
|
|
10
10
|
reason: "unknown"
|
|
11
11
|
};
|
|
12
|
+
const sendCancelledError = /* @__PURE__ */ new Error("eve send was dropped because the run was cancelled.");
|
|
13
|
+
const hasRunConfig = (runConfig) => runConfig?.custom !== void 0 && Object.keys(runConfig.custom).length > 0;
|
|
14
|
+
/**
|
|
15
|
+
* Only the `custom` bag crosses the wire. Eve reads `clientContext` as its own
|
|
16
|
+
* namespace and serializes it into a model-visible context message, so sending
|
|
17
|
+
* the assistant-ui envelope would surface a literal `"custom"` key in the
|
|
18
|
+
* prompt and to every eve-side handler.
|
|
19
|
+
*/
|
|
20
|
+
const toEveClientContext = (runConfig) => hasRunConfig(runConfig) ? { clientContext: runConfig.custom } : {};
|
|
12
21
|
/**
|
|
13
22
|
* Connects Eve's `useEveAgent` hook to assistant-ui's runtime contract.
|
|
14
23
|
*
|
|
@@ -18,7 +27,15 @@ const USER_STAGED_STATUS = {
|
|
|
18
27
|
*/
|
|
19
28
|
const useEveAgentRuntime = (options = {}) => {
|
|
20
29
|
const { adapters, isDisabled: _isDisabled, isSendDisabled: _isSendDisabled, suggestions: _suggestions, unstable_capabilities: _unstable_capabilities, ...agentOptions } = options;
|
|
21
|
-
const
|
|
30
|
+
const { onFinish } = agentOptions;
|
|
31
|
+
const lastFinishStatusRef = useRef(null);
|
|
32
|
+
const agent = useEveAgent({
|
|
33
|
+
...agentOptions,
|
|
34
|
+
onFinish: (snapshot) => {
|
|
35
|
+
lastFinishStatusRef.current = snapshot.status;
|
|
36
|
+
onFinish?.(snapshot);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
22
39
|
const runtimeAdapters = useRuntimeAdapters();
|
|
23
40
|
const [toolStatuses, setToolStatuses] = useState({});
|
|
24
41
|
const [stagedMessages, setStagedMessages] = useState(null);
|
|
@@ -49,6 +66,22 @@ const useEveAgentRuntime = (options = {}) => {
|
|
|
49
66
|
const messages = stagedMessages ?? convertedMessages;
|
|
50
67
|
const messagesRef = useRef(messages);
|
|
51
68
|
messagesRef.current = messages;
|
|
69
|
+
const sendChainRef = useRef(Promise.resolve());
|
|
70
|
+
const sendEpochRef = useRef(0);
|
|
71
|
+
const enqueueSend = (payload) => {
|
|
72
|
+
const epoch = sendEpochRef.current;
|
|
73
|
+
const next = sendChainRef.current.then(() => {
|
|
74
|
+
if (epoch !== sendEpochRef.current) throw sendCancelledError;
|
|
75
|
+
return agent.send(payload);
|
|
76
|
+
});
|
|
77
|
+
sendChainRef.current = next.catch(() => {});
|
|
78
|
+
return next;
|
|
79
|
+
};
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
return () => {
|
|
82
|
+
sendEpochRef.current += 1;
|
|
83
|
+
};
|
|
84
|
+
}, []);
|
|
52
85
|
useEffect(() => {
|
|
53
86
|
if (stagedInputsRef.current.size === 0) return;
|
|
54
87
|
const baseIds = new Set(convertedMessages.map((message) => message.id));
|
|
@@ -96,33 +129,53 @@ const useEveAgentRuntime = (options = {}) => {
|
|
|
96
129
|
stageUserMessage(message);
|
|
97
130
|
return;
|
|
98
131
|
}
|
|
99
|
-
|
|
132
|
+
try {
|
|
133
|
+
await enqueueSend({
|
|
134
|
+
message: getEveMessageContent(message),
|
|
135
|
+
...toEveClientContext(message.runConfig)
|
|
136
|
+
});
|
|
137
|
+
} catch (error) {
|
|
138
|
+
if (error !== sendCancelledError) throw error;
|
|
139
|
+
}
|
|
100
140
|
},
|
|
101
|
-
...stagedMessages ? { onReload: async (parentId) => {
|
|
141
|
+
...stagedMessages ? { onReload: async (parentId, config) => {
|
|
102
142
|
const stagedRun = getStagedRun(parentId);
|
|
103
143
|
if (!stagedRun) throw new Error("Runtime does not support reloading messages.");
|
|
144
|
+
const epoch = sendEpochRef.current;
|
|
104
145
|
for (const { message: stagedMessage, input } of stagedRun) {
|
|
146
|
+
if (epoch !== sendEpochRef.current) return;
|
|
105
147
|
const previousMessages = messagesRef.current;
|
|
106
148
|
stagedInputsRef.current.delete(stagedMessage.id);
|
|
107
149
|
const nextMessages = previousMessages.filter((message) => message.id !== stagedMessage.id);
|
|
108
150
|
messagesRef.current = nextMessages;
|
|
109
151
|
setStagedMessages(stagedInputsRef.current.size > 0 ? nextMessages : null);
|
|
152
|
+
const runConfig = stagedMessage.id === parentId && hasRunConfig(config.runConfig) ? config.runConfig : input.runConfig;
|
|
110
153
|
try {
|
|
111
|
-
await
|
|
154
|
+
await enqueueSend({
|
|
155
|
+
message: getEveMessageContent(input.message),
|
|
156
|
+
...toEveClientContext(runConfig)
|
|
157
|
+
});
|
|
112
158
|
} catch (error) {
|
|
113
159
|
stagedInputsRef.current.set(stagedMessage.id, input);
|
|
114
160
|
messagesRef.current = previousMessages;
|
|
115
161
|
setStagedMessages(previousMessages);
|
|
162
|
+
if (error === sendCancelledError) return;
|
|
116
163
|
throw error;
|
|
117
164
|
}
|
|
165
|
+
if (lastFinishStatusRef.current === "error") return;
|
|
118
166
|
}
|
|
119
167
|
} } : {},
|
|
120
168
|
onCancel: () => {
|
|
169
|
+
sendEpochRef.current += 1;
|
|
121
170
|
agent.stop();
|
|
122
171
|
return Promise.resolve();
|
|
123
172
|
},
|
|
124
173
|
onRespondToToolApproval: async (response) => {
|
|
125
|
-
|
|
174
|
+
try {
|
|
175
|
+
await enqueueSend({ inputResponses: [toEveInputResponse(response)] });
|
|
176
|
+
} catch (error) {
|
|
177
|
+
if (error !== sendCancelledError) throw error;
|
|
178
|
+
}
|
|
126
179
|
}
|
|
127
180
|
});
|
|
128
181
|
};
|
|
@@ -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} from \"eve/react\";\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\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 agent = useEveAgent(agentOptions);\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 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 await agent.send({ message: getEveMessageContent(message) });\n },\n ...(stagedMessages\n ? {\n onReload: async (parentId: string | null) => {\n const stagedRun = getStagedRun(parentId);\n if (!stagedRun)\n throw new Error(\"Runtime does not support reloading messages.\");\n for (const { message: stagedMessage, input } of stagedRun) {\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 try {\n await agent.send({\n message: getEveMessageContent(input.message),\n });\n } catch (error) {\n stagedInputsRef.current.set(stagedMessage.id, input);\n messagesRef.current = previousMessages;\n setStagedMessages(previousMessages);\n throw error;\n }\n }\n },\n }\n : {}),\n onCancel: () => {\n agent.stop();\n return Promise.resolve();\n },\n onRespondToToolApproval: async (response) => {\n await agent.send({ inputResponses: [toEveInputResponse(response)] });\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 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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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@assistant-ui/eve",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.10",
|
|
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.8"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
34
|
"@types/react": "*",
|
|
@@ -90,6 +90,180 @@ describe("useEveAgentRuntime status forwarding", () => {
|
|
|
90
90
|
});
|
|
91
91
|
});
|
|
92
92
|
|
|
93
|
+
it("forwards runConfig to eve as one-turn client context when sending", async () => {
|
|
94
|
+
const agent = createAgent({
|
|
95
|
+
data: { messages: [] } satisfies EveMessageData,
|
|
96
|
+
});
|
|
97
|
+
mockUseEveAgent.mockReturnValue(agent as never);
|
|
98
|
+
|
|
99
|
+
const { result } = renderHook(() => useEveAgentRuntime());
|
|
100
|
+
|
|
101
|
+
act(() => {
|
|
102
|
+
result.current.thread.append({
|
|
103
|
+
role: "user",
|
|
104
|
+
content: [{ type: "text", text: "hello" }],
|
|
105
|
+
runConfig: { custom: { page: "/pricing" } },
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
await waitFor(() => {
|
|
110
|
+
expect(agent.send).toHaveBeenCalledWith({
|
|
111
|
+
message: "hello",
|
|
112
|
+
clientContext: { page: "/pricing" },
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("omits clientContext when no runConfig is provided", async () => {
|
|
118
|
+
const agent = createAgent({
|
|
119
|
+
data: { messages: [] } satisfies EveMessageData,
|
|
120
|
+
});
|
|
121
|
+
mockUseEveAgent.mockReturnValue(agent as never);
|
|
122
|
+
|
|
123
|
+
const { result } = renderHook(() => useEveAgentRuntime());
|
|
124
|
+
|
|
125
|
+
act(() => {
|
|
126
|
+
result.current.thread.append({
|
|
127
|
+
role: "user",
|
|
128
|
+
content: [{ type: "text", text: "hello" }],
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
await waitFor(() => {
|
|
133
|
+
expect(agent.send).toHaveBeenCalledWith({ message: "hello" });
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("omits clientContext when runConfig.custom is empty", async () => {
|
|
138
|
+
const agent = createAgent({
|
|
139
|
+
data: { messages: [] } satisfies EveMessageData,
|
|
140
|
+
});
|
|
141
|
+
mockUseEveAgent.mockReturnValue(agent as never);
|
|
142
|
+
|
|
143
|
+
const { result } = renderHook(() => useEveAgentRuntime());
|
|
144
|
+
|
|
145
|
+
act(() => {
|
|
146
|
+
result.current.thread.append({
|
|
147
|
+
role: "user",
|
|
148
|
+
content: [{ type: "text", text: "hello" }],
|
|
149
|
+
runConfig: { custom: {} },
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
await waitFor(() => {
|
|
154
|
+
expect(agent.send).toHaveBeenCalledWith({ message: "hello" });
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("prefers the reload-time runConfig over the staged one", async () => {
|
|
159
|
+
const agent = createAgent({
|
|
160
|
+
data: { messages: [] } satisfies EveMessageData,
|
|
161
|
+
});
|
|
162
|
+
mockUseEveAgent.mockReturnValue(agent as never);
|
|
163
|
+
|
|
164
|
+
const { result } = renderHook(() => useEveAgentRuntime());
|
|
165
|
+
|
|
166
|
+
act(() => {
|
|
167
|
+
result.current.thread.append({
|
|
168
|
+
role: "user",
|
|
169
|
+
content: [{ type: "text", text: "hello" }],
|
|
170
|
+
startRun: false,
|
|
171
|
+
runConfig: { custom: { page: "/staged" } },
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
await waitFor(() => {
|
|
176
|
+
expect(result.current.thread.getState().messages.length).toBe(1);
|
|
177
|
+
});
|
|
178
|
+
const stagedId = result.current.thread.getState().messages[0]!.id;
|
|
179
|
+
|
|
180
|
+
act(() => {
|
|
181
|
+
result.current.thread.startRun({
|
|
182
|
+
parentId: stagedId,
|
|
183
|
+
runConfig: { custom: { page: "/reload" } },
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
await waitFor(() => {
|
|
188
|
+
expect(agent.send).toHaveBeenCalledWith({
|
|
189
|
+
message: "hello",
|
|
190
|
+
clientContext: { page: "/reload" },
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("falls back to the staged runConfig when reload passes none", async () => {
|
|
196
|
+
const agent = createAgent({
|
|
197
|
+
data: { messages: [] } satisfies EveMessageData,
|
|
198
|
+
});
|
|
199
|
+
mockUseEveAgent.mockReturnValue(agent as never);
|
|
200
|
+
|
|
201
|
+
const { result } = renderHook(() => useEveAgentRuntime());
|
|
202
|
+
|
|
203
|
+
act(() => {
|
|
204
|
+
result.current.thread.append({
|
|
205
|
+
role: "user",
|
|
206
|
+
content: [{ type: "text", text: "hello" }],
|
|
207
|
+
startRun: false,
|
|
208
|
+
runConfig: { custom: { page: "/staged" } },
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
await waitFor(() => {
|
|
213
|
+
expect(result.current.thread.getState().messages.length).toBe(1);
|
|
214
|
+
});
|
|
215
|
+
const stagedId = result.current.thread.getState().messages[0]!.id;
|
|
216
|
+
|
|
217
|
+
act(() => {
|
|
218
|
+
result.current.thread.startRun({ parentId: stagedId });
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
await waitFor(() => {
|
|
222
|
+
expect(agent.send).toHaveBeenCalledWith({
|
|
223
|
+
message: "hello",
|
|
224
|
+
clientContext: { page: "/staged" },
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// Known limitation: core normalizes an omitted reload runConfig to {}
|
|
230
|
+
// (toStartRunConfig in core/src/runtime/api/thread-runtime.ts), so an
|
|
231
|
+
// explicit empty reload config is indistinguishable from an omitted one at
|
|
232
|
+
// the adapter and cannot clear the staged context.
|
|
233
|
+
it("cannot clear the staged runConfig with an explicit empty reload config", async () => {
|
|
234
|
+
const agent = createAgent({
|
|
235
|
+
data: { messages: [] } satisfies EveMessageData,
|
|
236
|
+
});
|
|
237
|
+
mockUseEveAgent.mockReturnValue(agent as never);
|
|
238
|
+
|
|
239
|
+
const { result } = renderHook(() => useEveAgentRuntime());
|
|
240
|
+
|
|
241
|
+
act(() => {
|
|
242
|
+
result.current.thread.append({
|
|
243
|
+
role: "user",
|
|
244
|
+
content: [{ type: "text", text: "hello" }],
|
|
245
|
+
startRun: false,
|
|
246
|
+
runConfig: { custom: { page: "/staged" } },
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
await waitFor(() => {
|
|
251
|
+
expect(result.current.thread.getState().messages.length).toBe(1);
|
|
252
|
+
});
|
|
253
|
+
const stagedId = result.current.thread.getState().messages[0]!.id;
|
|
254
|
+
|
|
255
|
+
act(() => {
|
|
256
|
+
result.current.thread.startRun({ parentId: stagedId, runConfig: {} });
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
await waitFor(() => {
|
|
260
|
+
expect(agent.send).toHaveBeenCalledWith({
|
|
261
|
+
message: "hello",
|
|
262
|
+
clientContext: { page: "/staged" },
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
93
267
|
it("keeps the last assistant message running while streaming", () => {
|
|
94
268
|
mockUseEveAgent.mockReturnValue(
|
|
95
269
|
createAgent({ status: "streaming" }) as never,
|
|
@@ -284,6 +458,46 @@ describe("useEveAgentRuntime staged messages", () => {
|
|
|
284
458
|
});
|
|
285
459
|
});
|
|
286
460
|
|
|
461
|
+
it("applies the reload runConfig only to the message being reloaded", async () => {
|
|
462
|
+
const agent = createAgent({ data: settledData });
|
|
463
|
+
mockUseEveAgent.mockReturnValue(agent as never);
|
|
464
|
+
const { result } = renderHook(() => useEveAgentRuntime());
|
|
465
|
+
|
|
466
|
+
const stageWithConfig = async (text: string, page: string) => {
|
|
467
|
+
await act(async () => {
|
|
468
|
+
result.current.thread.append({
|
|
469
|
+
role: "user",
|
|
470
|
+
content: [{ type: "text", text }],
|
|
471
|
+
startRun: false,
|
|
472
|
+
runConfig: { custom: { page } },
|
|
473
|
+
});
|
|
474
|
+
});
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
await stageWithConfig("first staged", "/first");
|
|
478
|
+
await stageWithConfig("second staged", "/second");
|
|
479
|
+
|
|
480
|
+
const secondStagedId = result.current.thread.getState().messages[3]!.id;
|
|
481
|
+
await act(async () => {
|
|
482
|
+
await result.current.thread.startRun({
|
|
483
|
+
parentId: secondStagedId,
|
|
484
|
+
sourceId: null,
|
|
485
|
+
runConfig: { custom: { page: "/reloaded" } },
|
|
486
|
+
});
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
// the earlier draft keeps the context it was staged with; only the
|
|
490
|
+
// reloaded message takes the reload-time config
|
|
491
|
+
expect(agent.send).toHaveBeenNthCalledWith(1, {
|
|
492
|
+
message: "first staged",
|
|
493
|
+
clientContext: { page: "/first" },
|
|
494
|
+
});
|
|
495
|
+
expect(agent.send).toHaveBeenNthCalledWith(2, {
|
|
496
|
+
message: "second staged",
|
|
497
|
+
clientContext: { page: "/reloaded" },
|
|
498
|
+
});
|
|
499
|
+
});
|
|
500
|
+
|
|
287
501
|
it("keeps later staged messages visible after promoting one staged parent", async () => {
|
|
288
502
|
const agent = createAgent({ data: settledData });
|
|
289
503
|
mockUseEveAgent.mockReturnValue(agent as never);
|
|
@@ -387,6 +601,88 @@ describe("useEveAgentRuntime staged messages", () => {
|
|
|
387
601
|
});
|
|
388
602
|
});
|
|
389
603
|
|
|
604
|
+
it("stops promoting once a promoted turn parks with an error status", async () => {
|
|
605
|
+
let capturedOptions: {
|
|
606
|
+
onFinish?: (snapshot: { status: string }) => void;
|
|
607
|
+
} = {};
|
|
608
|
+
const send = vi.fn().mockImplementation(async () => {
|
|
609
|
+
capturedOptions.onFinish?.({ status: "error" });
|
|
610
|
+
});
|
|
611
|
+
const agent = createAgent({ data: settledData, send });
|
|
612
|
+
mockUseEveAgent.mockImplementation((options) => {
|
|
613
|
+
capturedOptions = options as typeof capturedOptions;
|
|
614
|
+
return agent as never;
|
|
615
|
+
});
|
|
616
|
+
const { result } = renderHook(() => useEveAgentRuntime());
|
|
617
|
+
|
|
618
|
+
await stageMessage(result.current, "first staged");
|
|
619
|
+
await stageMessage(result.current, "second staged");
|
|
620
|
+
|
|
621
|
+
const secondStagedId = result.current.thread.getState().messages[3]!.id;
|
|
622
|
+
await act(async () => {
|
|
623
|
+
await result.current.thread.startRun({
|
|
624
|
+
parentId: secondStagedId,
|
|
625
|
+
sourceId: null,
|
|
626
|
+
runConfig: {},
|
|
627
|
+
});
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
expect(send).toHaveBeenCalledTimes(1);
|
|
631
|
+
expect(send).toHaveBeenCalledWith({ message: "first staged" });
|
|
632
|
+
await waitFor(() => {
|
|
633
|
+
expect(getText(result.current)).toEqual([
|
|
634
|
+
"earlier",
|
|
635
|
+
"earlier answer",
|
|
636
|
+
"second staged",
|
|
637
|
+
]);
|
|
638
|
+
});
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
it("stops promoting when the run is cancelled mid-prefix", async () => {
|
|
642
|
+
let resolveFirstSend!: () => void;
|
|
643
|
+
const send = vi
|
|
644
|
+
.fn()
|
|
645
|
+
.mockImplementationOnce(
|
|
646
|
+
() =>
|
|
647
|
+
new Promise<void>((resolve) => {
|
|
648
|
+
resolveFirstSend = resolve;
|
|
649
|
+
}),
|
|
650
|
+
)
|
|
651
|
+
.mockResolvedValue(undefined);
|
|
652
|
+
const agent = createAgent({ data: settledData, send });
|
|
653
|
+
mockUseEveAgent.mockReturnValue(agent as never);
|
|
654
|
+
const { result } = renderHook(() => useEveAgentRuntime());
|
|
655
|
+
|
|
656
|
+
await stageMessage(result.current, "first staged");
|
|
657
|
+
await stageMessage(result.current, "second staged");
|
|
658
|
+
|
|
659
|
+
const secondStagedId = result.current.thread.getState().messages[3]!.id;
|
|
660
|
+
act(() => {
|
|
661
|
+
void result.current.thread.startRun({
|
|
662
|
+
parentId: secondStagedId,
|
|
663
|
+
sourceId: null,
|
|
664
|
+
runConfig: {},
|
|
665
|
+
});
|
|
666
|
+
});
|
|
667
|
+
await waitFor(() => expect(send).toHaveBeenCalledTimes(1));
|
|
668
|
+
|
|
669
|
+
act(() => {
|
|
670
|
+
result.current.thread.cancelRun();
|
|
671
|
+
});
|
|
672
|
+
await act(async () => {
|
|
673
|
+
resolveFirstSend();
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
expect(send).toHaveBeenCalledTimes(1);
|
|
677
|
+
await waitFor(() => {
|
|
678
|
+
expect(getText(result.current)).toEqual([
|
|
679
|
+
"earlier",
|
|
680
|
+
"earlier answer",
|
|
681
|
+
"second staged",
|
|
682
|
+
]);
|
|
683
|
+
});
|
|
684
|
+
});
|
|
685
|
+
|
|
390
686
|
it("still rejects reloading a non-staged message", async () => {
|
|
391
687
|
const agent = createAgent({ data: settledData });
|
|
392
688
|
mockUseEveAgent.mockReturnValue(agent as never);
|
|
@@ -406,3 +702,261 @@ describe("useEveAgentRuntime staged messages", () => {
|
|
|
406
702
|
expect(agent.send).not.toHaveBeenCalled();
|
|
407
703
|
});
|
|
408
704
|
});
|
|
705
|
+
|
|
706
|
+
const approvalData: EveMessageData = {
|
|
707
|
+
messages: [
|
|
708
|
+
{ id: "u1", role: "user", parts: [{ type: "text", text: "send it" }] },
|
|
709
|
+
{
|
|
710
|
+
id: "a1",
|
|
711
|
+
role: "assistant",
|
|
712
|
+
parts: [
|
|
713
|
+
{
|
|
714
|
+
type: "dynamic-tool",
|
|
715
|
+
state: "approval-requested",
|
|
716
|
+
toolCallId: "call_1",
|
|
717
|
+
toolName: "send_email",
|
|
718
|
+
input: { to: "dev@example.com" },
|
|
719
|
+
approval: { id: "req_1" },
|
|
720
|
+
toolMetadata: {
|
|
721
|
+
eve: {
|
|
722
|
+
kind: "tool-call",
|
|
723
|
+
name: "send_email",
|
|
724
|
+
inputRequest: {
|
|
725
|
+
requestId: "req_1",
|
|
726
|
+
prompt: "Send the email?",
|
|
727
|
+
display: "confirmation",
|
|
728
|
+
options: [
|
|
729
|
+
{ id: "approve", label: "Approve" },
|
|
730
|
+
{ id: "deny", label: "Deny" },
|
|
731
|
+
],
|
|
732
|
+
},
|
|
733
|
+
},
|
|
734
|
+
},
|
|
735
|
+
},
|
|
736
|
+
],
|
|
737
|
+
},
|
|
738
|
+
],
|
|
739
|
+
};
|
|
740
|
+
|
|
741
|
+
describe("useEveAgentRuntime concurrent sends", () => {
|
|
742
|
+
it("defers an approval clicked while a turn is in flight until the turn parks", async () => {
|
|
743
|
+
let resolveFirstSend!: () => void;
|
|
744
|
+
const send = vi
|
|
745
|
+
.fn()
|
|
746
|
+
.mockImplementationOnce(
|
|
747
|
+
() =>
|
|
748
|
+
new Promise<void>((resolve) => {
|
|
749
|
+
resolveFirstSend = resolve;
|
|
750
|
+
}),
|
|
751
|
+
)
|
|
752
|
+
.mockResolvedValue(undefined);
|
|
753
|
+
const agent = createAgent({ data: approvalData, send });
|
|
754
|
+
mockUseEveAgent.mockReturnValue(agent as never);
|
|
755
|
+
const { result } = renderHook(() => useEveAgentRuntime());
|
|
756
|
+
|
|
757
|
+
await act(async () => {
|
|
758
|
+
result.current.thread.append({
|
|
759
|
+
role: "user",
|
|
760
|
+
content: [{ type: "text", text: "go" }],
|
|
761
|
+
});
|
|
762
|
+
});
|
|
763
|
+
await waitFor(() => expect(send).toHaveBeenCalledTimes(1));
|
|
764
|
+
|
|
765
|
+
act(() => {
|
|
766
|
+
result.current.thread
|
|
767
|
+
.getMessageByIndex(1)
|
|
768
|
+
.getMessagePartByToolCallId("call_1")
|
|
769
|
+
.respondToToolApproval({ optionId: "approve" });
|
|
770
|
+
});
|
|
771
|
+
expect(send).toHaveBeenCalledTimes(1);
|
|
772
|
+
|
|
773
|
+
await act(async () => {
|
|
774
|
+
resolveFirstSend();
|
|
775
|
+
});
|
|
776
|
+
await waitFor(() => expect(send).toHaveBeenCalledTimes(2));
|
|
777
|
+
expect(send).toHaveBeenNthCalledWith(2, {
|
|
778
|
+
inputResponses: [{ requestId: "req_1", optionId: "approve" }],
|
|
779
|
+
});
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
it("queues a send issued while a turn is in flight and preserves order", async () => {
|
|
783
|
+
let resolveFirstSend!: () => void;
|
|
784
|
+
const send = vi
|
|
785
|
+
.fn()
|
|
786
|
+
.mockImplementationOnce(
|
|
787
|
+
() =>
|
|
788
|
+
new Promise<void>((resolve) => {
|
|
789
|
+
resolveFirstSend = resolve;
|
|
790
|
+
}),
|
|
791
|
+
)
|
|
792
|
+
.mockResolvedValue(undefined);
|
|
793
|
+
const agent = createAgent({ data: settledData, send });
|
|
794
|
+
mockUseEveAgent.mockReturnValue(agent as never);
|
|
795
|
+
const { result } = renderHook(() => useEveAgentRuntime());
|
|
796
|
+
|
|
797
|
+
await act(async () => {
|
|
798
|
+
result.current.thread.append({
|
|
799
|
+
role: "user",
|
|
800
|
+
content: [{ type: "text", text: "first" }],
|
|
801
|
+
});
|
|
802
|
+
});
|
|
803
|
+
await waitFor(() => expect(send).toHaveBeenCalledTimes(1));
|
|
804
|
+
|
|
805
|
+
await act(async () => {
|
|
806
|
+
result.current.thread.append({
|
|
807
|
+
role: "user",
|
|
808
|
+
content: [{ type: "text", text: "second" }],
|
|
809
|
+
});
|
|
810
|
+
});
|
|
811
|
+
expect(send).toHaveBeenCalledTimes(1);
|
|
812
|
+
|
|
813
|
+
await act(async () => {
|
|
814
|
+
resolveFirstSend();
|
|
815
|
+
});
|
|
816
|
+
await waitFor(() => expect(send).toHaveBeenCalledTimes(2));
|
|
817
|
+
expect(send).toHaveBeenNthCalledWith(1, { message: "first" });
|
|
818
|
+
expect(send).toHaveBeenNthCalledWith(2, { message: "second" });
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
it("drops a queued send when the run is cancelled before it dispatches", async () => {
|
|
822
|
+
let resolveFirstSend!: () => void;
|
|
823
|
+
const send = vi
|
|
824
|
+
.fn()
|
|
825
|
+
.mockImplementationOnce(
|
|
826
|
+
() =>
|
|
827
|
+
new Promise<void>((resolve) => {
|
|
828
|
+
resolveFirstSend = resolve;
|
|
829
|
+
}),
|
|
830
|
+
)
|
|
831
|
+
.mockResolvedValue(undefined);
|
|
832
|
+
const agent = createAgent({ data: settledData, send });
|
|
833
|
+
mockUseEveAgent.mockReturnValue(agent as never);
|
|
834
|
+
const { result } = renderHook(() => useEveAgentRuntime());
|
|
835
|
+
|
|
836
|
+
await act(async () => {
|
|
837
|
+
result.current.thread.append({
|
|
838
|
+
role: "user",
|
|
839
|
+
content: [{ type: "text", text: "first" }],
|
|
840
|
+
});
|
|
841
|
+
});
|
|
842
|
+
await waitFor(() => expect(send).toHaveBeenCalledTimes(1));
|
|
843
|
+
|
|
844
|
+
await act(async () => {
|
|
845
|
+
result.current.thread.append({
|
|
846
|
+
role: "user",
|
|
847
|
+
content: [{ type: "text", text: "queued" }],
|
|
848
|
+
});
|
|
849
|
+
});
|
|
850
|
+
act(() => {
|
|
851
|
+
result.current.thread.cancelRun();
|
|
852
|
+
});
|
|
853
|
+
expect(agent.stop).toHaveBeenCalledTimes(1);
|
|
854
|
+
|
|
855
|
+
await act(async () => {
|
|
856
|
+
resolveFirstSend();
|
|
857
|
+
});
|
|
858
|
+
expect(send).toHaveBeenCalledTimes(1);
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
it("drops a queued send when the hook unmounts before it dispatches", async () => {
|
|
862
|
+
let resolveFirstSend!: () => void;
|
|
863
|
+
const send = vi
|
|
864
|
+
.fn()
|
|
865
|
+
.mockImplementationOnce(
|
|
866
|
+
() =>
|
|
867
|
+
new Promise<void>((resolve) => {
|
|
868
|
+
resolveFirstSend = resolve;
|
|
869
|
+
}),
|
|
870
|
+
)
|
|
871
|
+
.mockResolvedValue(undefined);
|
|
872
|
+
const agent = createAgent({ data: settledData, send });
|
|
873
|
+
mockUseEveAgent.mockReturnValue(agent as never);
|
|
874
|
+
const { result, unmount } = renderHook(() => useEveAgentRuntime());
|
|
875
|
+
|
|
876
|
+
await act(async () => {
|
|
877
|
+
result.current.thread.append({
|
|
878
|
+
role: "user",
|
|
879
|
+
content: [{ type: "text", text: "first" }],
|
|
880
|
+
});
|
|
881
|
+
});
|
|
882
|
+
await waitFor(() => expect(send).toHaveBeenCalledTimes(1));
|
|
883
|
+
|
|
884
|
+
await act(async () => {
|
|
885
|
+
result.current.thread.append({
|
|
886
|
+
role: "user",
|
|
887
|
+
content: [{ type: "text", text: "queued" }],
|
|
888
|
+
});
|
|
889
|
+
});
|
|
890
|
+
unmount();
|
|
891
|
+
|
|
892
|
+
resolveFirstSend();
|
|
893
|
+
await Promise.resolve();
|
|
894
|
+
await Promise.resolve();
|
|
895
|
+
expect(send).toHaveBeenCalledTimes(1);
|
|
896
|
+
});
|
|
897
|
+
|
|
898
|
+
it("forwards onFinish snapshots to the caller's onFinish", () => {
|
|
899
|
+
let capturedOptions: {
|
|
900
|
+
onFinish?: (snapshot: { status: string }) => void;
|
|
901
|
+
} = {};
|
|
902
|
+
const agent = createAgent({ data: settledData });
|
|
903
|
+
mockUseEveAgent.mockImplementation((options) => {
|
|
904
|
+
capturedOptions = options as typeof capturedOptions;
|
|
905
|
+
return agent as never;
|
|
906
|
+
});
|
|
907
|
+
const onFinish = vi.fn();
|
|
908
|
+
renderHook(() => useEveAgentRuntime({ onFinish }));
|
|
909
|
+
|
|
910
|
+
capturedOptions.onFinish?.({ status: "ready" });
|
|
911
|
+
|
|
912
|
+
expect(onFinish).toHaveBeenCalledWith({ status: "ready" });
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
it("orders a send issued mid-promotion after the in-flight staged draft", async () => {
|
|
916
|
+
let resolveFirstSend!: () => void;
|
|
917
|
+
const send = vi
|
|
918
|
+
.fn()
|
|
919
|
+
.mockImplementationOnce(
|
|
920
|
+
() =>
|
|
921
|
+
new Promise<void>((resolve) => {
|
|
922
|
+
resolveFirstSend = resolve;
|
|
923
|
+
}),
|
|
924
|
+
)
|
|
925
|
+
.mockResolvedValue(undefined);
|
|
926
|
+
const agent = createAgent({ data: settledData, send });
|
|
927
|
+
mockUseEveAgent.mockReturnValue(agent as never);
|
|
928
|
+
const { result } = renderHook(() => useEveAgentRuntime());
|
|
929
|
+
|
|
930
|
+
await stageMessage(result.current, "first staged");
|
|
931
|
+
await stageMessage(result.current, "second staged");
|
|
932
|
+
|
|
933
|
+
const secondStagedId = result.current.thread.getState().messages[3]!.id;
|
|
934
|
+
act(() => {
|
|
935
|
+
void result.current.thread.startRun({
|
|
936
|
+
parentId: secondStagedId,
|
|
937
|
+
sourceId: null,
|
|
938
|
+
runConfig: {},
|
|
939
|
+
});
|
|
940
|
+
});
|
|
941
|
+
await waitFor(() => expect(send).toHaveBeenCalledTimes(1));
|
|
942
|
+
|
|
943
|
+
await act(async () => {
|
|
944
|
+
result.current.thread.append({
|
|
945
|
+
role: "user",
|
|
946
|
+
content: [{ type: "text", text: "interleaved" }],
|
|
947
|
+
});
|
|
948
|
+
});
|
|
949
|
+
expect(send).toHaveBeenCalledTimes(1);
|
|
950
|
+
|
|
951
|
+
await act(async () => {
|
|
952
|
+
resolveFirstSend();
|
|
953
|
+
});
|
|
954
|
+
await waitFor(() => expect(send).toHaveBeenCalledTimes(3));
|
|
955
|
+
|
|
956
|
+
expect(send.mock.calls.map(([payload]) => payload)).toEqual([
|
|
957
|
+
{ message: "first staged" },
|
|
958
|
+
{ message: "interleaved" },
|
|
959
|
+
{ message: "second staged" },
|
|
960
|
+
]);
|
|
961
|
+
});
|
|
962
|
+
});
|
|
@@ -23,7 +23,9 @@ import {
|
|
|
23
23
|
useEveAgent,
|
|
24
24
|
type EveMessageData,
|
|
25
25
|
type UseEveAgentOptions,
|
|
26
|
+
type UseEveAgentStatus,
|
|
26
27
|
} from "eve/react";
|
|
28
|
+
import type { SendTurnPayload } from "eve/client";
|
|
27
29
|
import {
|
|
28
30
|
convertEveMessages,
|
|
29
31
|
getEveMessageContent,
|
|
@@ -35,6 +37,32 @@ const USER_STAGED_STATUS = {
|
|
|
35
37
|
reason: "unknown",
|
|
36
38
|
} as const;
|
|
37
39
|
|
|
40
|
+
const sendCancelledError = new Error(
|
|
41
|
+
"eve send was dropped because the run was cancelled.",
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const hasRunConfig = (
|
|
45
|
+
runConfig: AppendMessage["runConfig"],
|
|
46
|
+
): runConfig is NonNullable<AppendMessage["runConfig"]> =>
|
|
47
|
+
runConfig?.custom !== undefined && Object.keys(runConfig.custom).length > 0;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Only the `custom` bag crosses the wire. Eve reads `clientContext` as its own
|
|
51
|
+
* namespace and serializes it into a model-visible context message, so sending
|
|
52
|
+
* the assistant-ui envelope would surface a literal `"custom"` key in the
|
|
53
|
+
* prompt and to every eve-side handler.
|
|
54
|
+
*/
|
|
55
|
+
const toEveClientContext = (
|
|
56
|
+
runConfig: AppendMessage["runConfig"],
|
|
57
|
+
): Pick<SendTurnPayload, "clientContext"> =>
|
|
58
|
+
hasRunConfig(runConfig)
|
|
59
|
+
? {
|
|
60
|
+
clientContext: runConfig.custom as NonNullable<
|
|
61
|
+
SendTurnPayload["clientContext"]
|
|
62
|
+
>,
|
|
63
|
+
}
|
|
64
|
+
: {};
|
|
65
|
+
|
|
38
66
|
export type UseEveAgentRuntimeOptions = Omit<
|
|
39
67
|
UseEveAgentOptions<EveMessageData>,
|
|
40
68
|
"reducer"
|
|
@@ -72,7 +100,15 @@ export const useEveAgentRuntime = (options: UseEveAgentRuntimeOptions = {}) => {
|
|
|
72
100
|
? true
|
|
73
101
|
: never;
|
|
74
102
|
|
|
75
|
-
const
|
|
103
|
+
const { onFinish } = agentOptions;
|
|
104
|
+
const lastFinishStatusRef = useRef<UseEveAgentStatus | null>(null);
|
|
105
|
+
const agent = useEveAgent({
|
|
106
|
+
...agentOptions,
|
|
107
|
+
onFinish: (snapshot) => {
|
|
108
|
+
lastFinishStatusRef.current = snapshot.status;
|
|
109
|
+
onFinish?.(snapshot);
|
|
110
|
+
},
|
|
111
|
+
});
|
|
76
112
|
const runtimeAdapters = useRuntimeAdapters();
|
|
77
113
|
const [toolStatuses, setToolStatuses] = useState<
|
|
78
114
|
Record<string, ToolExecutionStatus>
|
|
@@ -123,6 +159,31 @@ export const useEveAgentRuntime = (options: UseEveAgentRuntimeOptions = {}) => {
|
|
|
123
159
|
const messagesRef = useRef(messages);
|
|
124
160
|
messagesRef.current = messages;
|
|
125
161
|
|
|
162
|
+
// Upstream `EveAgentStore.send` rejects while a turn is in flight and only
|
|
163
|
+
// resolves once the turn's stream parks, so a pending chain link is exactly
|
|
164
|
+
// an active turn; chaining every send serializes them without watching
|
|
165
|
+
// status.
|
|
166
|
+
const sendChainRef = useRef<Promise<void>>(Promise.resolve());
|
|
167
|
+
const sendEpochRef = useRef(0);
|
|
168
|
+
|
|
169
|
+
const enqueueSend = (payload: Parameters<typeof agent.send>[0]) => {
|
|
170
|
+
const epoch = sendEpochRef.current;
|
|
171
|
+
const next = sendChainRef.current.then(() => {
|
|
172
|
+
if (epoch !== sendEpochRef.current) throw sendCancelledError;
|
|
173
|
+
return agent.send(payload);
|
|
174
|
+
});
|
|
175
|
+
sendChainRef.current = next.catch(() => {});
|
|
176
|
+
return next;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
// 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.
|
|
181
|
+
useEffect(() => {
|
|
182
|
+
return () => {
|
|
183
|
+
sendEpochRef.current += 1;
|
|
184
|
+
};
|
|
185
|
+
}, []);
|
|
186
|
+
|
|
126
187
|
useEffect(() => {
|
|
127
188
|
if (stagedInputsRef.current.size === 0) return;
|
|
128
189
|
const baseIds = new Set(convertedMessages.map((message) => message.id));
|
|
@@ -182,15 +243,24 @@ export const useEveAgentRuntime = (options: UseEveAgentRuntimeOptions = {}) => {
|
|
|
182
243
|
stageUserMessage(message);
|
|
183
244
|
return;
|
|
184
245
|
}
|
|
185
|
-
|
|
246
|
+
try {
|
|
247
|
+
await enqueueSend({
|
|
248
|
+
message: getEveMessageContent(message),
|
|
249
|
+
...toEveClientContext(message.runConfig),
|
|
250
|
+
});
|
|
251
|
+
} catch (error) {
|
|
252
|
+
if (error !== sendCancelledError) throw error;
|
|
253
|
+
}
|
|
186
254
|
},
|
|
187
255
|
...(stagedMessages
|
|
188
256
|
? {
|
|
189
|
-
onReload: async (parentId: string | null) => {
|
|
257
|
+
onReload: async (parentId: string | null, config) => {
|
|
190
258
|
const stagedRun = getStagedRun(parentId);
|
|
191
259
|
if (!stagedRun)
|
|
192
260
|
throw new Error("Runtime does not support reloading messages.");
|
|
261
|
+
const epoch = sendEpochRef.current;
|
|
193
262
|
for (const { message: stagedMessage, input } of stagedRun) {
|
|
263
|
+
if (epoch !== sendEpochRef.current) return;
|
|
194
264
|
const previousMessages = messagesRef.current;
|
|
195
265
|
stagedInputsRef.current.delete(stagedMessage.id);
|
|
196
266
|
const nextMessages = previousMessages.filter(
|
|
@@ -200,26 +270,41 @@ export const useEveAgentRuntime = (options: UseEveAgentRuntimeOptions = {}) => {
|
|
|
200
270
|
setStagedMessages(
|
|
201
271
|
stagedInputsRef.current.size > 0 ? nextMessages : null,
|
|
202
272
|
);
|
|
273
|
+
// The reload config belongs to the message being reloaded; the
|
|
274
|
+
// drafts promoted ahead of it keep the config they were staged
|
|
275
|
+
// with, or reloading the tail would rewrite their context too.
|
|
276
|
+
const runConfig =
|
|
277
|
+
stagedMessage.id === parentId && hasRunConfig(config.runConfig)
|
|
278
|
+
? config.runConfig
|
|
279
|
+
: input.runConfig;
|
|
203
280
|
try {
|
|
204
|
-
await
|
|
281
|
+
await enqueueSend({
|
|
205
282
|
message: getEveMessageContent(input.message),
|
|
283
|
+
...toEveClientContext(runConfig),
|
|
206
284
|
});
|
|
207
285
|
} catch (error) {
|
|
208
286
|
stagedInputsRef.current.set(stagedMessage.id, input);
|
|
209
287
|
messagesRef.current = previousMessages;
|
|
210
288
|
setStagedMessages(previousMessages);
|
|
289
|
+
if (error === sendCancelledError) return;
|
|
211
290
|
throw error;
|
|
212
291
|
}
|
|
292
|
+
if (lastFinishStatusRef.current === "error") return;
|
|
213
293
|
}
|
|
214
294
|
},
|
|
215
295
|
}
|
|
216
296
|
: {}),
|
|
217
297
|
onCancel: () => {
|
|
298
|
+
sendEpochRef.current += 1;
|
|
218
299
|
agent.stop();
|
|
219
300
|
return Promise.resolve();
|
|
220
301
|
},
|
|
221
302
|
onRespondToToolApproval: async (response) => {
|
|
222
|
-
|
|
303
|
+
try {
|
|
304
|
+
await enqueueSend({ inputResponses: [toEveInputResponse(response)] });
|
|
305
|
+
} catch (error) {
|
|
306
|
+
if (error !== sendCancelledError) throw error;
|
|
307
|
+
}
|
|
223
308
|
},
|
|
224
309
|
});
|
|
225
310
|
};
|