@assistant-ui/eve 0.0.0
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.
- package/LICENSE +21 -0
- package/README.md +20 -0
- package/dist/convertEveMessages.d.ts +32 -0
- package/dist/convertEveMessages.d.ts.map +1 -0
- package/dist/convertEveMessages.js +191 -0
- package/dist/convertEveMessages.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/useEveAgentRuntime.d.ts +24 -0
- package/dist/useEveAgentRuntime.d.ts.map +1 -0
- package/dist/useEveAgentRuntime.js +66 -0
- package/dist/useEveAgentRuntime.js.map +1 -0
- package/package.json +68 -0
- package/src/convertEveMessages.test.ts +193 -0
- package/src/convertEveMessages.ts +345 -0
- package/src/index.ts +10 -0
- package/src/useEveAgentRuntime.ts +127 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 AgentbaseAI Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# @assistant-ui/eve
|
|
2
|
+
|
|
3
|
+
Eve runtime adapter for assistant-ui.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
"use client";
|
|
7
|
+
|
|
8
|
+
import { AssistantRuntimeProvider } from "@assistant-ui/react";
|
|
9
|
+
import { useEveAgentRuntime } from "@assistant-ui/eve";
|
|
10
|
+
|
|
11
|
+
export function RuntimeProvider({ children }: { children: React.ReactNode }) {
|
|
12
|
+
const runtime = useEveAgentRuntime();
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<AssistantRuntimeProvider runtime={runtime}>
|
|
16
|
+
{children}
|
|
17
|
+
</AssistantRuntimeProvider>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
```
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { AppendMessage, RespondToToolApprovalOptions, ThreadMessage } from "@assistant-ui/core";
|
|
2
|
+
import { EveMessage, EveMessageData, InputResponse, SendTurnPayload } from "eve/react";
|
|
3
|
+
|
|
4
|
+
//#region src/convertEveMessages.d.ts
|
|
5
|
+
type ConvertEveMessagesOptions = {
|
|
6
|
+
/**
|
|
7
|
+
* Marks the last assistant message as running while Eve is submitting or
|
|
8
|
+
* streaming.
|
|
9
|
+
*/
|
|
10
|
+
readonly isRunning?: boolean | undefined;
|
|
11
|
+
readonly getCreatedAt?: ((message: EveMessage) => Date) | undefined;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Converts a single Eve message into an assistant-ui thread message.
|
|
15
|
+
*/
|
|
16
|
+
declare const convertEveMessage: (message: EveMessage, index: number, messages: readonly EveMessage[], options?: ConvertEveMessagesOptions) => ThreadMessage;
|
|
17
|
+
/**
|
|
18
|
+
* Converts the full Eve message data object into assistant-ui thread messages.
|
|
19
|
+
*/
|
|
20
|
+
declare const convertEveMessages: (data: EveMessageData, options?: ConvertEveMessagesOptions) => ThreadMessage[];
|
|
21
|
+
/**
|
|
22
|
+
* Converts an assistant-ui append message into the message payload accepted by
|
|
23
|
+
* Eve's `send` API.
|
|
24
|
+
*/
|
|
25
|
+
declare const getEveMessageContent: (message: AppendMessage) => NonNullable<SendTurnPayload["message"]>;
|
|
26
|
+
/**
|
|
27
|
+
* Converts an assistant-ui tool approval response into an Eve input response.
|
|
28
|
+
*/
|
|
29
|
+
declare const toEveInputResponse: (response: RespondToToolApprovalOptions) => InputResponse;
|
|
30
|
+
//#endregion
|
|
31
|
+
export { ConvertEveMessagesOptions, convertEveMessage, convertEveMessages, getEveMessageContent, toEveInputResponse };
|
|
32
|
+
//# sourceMappingURL=convertEveMessages.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"convertEveMessages.d.ts","names":[],"sources":["../src/convertEveMessages.ts"],"mappings":";;;;KAmCY,yBAAA;;AAAZ;;;WAKW,SAAA;EAAA,SACA,YAAA,KAAiB,OAAA,EAAS,UAAA,KAAe,IAAI;AAAA;;;;cA0K3C,iBAAA,GACX,OAAA,EAAS,UAAA,EACT,KAAA,UACA,QAAA,WAAmB,UAAA,IACnB,OAAA,GAAS,yBAAA,KACR,aAAA;AA/KqD;AA0KxD;;AA1KwD,cAsO3C,kBAAA,GACX,IAAA,EAAM,cAAA,EACN,OAAA,GAAS,yBAAA,KACR,aAAA;;;;;cASU,oBAAA,GACX,OAAA,EAAS,aAAA,KACR,WAAA,CAAY,eAAA;;;;cAqDF,kBAAA,GACX,QAAA,EAAU,4BAAA,KACT,aAID"}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
//#region src/convertEveMessages.ts
|
|
2
|
+
const ASSISTANT_COMPLETE_STATUS = {
|
|
3
|
+
type: "complete",
|
|
4
|
+
reason: "stop"
|
|
5
|
+
};
|
|
6
|
+
const ASSISTANT_RUNNING_STATUS = { type: "running" };
|
|
7
|
+
const USER_FALLBACK_STATUS = {
|
|
8
|
+
type: "complete",
|
|
9
|
+
reason: "unknown"
|
|
10
|
+
};
|
|
11
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
12
|
+
const toJsonObject = (value) => {
|
|
13
|
+
if (isRecord(value)) return value;
|
|
14
|
+
if (value === void 0) return {};
|
|
15
|
+
return { value };
|
|
16
|
+
};
|
|
17
|
+
const stringifyArgs = (value) => {
|
|
18
|
+
try {
|
|
19
|
+
return JSON.stringify(value ?? {});
|
|
20
|
+
} catch {
|
|
21
|
+
return "";
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
const toMessageStatus = (message, index, messages, options) => {
|
|
25
|
+
if (message.role !== "assistant") return USER_FALLBACK_STATUS;
|
|
26
|
+
if (message.parts.some((part) => part.type === "dynamic-tool" && part.state === "approval-requested")) return {
|
|
27
|
+
type: "requires-action",
|
|
28
|
+
reason: "tool-calls"
|
|
29
|
+
};
|
|
30
|
+
if (message.metadata?.status === "failed") return {
|
|
31
|
+
type: "incomplete",
|
|
32
|
+
reason: "error"
|
|
33
|
+
};
|
|
34
|
+
if (message.metadata?.status === "streaming" || options.isRunning === true && index === messages.length - 1) return ASSISTANT_RUNNING_STATUS;
|
|
35
|
+
return ASSISTANT_COMPLETE_STATUS;
|
|
36
|
+
};
|
|
37
|
+
const toolApprovalOptionsFromInputRequest = (inputRequest) => {
|
|
38
|
+
const options = inputRequest?.options;
|
|
39
|
+
if (!options || options.length === 0) return void 0;
|
|
40
|
+
return options.map((option) => ({
|
|
41
|
+
id: option.id,
|
|
42
|
+
kind: option.id === "approve" ? "allow-once" : option.id === "deny" ? "reject-once" : `_${option.id}`,
|
|
43
|
+
...option.label && { label: option.label },
|
|
44
|
+
...option.description && { description: option.description }
|
|
45
|
+
}));
|
|
46
|
+
};
|
|
47
|
+
const toApproval = (part) => {
|
|
48
|
+
if (part.state !== "approval-requested" && part.state !== "approval-responded" && part.state !== "output-available" && part.state !== "output-error" && part.state !== "output-denied") return;
|
|
49
|
+
const approval = part.approval;
|
|
50
|
+
if (!approval) return void 0;
|
|
51
|
+
const options = toolApprovalOptionsFromInputRequest(part.toolMetadata?.eve?.inputRequest);
|
|
52
|
+
return {
|
|
53
|
+
id: approval.id,
|
|
54
|
+
...approval.approved !== void 0 && { approved: approval.approved },
|
|
55
|
+
...approval.reason && { reason: approval.reason },
|
|
56
|
+
...approval.isAutomatic !== void 0 && { isAutomatic: approval.isAutomatic },
|
|
57
|
+
...options && { options }
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
const convertDynamicToolPart = (part) => {
|
|
61
|
+
const approval = toApproval(part);
|
|
62
|
+
const toolCall = {
|
|
63
|
+
type: "tool-call",
|
|
64
|
+
toolCallId: part.toolCallId,
|
|
65
|
+
toolName: part.toolName,
|
|
66
|
+
args: toJsonObject(part.input),
|
|
67
|
+
argsText: stringifyArgs(part.input),
|
|
68
|
+
...approval && { approval }
|
|
69
|
+
};
|
|
70
|
+
switch (part.state) {
|
|
71
|
+
case "output-available": return {
|
|
72
|
+
...toolCall,
|
|
73
|
+
result: part.output
|
|
74
|
+
};
|
|
75
|
+
case "output-error": return {
|
|
76
|
+
...toolCall,
|
|
77
|
+
result: { error: part.errorText },
|
|
78
|
+
isError: true
|
|
79
|
+
};
|
|
80
|
+
case "output-denied": return {
|
|
81
|
+
...toolCall,
|
|
82
|
+
result: { error: part.approval?.reason ?? "Tool approval denied" },
|
|
83
|
+
isError: true
|
|
84
|
+
};
|
|
85
|
+
default: return toolCall;
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
const convertAssistantPart = (part) => {
|
|
89
|
+
switch (part.type) {
|
|
90
|
+
case "text":
|
|
91
|
+
case "reasoning": return {
|
|
92
|
+
type: part.type,
|
|
93
|
+
text: part.text
|
|
94
|
+
};
|
|
95
|
+
case "step-start": return null;
|
|
96
|
+
case "dynamic-tool": return convertDynamicToolPart(part);
|
|
97
|
+
default: return null;
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
const convertUserPart = (part) => {
|
|
101
|
+
switch (part.type) {
|
|
102
|
+
case "text": return {
|
|
103
|
+
type: "text",
|
|
104
|
+
text: part.text
|
|
105
|
+
};
|
|
106
|
+
default: return null;
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
/**
|
|
110
|
+
* Converts a single Eve message into an assistant-ui thread message.
|
|
111
|
+
*/
|
|
112
|
+
const convertEveMessage = (message, index, messages, options = {}) => {
|
|
113
|
+
const createdAt = options.getCreatedAt?.(message) ?? /* @__PURE__ */ new Date();
|
|
114
|
+
const metadata = {
|
|
115
|
+
...message.metadata?.optimistic && { isOptimistic: true },
|
|
116
|
+
custom: { ...message.metadata ?? {} }
|
|
117
|
+
};
|
|
118
|
+
const content = message.role === "assistant" ? message.parts.map(convertAssistantPart).filter((part) => part !== null) : message.parts.map(convertUserPart).filter((part) => part !== null);
|
|
119
|
+
const fallbackTextPart = {
|
|
120
|
+
type: "text",
|
|
121
|
+
text: ""
|
|
122
|
+
};
|
|
123
|
+
if (message.role === "user") return {
|
|
124
|
+
id: message.id,
|
|
125
|
+
createdAt,
|
|
126
|
+
role: "user",
|
|
127
|
+
content: content.length > 0 ? content : [fallbackTextPart],
|
|
128
|
+
attachments: [],
|
|
129
|
+
metadata
|
|
130
|
+
};
|
|
131
|
+
return {
|
|
132
|
+
id: message.id,
|
|
133
|
+
createdAt,
|
|
134
|
+
role: "assistant",
|
|
135
|
+
content: content.length > 0 ? content : [],
|
|
136
|
+
status: toMessageStatus(message, index, messages, options),
|
|
137
|
+
metadata: {
|
|
138
|
+
unstable_state: null,
|
|
139
|
+
unstable_annotations: [],
|
|
140
|
+
unstable_data: [],
|
|
141
|
+
steps: [],
|
|
142
|
+
...metadata
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
};
|
|
146
|
+
/**
|
|
147
|
+
* Converts the full Eve message data object into assistant-ui thread messages.
|
|
148
|
+
*/
|
|
149
|
+
const convertEveMessages = (data, options = {}) => data.messages.map((message, index, messages) => convertEveMessage(message, index, messages, options));
|
|
150
|
+
/**
|
|
151
|
+
* Converts an assistant-ui append message into the message payload accepted by
|
|
152
|
+
* Eve's `send` API.
|
|
153
|
+
*/
|
|
154
|
+
const getEveMessageContent = (message) => {
|
|
155
|
+
const parts = [...message.content, ...message.attachments?.flatMap((attachment) => attachment.content) ?? []].map((part) => {
|
|
156
|
+
const type = part.type;
|
|
157
|
+
switch (type) {
|
|
158
|
+
case "text": return {
|
|
159
|
+
type: "text",
|
|
160
|
+
text: part.text
|
|
161
|
+
};
|
|
162
|
+
case "file": return {
|
|
163
|
+
type: "file",
|
|
164
|
+
data: part.data,
|
|
165
|
+
mediaType: part.mimeType,
|
|
166
|
+
...part.filename && { filename: part.filename }
|
|
167
|
+
};
|
|
168
|
+
case "image": return {
|
|
169
|
+
type: "file",
|
|
170
|
+
data: part.image,
|
|
171
|
+
mediaType: "image/*",
|
|
172
|
+
...part.filename && { filename: part.filename }
|
|
173
|
+
};
|
|
174
|
+
default: throw new Error(`Unsupported eve append message part type: ${type}`);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
if (parts.length === 1 && parts[0]?.type === "text") return parts[0].text;
|
|
178
|
+
return parts;
|
|
179
|
+
};
|
|
180
|
+
/**
|
|
181
|
+
* Converts an assistant-ui tool approval response into an Eve input response.
|
|
182
|
+
*/
|
|
183
|
+
const toEveInputResponse = (response) => ({
|
|
184
|
+
requestId: response.approvalId,
|
|
185
|
+
optionId: response.optionId ?? (response.approved ? "approve" : "deny"),
|
|
186
|
+
...response.reason && { text: response.reason }
|
|
187
|
+
});
|
|
188
|
+
//#endregion
|
|
189
|
+
export { convertEveMessage, convertEveMessages, getEveMessageContent, toEveInputResponse };
|
|
190
|
+
|
|
191
|
+
//# sourceMappingURL=convertEveMessages.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"convertEveMessages.js","names":["_exhaustiveCheck"],"sources":["../src/convertEveMessages.ts"],"sourcesContent":["import type {\n AppendMessage,\n MessageStatus,\n RespondToToolApprovalOptions,\n TextMessagePart,\n ThreadAssistantMessagePart,\n ThreadMessage,\n ThreadUserMessagePart,\n ToolApprovalOption,\n ToolCallMessagePart,\n} from \"@assistant-ui/core\";\nimport type {\n EveDynamicToolPart,\n EveMessage,\n EveMessageData,\n EveMessageInputRequest,\n EveMessagePart,\n InputResponse,\n SendTurnPayload,\n} from \"eve/react\";\n\nconst ASSISTANT_COMPLETE_STATUS = {\n type: \"complete\",\n reason: \"stop\",\n} satisfies MessageStatus;\n\nconst ASSISTANT_RUNNING_STATUS = {\n type: \"running\",\n} satisfies MessageStatus;\n\nconst USER_FALLBACK_STATUS = {\n type: \"complete\",\n reason: \"unknown\",\n} satisfies MessageStatus;\n\nexport type ConvertEveMessagesOptions = {\n /**\n * Marks the last assistant message as running while Eve is submitting or\n * streaming.\n */\n readonly isRunning?: boolean | undefined;\n readonly getCreatedAt?: ((message: EveMessage) => Date) | undefined;\n};\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === \"object\" && value !== null && !Array.isArray(value);\n\nconst toJsonObject = (value: unknown): Record<string, unknown> => {\n if (isRecord(value)) return value;\n if (value === undefined) return {};\n return { value };\n};\n\nconst stringifyArgs = (value: unknown): string => {\n try {\n return JSON.stringify(value ?? {});\n } catch {\n return \"\";\n }\n};\n\nconst toMessageStatus = (\n message: EveMessage,\n index: number,\n messages: readonly EveMessage[],\n options: ConvertEveMessagesOptions,\n): MessageStatus => {\n if (message.role !== \"assistant\") return USER_FALLBACK_STATUS;\n\n const hasPendingApproval = message.parts.some(\n (part) =>\n part.type === \"dynamic-tool\" && part.state === \"approval-requested\",\n );\n\n if (hasPendingApproval) {\n return { type: \"requires-action\", reason: \"tool-calls\" };\n }\n\n if (message.metadata?.status === \"failed\") {\n return { type: \"incomplete\", reason: \"error\" };\n }\n\n if (\n message.metadata?.status === \"streaming\" ||\n (options.isRunning === true && index === messages.length - 1)\n ) {\n return ASSISTANT_RUNNING_STATUS;\n }\n\n return ASSISTANT_COMPLETE_STATUS;\n};\n\nconst toolApprovalOptionsFromInputRequest = (\n inputRequest: EveMessageInputRequest | undefined,\n): readonly ToolApprovalOption[] | undefined => {\n const options = inputRequest?.options;\n if (!options || options.length === 0) return undefined;\n\n return options.map((option) => ({\n id: option.id,\n kind:\n option.id === \"approve\"\n ? \"allow-once\"\n : option.id === \"deny\"\n ? \"reject-once\"\n : `_${option.id}`,\n ...(option.label && { label: option.label }),\n ...(option.description && { description: option.description }),\n }));\n};\n\nconst toApproval = (\n part: EveDynamicToolPart,\n): ToolCallMessagePart[\"approval\"] | undefined => {\n if (\n part.state !== \"approval-requested\" &&\n part.state !== \"approval-responded\" &&\n part.state !== \"output-available\" &&\n part.state !== \"output-error\" &&\n part.state !== \"output-denied\"\n ) {\n return undefined;\n }\n\n const approval = part.approval;\n if (!approval) return undefined;\n const options = toolApprovalOptionsFromInputRequest(\n part.toolMetadata?.eve?.inputRequest,\n );\n\n return {\n id: approval.id,\n ...(approval.approved !== undefined && { approved: approval.approved }),\n ...(approval.reason && { reason: approval.reason }),\n ...(approval.isAutomatic !== undefined && {\n isAutomatic: approval.isAutomatic,\n }),\n ...(options && { options }),\n };\n};\n\nconst convertDynamicToolPart = (\n part: EveDynamicToolPart,\n): ToolCallMessagePart => {\n const approval = toApproval(part);\n const toolCall: ToolCallMessagePart = {\n type: \"tool-call\",\n toolCallId: part.toolCallId,\n toolName: part.toolName,\n args: toJsonObject(part.input),\n argsText: stringifyArgs(part.input),\n ...(approval && { approval }),\n };\n\n switch (part.state) {\n case \"output-available\":\n return { ...toolCall, result: part.output };\n\n case \"output-error\":\n return {\n ...toolCall,\n result: { error: part.errorText },\n isError: true,\n };\n\n case \"output-denied\":\n return {\n ...toolCall,\n result: { error: part.approval?.reason ?? \"Tool approval denied\" },\n isError: true,\n };\n\n default:\n return toolCall;\n }\n};\n\nconst convertAssistantPart = (\n part: EveMessagePart,\n): ThreadAssistantMessagePart | null => {\n switch (part.type) {\n case \"text\":\n case \"reasoning\":\n return { type: part.type, text: part.text };\n\n case \"step-start\":\n return null;\n\n case \"dynamic-tool\":\n return convertDynamicToolPart(part);\n\n default:\n return null;\n }\n};\n\nconst convertUserPart = (\n part: EveMessagePart,\n): ThreadUserMessagePart | null => {\n switch (part.type) {\n case \"text\":\n return { type: \"text\", text: part.text };\n\n default:\n return null;\n }\n};\n\n/**\n * Converts a single Eve message into an assistant-ui thread message.\n */\nexport const convertEveMessage = (\n message: EveMessage,\n index: number,\n messages: readonly EveMessage[],\n options: ConvertEveMessagesOptions = {},\n): ThreadMessage => {\n const createdAt = options.getCreatedAt?.(message) ?? new Date();\n const metadata = {\n ...(message.metadata?.optimistic && { isOptimistic: true }),\n custom: {\n ...(message.metadata ?? {}),\n },\n };\n\n const content =\n message.role === \"assistant\"\n ? message.parts.map(convertAssistantPart).filter((part) => part !== null)\n : message.parts.map(convertUserPart).filter((part) => part !== null);\n\n const fallbackTextPart: TextMessagePart = {\n type: \"text\",\n text: \"\",\n };\n\n if (message.role === \"user\") {\n return {\n id: message.id,\n createdAt,\n role: \"user\",\n content:\n content.length > 0\n ? (content as readonly ThreadUserMessagePart[])\n : [fallbackTextPart],\n attachments: [],\n metadata,\n };\n }\n\n return {\n id: message.id,\n createdAt,\n role: \"assistant\",\n content:\n content.length > 0\n ? (content as readonly ThreadAssistantMessagePart[])\n : [],\n status: toMessageStatus(message, index, messages, options),\n metadata: {\n unstable_state: null,\n unstable_annotations: [],\n unstable_data: [],\n steps: [],\n ...metadata,\n },\n };\n};\n\n/**\n * Converts the full Eve message data object into assistant-ui thread messages.\n */\nexport const convertEveMessages = (\n data: EveMessageData,\n options: ConvertEveMessagesOptions = {},\n): ThreadMessage[] =>\n data.messages.map((message, index, messages) =>\n convertEveMessage(message, index, messages, options),\n );\n\n/**\n * Converts an assistant-ui append message into the message payload accepted by\n * Eve's `send` API.\n */\nexport const getEveMessageContent = (\n message: AppendMessage,\n): NonNullable<SendTurnPayload[\"message\"]> => {\n const content = [\n ...message.content,\n ...(message.attachments?.flatMap((attachment) => attachment.content) ?? []),\n ];\n\n const parts = content.map((part) => {\n const type = part.type;\n switch (type) {\n case \"text\":\n return { type: \"text\" as const, text: part.text };\n\n case \"file\":\n return {\n type: \"file\" as const,\n data: part.data,\n mediaType: part.mimeType,\n ...(part.filename && { filename: part.filename }),\n };\n\n case \"image\":\n return {\n type: \"file\" as const,\n data: part.image,\n mediaType: \"image/*\",\n ...(part.filename && { filename: part.filename }),\n };\n\n default: {\n const _exhaustiveCheck:\n | \"audio\"\n | \"data\"\n | \"generative-ui\"\n | \"reasoning\"\n | \"source\"\n | \"tool-call\" = type;\n throw new Error(\n `Unsupported eve append message part type: ${_exhaustiveCheck}`,\n );\n }\n }\n });\n\n if (parts.length === 1 && parts[0]?.type === \"text\") {\n return parts[0].text;\n }\n\n return parts;\n};\n\n/**\n * Converts an assistant-ui tool approval response into an Eve input response.\n */\nexport const toEveInputResponse = (\n response: RespondToToolApprovalOptions,\n): InputResponse => ({\n requestId: response.approvalId,\n optionId: response.optionId ?? (response.approved ? \"approve\" : \"deny\"),\n ...(response.reason && { text: response.reason }),\n});\n"],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/// <reference types="@assistant-ui/core/react" />
|
|
2
|
+
import { ConvertEveMessagesOptions, convertEveMessage, convertEveMessages, getEveMessageContent } from "./convertEveMessages.js";
|
|
3
|
+
import { UseEveAgentRuntimeOptions, useEveAgentRuntime } from "./useEveAgentRuntime.js";
|
|
4
|
+
export { type ConvertEveMessagesOptions, type UseEveAgentRuntimeOptions, convertEveMessage, convertEveMessages, getEveMessageContent, useEveAgentRuntime };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { AttachmentAdapter, DictationAdapter, ExternalStoreSharedOptions, FeedbackAdapter, RealtimeVoiceAdapter, SpeechSynthesisAdapter } from "@assistant-ui/core";
|
|
2
|
+
import { EveMessageData, UseEveAgentOptions } from "eve/react";
|
|
3
|
+
|
|
4
|
+
//#region src/useEveAgentRuntime.d.ts
|
|
5
|
+
type UseEveAgentRuntimeOptions = Omit<UseEveAgentOptions<EveMessageData>, "reducer"> & ExternalStoreSharedOptions & {
|
|
6
|
+
readonly adapters?: {
|
|
7
|
+
readonly attachments?: AttachmentAdapter | undefined;
|
|
8
|
+
readonly speech?: SpeechSynthesisAdapter | undefined;
|
|
9
|
+
readonly dictation?: DictationAdapter | undefined;
|
|
10
|
+
readonly voice?: RealtimeVoiceAdapter | undefined;
|
|
11
|
+
readonly feedback?: FeedbackAdapter | undefined;
|
|
12
|
+
} | undefined;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Connects Eve's `useEveAgent` hook to assistant-ui's runtime contract.
|
|
16
|
+
*
|
|
17
|
+
* The runtime renders Eve messages, forwards new user messages to the Eve
|
|
18
|
+
* session, supports cancellation, and maps Eve input requests to assistant-ui
|
|
19
|
+
* tool approval UI.
|
|
20
|
+
*/
|
|
21
|
+
declare const useEveAgentRuntime: (options?: UseEveAgentRuntimeOptions) => import("@assistant-ui/core").AssistantRuntime;
|
|
22
|
+
//#endregion
|
|
23
|
+
export { UseEveAgentRuntimeOptions, useEveAgentRuntime };
|
|
24
|
+
//# sourceMappingURL=useEveAgentRuntime.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useEveAgentRuntime.d.ts","names":[],"sources":["../src/useEveAgentRuntime.ts"],"mappings":";;;;KA4BY,yBAAA,GAA4B,IAAA,CACtC,kBAAA,CAAmB,cAAA,gBAGnB,0BAAA;EAAA,SACW,QAAA;IAAA,SAEM,WAAA,GAAc,iBAAA;IAAA,SACd,MAAA,GAAS,sBAAA;IAAA,SACT,SAAA,GAAY,gBAAA;IAAA,SACZ,KAAA,GAAQ,oBAAA;IAAA,SACR,QAAA,GAAW,eAAA;EAAA;AAAA;;;;;;;;cAYjB,kBAAA,GAAsB,OAAA,GAAS,yBAA8B,kCAAA,gBAAA"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { convertEveMessages, getEveMessageContent, toEveInputResponse } from "./convertEveMessages.js";
|
|
3
|
+
import { useMemo, useRef, useState } from "react";
|
|
4
|
+
import { pickExternalStoreSharedOptions } from "@assistant-ui/core";
|
|
5
|
+
import { useExternalStoreRuntime, useRuntimeAdapters } from "@assistant-ui/core/react";
|
|
6
|
+
import { useEveAgent } from "eve/react";
|
|
7
|
+
//#region src/useEveAgentRuntime.ts
|
|
8
|
+
/**
|
|
9
|
+
* Connects Eve's `useEveAgent` hook to assistant-ui's runtime contract.
|
|
10
|
+
*
|
|
11
|
+
* The runtime renders Eve messages, forwards new user messages to the Eve
|
|
12
|
+
* session, supports cancellation, and maps Eve input requests to assistant-ui
|
|
13
|
+
* tool approval UI.
|
|
14
|
+
*/
|
|
15
|
+
const useEveAgentRuntime = (options = {}) => {
|
|
16
|
+
const { adapters, isDisabled: _isDisabled, isSendDisabled: _isSendDisabled, suggestions: _suggestions, unstable_capabilities: _unstable_capabilities, ...agentOptions } = options;
|
|
17
|
+
const agent = useEveAgent(agentOptions);
|
|
18
|
+
const runtimeAdapters = useRuntimeAdapters();
|
|
19
|
+
const [toolStatuses, setToolStatuses] = useState({});
|
|
20
|
+
const createdAtByMessageIdRef = useRef(/* @__PURE__ */ new Map());
|
|
21
|
+
const hasExecutingTools = Object.values(toolStatuses).some((status) => status?.type === "executing");
|
|
22
|
+
const isRunning = agent.status === "submitted" || agent.status === "streaming" || hasExecutingTools;
|
|
23
|
+
const messages = useMemo(() => {
|
|
24
|
+
const createdAtByMessageId = createdAtByMessageIdRef.current;
|
|
25
|
+
const messageIds = new Set(agent.data.messages.map((message) => message.id));
|
|
26
|
+
for (const messageId of createdAtByMessageId.keys()) if (!messageIds.has(messageId)) createdAtByMessageId.delete(messageId);
|
|
27
|
+
return convertEveMessages(agent.data, {
|
|
28
|
+
isRunning,
|
|
29
|
+
getCreatedAt: (message) => {
|
|
30
|
+
const existing = createdAtByMessageId.get(message.id);
|
|
31
|
+
if (existing) return existing;
|
|
32
|
+
const createdAt = /* @__PURE__ */ new Date();
|
|
33
|
+
createdAtByMessageId.set(message.id, createdAt);
|
|
34
|
+
return createdAt;
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}, [agent.data, isRunning]);
|
|
38
|
+
return useExternalStoreRuntime({
|
|
39
|
+
...pickExternalStoreSharedOptions(options),
|
|
40
|
+
messages,
|
|
41
|
+
isRunning,
|
|
42
|
+
unstable_enableToolInvocations: true,
|
|
43
|
+
setToolStatuses,
|
|
44
|
+
adapters: {
|
|
45
|
+
attachments: adapters?.attachments ?? runtimeAdapters?.attachments,
|
|
46
|
+
speech: adapters?.speech,
|
|
47
|
+
dictation: adapters?.dictation,
|
|
48
|
+
voice: adapters?.voice,
|
|
49
|
+
feedback: adapters?.feedback
|
|
50
|
+
},
|
|
51
|
+
onNew: async (message) => {
|
|
52
|
+
await agent.send({ message: getEveMessageContent(message) });
|
|
53
|
+
},
|
|
54
|
+
onCancel: () => {
|
|
55
|
+
agent.stop();
|
|
56
|
+
return Promise.resolve();
|
|
57
|
+
},
|
|
58
|
+
onRespondToToolApproval: async (response) => {
|
|
59
|
+
await agent.send({ inputResponses: [toEveInputResponse(response)] });
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
};
|
|
63
|
+
//#endregion
|
|
64
|
+
export { useEveAgentRuntime };
|
|
65
|
+
|
|
66
|
+
//# sourceMappingURL=useEveAgentRuntime.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useEveAgentRuntime.js","names":[],"sources":["../src/useEveAgentRuntime.ts"],"sourcesContent":["\"use client\";\n\nimport { useMemo, useRef, useState } from \"react\";\nimport {\n pickExternalStoreSharedOptions,\n type AttachmentAdapter,\n type DictationAdapter,\n type ExternalStoreSharedOptions,\n type FeedbackAdapter,\n type RealtimeVoiceAdapter,\n type SpeechSynthesisAdapter,\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\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 createdAtByMessageIdRef = useRef(new Map<string, Date>());\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 messages = 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 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, isRunning]);\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 await agent.send({ message: getEveMessageContent(message) });\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":";;;;;;;;;;;;;;AAmDA,MAAa,sBAAsB,UAAqC,CAAC,MAAM;CAC7E,MAAM,EACJ,UACA,YAAY,aACZ,gBAAgB,iBAChB,aAAa,cACb,uBAAuB,wBACvB,GAAG,iBACD;CAMJ,MAAM,QAAQ,YAAY,YAAY;CACtC,MAAM,kBAAkB,mBAAmB;CAC3C,MAAM,CAAC,cAAc,mBAAmB,SAEtC,CAAC,CAAC;CACJ,MAAM,0BAA0B,uBAAO,IAAI,IAAkB,CAAC;CAE9D,MAAM,oBAAoB,OAAO,OAAO,YAAY,CAAC,CAAC,MACnD,WAAW,QAAQ,SAAS,WAC/B;CACA,MAAM,YACJ,MAAM,WAAW,eACjB,MAAM,WAAW,eACjB;CAEF,MAAM,WAAW,cAAc;EAC7B,MAAM,uBAAuB,wBAAwB;EACrD,MAAM,aAAa,IAAI,IACrB,MAAM,KAAK,SAAS,KAAK,YAAY,QAAQ,EAAE,CACjD;EACA,KAAK,MAAM,aAAa,qBAAqB,KAAK,GAChD,IAAI,CAAC,WAAW,IAAI,SAAS,GAAG,qBAAqB,OAAO,SAAS;EAGvE,OAAO,mBAAmB,MAAM,MAAM;GACpC;GACA,eAAe,YAAY;IACzB,MAAM,WAAW,qBAAqB,IAAI,QAAQ,EAAE;IACpD,IAAI,UAAU,OAAO;IAErB,MAAM,4BAAY,IAAI,KAAK;IAC3B,qBAAqB,IAAI,QAAQ,IAAI,SAAS;IAC9C,OAAO;GACT;EACF,CAAC;CACH,GAAG,CAAC,MAAM,MAAM,SAAS,CAAC;CAE1B,OAAO,wBAAwB;EAC7B,GAAG,+BAA+B,OAAO;EACzC;EACA;EACA,gCAAgC;EAChC;EACA,UAAU;GACR,aAAa,UAAU,eAAe,iBAAiB;GACvD,QAAQ,UAAU;GAClB,WAAW,UAAU;GACrB,OAAO,UAAU;GACjB,UAAU,UAAU;EACtB;EACA,OAAO,OAAO,YAAY;GACxB,MAAM,MAAM,KAAK,EAAE,SAAS,qBAAqB,OAAO,EAAE,CAAC;EAC7D;EACA,gBAAgB;GACd,MAAM,KAAK;GACX,OAAO,QAAQ,QAAQ;EACzB;EACA,yBAAyB,OAAO,aAAa;GAC3C,MAAM,MAAM,KAAK,EAAE,gBAAgB,CAAC,mBAAmB,QAAQ,CAAC,EAAE,CAAC;EACrE;CACF,CAAC;AACH"}
|
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@assistant-ui/eve",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "Eve runtime adapter for assistant-ui",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"eve",
|
|
7
|
+
"assistant-ui",
|
|
8
|
+
"react",
|
|
9
|
+
"ai",
|
|
10
|
+
"chat",
|
|
11
|
+
"agents"
|
|
12
|
+
],
|
|
13
|
+
"author": "AgentbaseAI Inc.",
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"type": "module",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"default": "./dist/index.js"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"main": "./dist/index.js",
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"files": [
|
|
25
|
+
"dist",
|
|
26
|
+
"src",
|
|
27
|
+
"README.md"
|
|
28
|
+
],
|
|
29
|
+
"sideEffects": false,
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@assistant-ui/core": "^0.2.18"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"@types/react": "*",
|
|
35
|
+
"eve": "^0.11.4",
|
|
36
|
+
"react": "^18 || ^19"
|
|
37
|
+
},
|
|
38
|
+
"peerDependenciesMeta": {
|
|
39
|
+
"@types/react": {
|
|
40
|
+
"optional": true
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/react": "^19.2.17",
|
|
45
|
+
"eve": "^0.11.4",
|
|
46
|
+
"react": "^19.2.7",
|
|
47
|
+
"vitest": "^4.1.8",
|
|
48
|
+
"@assistant-ui/x-buildutils": "0.0.15"
|
|
49
|
+
},
|
|
50
|
+
"publishConfig": {
|
|
51
|
+
"access": "public",
|
|
52
|
+
"provenance": true
|
|
53
|
+
},
|
|
54
|
+
"homepage": "https://www.assistant-ui.com/",
|
|
55
|
+
"repository": {
|
|
56
|
+
"type": "git",
|
|
57
|
+
"url": "git+https://github.com/assistant-ui/assistant-ui.git",
|
|
58
|
+
"directory": "packages/eve"
|
|
59
|
+
},
|
|
60
|
+
"bugs": {
|
|
61
|
+
"url": "https://github.com/assistant-ui/assistant-ui/issues"
|
|
62
|
+
},
|
|
63
|
+
"scripts": {
|
|
64
|
+
"build": "aui-build",
|
|
65
|
+
"test": "vitest run",
|
|
66
|
+
"test:watch": "vitest"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { EveMessageData } from "eve/react";
|
|
3
|
+
import {
|
|
4
|
+
convertEveMessages,
|
|
5
|
+
getEveMessageContent,
|
|
6
|
+
toEveInputResponse,
|
|
7
|
+
} from "./convertEveMessages";
|
|
8
|
+
import type { AppendMessage } from "@assistant-ui/core";
|
|
9
|
+
|
|
10
|
+
describe("convertEveMessages", () => {
|
|
11
|
+
it("converts text and reasoning parts", () => {
|
|
12
|
+
const data = {
|
|
13
|
+
messages: [
|
|
14
|
+
{
|
|
15
|
+
id: "u1",
|
|
16
|
+
role: "user",
|
|
17
|
+
parts: [{ type: "text", text: "Hello" }],
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
id: "a1",
|
|
21
|
+
role: "assistant",
|
|
22
|
+
metadata: { status: "streaming" },
|
|
23
|
+
parts: [
|
|
24
|
+
{ type: "reasoning", text: "Thinking" },
|
|
25
|
+
{ type: "text", text: "Hi there" },
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
} satisfies EveMessageData;
|
|
30
|
+
|
|
31
|
+
const messages = convertEveMessages(data, { isRunning: true });
|
|
32
|
+
|
|
33
|
+
expect(messages).toHaveLength(2);
|
|
34
|
+
expect(messages[0]).toMatchObject({
|
|
35
|
+
id: "u1",
|
|
36
|
+
role: "user",
|
|
37
|
+
content: [{ type: "text", text: "Hello" }],
|
|
38
|
+
});
|
|
39
|
+
expect(messages[1]).toMatchObject({
|
|
40
|
+
id: "a1",
|
|
41
|
+
role: "assistant",
|
|
42
|
+
status: { type: "running" },
|
|
43
|
+
content: [
|
|
44
|
+
{ type: "reasoning", text: "Thinking" },
|
|
45
|
+
{ type: "text", text: "Hi there" },
|
|
46
|
+
],
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("converts dynamic tool parts with approval options", () => {
|
|
51
|
+
const data = {
|
|
52
|
+
messages: [
|
|
53
|
+
{
|
|
54
|
+
id: "a1",
|
|
55
|
+
role: "assistant",
|
|
56
|
+
parts: [
|
|
57
|
+
{
|
|
58
|
+
type: "dynamic-tool",
|
|
59
|
+
state: "approval-requested",
|
|
60
|
+
toolCallId: "call_1",
|
|
61
|
+
toolName: "send_email",
|
|
62
|
+
input: { to: "dev@example.com" },
|
|
63
|
+
approval: { id: "req_1" },
|
|
64
|
+
toolMetadata: {
|
|
65
|
+
eve: {
|
|
66
|
+
kind: "tool-call",
|
|
67
|
+
name: "send_email",
|
|
68
|
+
inputRequest: {
|
|
69
|
+
requestId: "req_1",
|
|
70
|
+
prompt: "Send the email?",
|
|
71
|
+
display: "confirmation",
|
|
72
|
+
options: [
|
|
73
|
+
{ id: "approve", label: "Approve" },
|
|
74
|
+
{ id: "deny", label: "Deny", style: "danger" },
|
|
75
|
+
{ id: "escalate", label: "Escalate" },
|
|
76
|
+
],
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
} satisfies EveMessageData;
|
|
85
|
+
|
|
86
|
+
const [message] = convertEveMessages(data);
|
|
87
|
+
|
|
88
|
+
expect(message).toMatchObject({
|
|
89
|
+
status: { type: "requires-action", reason: "tool-calls" },
|
|
90
|
+
content: [
|
|
91
|
+
{
|
|
92
|
+
type: "tool-call",
|
|
93
|
+
toolCallId: "call_1",
|
|
94
|
+
toolName: "send_email",
|
|
95
|
+
args: { to: "dev@example.com" },
|
|
96
|
+
approval: {
|
|
97
|
+
id: "req_1",
|
|
98
|
+
options: [
|
|
99
|
+
{ id: "approve", kind: "allow-once", label: "Approve" },
|
|
100
|
+
{ id: "deny", kind: "reject-once", label: "Deny" },
|
|
101
|
+
{ id: "escalate", kind: "_escalate", label: "Escalate" },
|
|
102
|
+
],
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("handles denied tool parts without approval metadata", () => {
|
|
110
|
+
const data = {
|
|
111
|
+
messages: [
|
|
112
|
+
{
|
|
113
|
+
id: "a1",
|
|
114
|
+
role: "assistant",
|
|
115
|
+
parts: [
|
|
116
|
+
{
|
|
117
|
+
type: "dynamic-tool",
|
|
118
|
+
state: "output-denied",
|
|
119
|
+
toolCallId: "call_1",
|
|
120
|
+
toolName: "send_email",
|
|
121
|
+
input: { to: "dev@example.com" },
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
} satisfies EveMessageData;
|
|
127
|
+
|
|
128
|
+
const [message] = convertEveMessages(data);
|
|
129
|
+
|
|
130
|
+
expect(message).toMatchObject({
|
|
131
|
+
content: [
|
|
132
|
+
{
|
|
133
|
+
type: "tool-call",
|
|
134
|
+
toolCallId: "call_1",
|
|
135
|
+
toolName: "send_email",
|
|
136
|
+
result: { error: "Tool approval denied" },
|
|
137
|
+
isError: true,
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("uses the supplied message creation time", () => {
|
|
144
|
+
const createdAt = new Date("2026-06-17T00:00:00.000Z");
|
|
145
|
+
const data = {
|
|
146
|
+
messages: [
|
|
147
|
+
{
|
|
148
|
+
id: "u1",
|
|
149
|
+
role: "user",
|
|
150
|
+
parts: [{ type: "text", text: "Hello" }],
|
|
151
|
+
},
|
|
152
|
+
],
|
|
153
|
+
} satisfies EveMessageData;
|
|
154
|
+
|
|
155
|
+
const [message] = convertEveMessages(data, {
|
|
156
|
+
getCreatedAt: () => createdAt,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
expect(message?.createdAt).toBe(createdAt);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe("getEveMessageContent", () => {
|
|
164
|
+
it("returns plain text for text-only messages", () => {
|
|
165
|
+
const message = {
|
|
166
|
+
role: "user",
|
|
167
|
+
parentId: null,
|
|
168
|
+
sourceId: null,
|
|
169
|
+
runConfig: undefined,
|
|
170
|
+
content: [{ type: "text", text: "Hello" }],
|
|
171
|
+
metadata: { custom: {} },
|
|
172
|
+
attachments: [],
|
|
173
|
+
} satisfies AppendMessage;
|
|
174
|
+
|
|
175
|
+
expect(getEveMessageContent(message)).toBe("Hello");
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe("toEveInputResponse", () => {
|
|
180
|
+
it("maps assistant-ui approval responses to eve input responses", () => {
|
|
181
|
+
expect(
|
|
182
|
+
toEveInputResponse({
|
|
183
|
+
approvalId: "req_1",
|
|
184
|
+
approved: false,
|
|
185
|
+
reason: "Not yet",
|
|
186
|
+
}),
|
|
187
|
+
).toEqual({
|
|
188
|
+
requestId: "req_1",
|
|
189
|
+
optionId: "deny",
|
|
190
|
+
text: "Not yet",
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
});
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AppendMessage,
|
|
3
|
+
MessageStatus,
|
|
4
|
+
RespondToToolApprovalOptions,
|
|
5
|
+
TextMessagePart,
|
|
6
|
+
ThreadAssistantMessagePart,
|
|
7
|
+
ThreadMessage,
|
|
8
|
+
ThreadUserMessagePart,
|
|
9
|
+
ToolApprovalOption,
|
|
10
|
+
ToolCallMessagePart,
|
|
11
|
+
} from "@assistant-ui/core";
|
|
12
|
+
import type {
|
|
13
|
+
EveDynamicToolPart,
|
|
14
|
+
EveMessage,
|
|
15
|
+
EveMessageData,
|
|
16
|
+
EveMessageInputRequest,
|
|
17
|
+
EveMessagePart,
|
|
18
|
+
InputResponse,
|
|
19
|
+
SendTurnPayload,
|
|
20
|
+
} from "eve/react";
|
|
21
|
+
|
|
22
|
+
const ASSISTANT_COMPLETE_STATUS = {
|
|
23
|
+
type: "complete",
|
|
24
|
+
reason: "stop",
|
|
25
|
+
} satisfies MessageStatus;
|
|
26
|
+
|
|
27
|
+
const ASSISTANT_RUNNING_STATUS = {
|
|
28
|
+
type: "running",
|
|
29
|
+
} satisfies MessageStatus;
|
|
30
|
+
|
|
31
|
+
const USER_FALLBACK_STATUS = {
|
|
32
|
+
type: "complete",
|
|
33
|
+
reason: "unknown",
|
|
34
|
+
} satisfies MessageStatus;
|
|
35
|
+
|
|
36
|
+
export type ConvertEveMessagesOptions = {
|
|
37
|
+
/**
|
|
38
|
+
* Marks the last assistant message as running while Eve is submitting or
|
|
39
|
+
* streaming.
|
|
40
|
+
*/
|
|
41
|
+
readonly isRunning?: boolean | undefined;
|
|
42
|
+
readonly getCreatedAt?: ((message: EveMessage) => Date) | undefined;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
46
|
+
typeof value === "object" && value !== null && !Array.isArray(value);
|
|
47
|
+
|
|
48
|
+
const toJsonObject = (value: unknown): Record<string, unknown> => {
|
|
49
|
+
if (isRecord(value)) return value;
|
|
50
|
+
if (value === undefined) return {};
|
|
51
|
+
return { value };
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const stringifyArgs = (value: unknown): string => {
|
|
55
|
+
try {
|
|
56
|
+
return JSON.stringify(value ?? {});
|
|
57
|
+
} catch {
|
|
58
|
+
return "";
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const toMessageStatus = (
|
|
63
|
+
message: EveMessage,
|
|
64
|
+
index: number,
|
|
65
|
+
messages: readonly EveMessage[],
|
|
66
|
+
options: ConvertEveMessagesOptions,
|
|
67
|
+
): MessageStatus => {
|
|
68
|
+
if (message.role !== "assistant") return USER_FALLBACK_STATUS;
|
|
69
|
+
|
|
70
|
+
const hasPendingApproval = message.parts.some(
|
|
71
|
+
(part) =>
|
|
72
|
+
part.type === "dynamic-tool" && part.state === "approval-requested",
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
if (hasPendingApproval) {
|
|
76
|
+
return { type: "requires-action", reason: "tool-calls" };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (message.metadata?.status === "failed") {
|
|
80
|
+
return { type: "incomplete", reason: "error" };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (
|
|
84
|
+
message.metadata?.status === "streaming" ||
|
|
85
|
+
(options.isRunning === true && index === messages.length - 1)
|
|
86
|
+
) {
|
|
87
|
+
return ASSISTANT_RUNNING_STATUS;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return ASSISTANT_COMPLETE_STATUS;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const toolApprovalOptionsFromInputRequest = (
|
|
94
|
+
inputRequest: EveMessageInputRequest | undefined,
|
|
95
|
+
): readonly ToolApprovalOption[] | undefined => {
|
|
96
|
+
const options = inputRequest?.options;
|
|
97
|
+
if (!options || options.length === 0) return undefined;
|
|
98
|
+
|
|
99
|
+
return options.map((option) => ({
|
|
100
|
+
id: option.id,
|
|
101
|
+
kind:
|
|
102
|
+
option.id === "approve"
|
|
103
|
+
? "allow-once"
|
|
104
|
+
: option.id === "deny"
|
|
105
|
+
? "reject-once"
|
|
106
|
+
: `_${option.id}`,
|
|
107
|
+
...(option.label && { label: option.label }),
|
|
108
|
+
...(option.description && { description: option.description }),
|
|
109
|
+
}));
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const toApproval = (
|
|
113
|
+
part: EveDynamicToolPart,
|
|
114
|
+
): ToolCallMessagePart["approval"] | undefined => {
|
|
115
|
+
if (
|
|
116
|
+
part.state !== "approval-requested" &&
|
|
117
|
+
part.state !== "approval-responded" &&
|
|
118
|
+
part.state !== "output-available" &&
|
|
119
|
+
part.state !== "output-error" &&
|
|
120
|
+
part.state !== "output-denied"
|
|
121
|
+
) {
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const approval = part.approval;
|
|
126
|
+
if (!approval) return undefined;
|
|
127
|
+
const options = toolApprovalOptionsFromInputRequest(
|
|
128
|
+
part.toolMetadata?.eve?.inputRequest,
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
id: approval.id,
|
|
133
|
+
...(approval.approved !== undefined && { approved: approval.approved }),
|
|
134
|
+
...(approval.reason && { reason: approval.reason }),
|
|
135
|
+
...(approval.isAutomatic !== undefined && {
|
|
136
|
+
isAutomatic: approval.isAutomatic,
|
|
137
|
+
}),
|
|
138
|
+
...(options && { options }),
|
|
139
|
+
};
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const convertDynamicToolPart = (
|
|
143
|
+
part: EveDynamicToolPart,
|
|
144
|
+
): ToolCallMessagePart => {
|
|
145
|
+
const approval = toApproval(part);
|
|
146
|
+
const toolCall: ToolCallMessagePart = {
|
|
147
|
+
type: "tool-call",
|
|
148
|
+
toolCallId: part.toolCallId,
|
|
149
|
+
toolName: part.toolName,
|
|
150
|
+
args: toJsonObject(part.input),
|
|
151
|
+
argsText: stringifyArgs(part.input),
|
|
152
|
+
...(approval && { approval }),
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
switch (part.state) {
|
|
156
|
+
case "output-available":
|
|
157
|
+
return { ...toolCall, result: part.output };
|
|
158
|
+
|
|
159
|
+
case "output-error":
|
|
160
|
+
return {
|
|
161
|
+
...toolCall,
|
|
162
|
+
result: { error: part.errorText },
|
|
163
|
+
isError: true,
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
case "output-denied":
|
|
167
|
+
return {
|
|
168
|
+
...toolCall,
|
|
169
|
+
result: { error: part.approval?.reason ?? "Tool approval denied" },
|
|
170
|
+
isError: true,
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
default:
|
|
174
|
+
return toolCall;
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const convertAssistantPart = (
|
|
179
|
+
part: EveMessagePart,
|
|
180
|
+
): ThreadAssistantMessagePart | null => {
|
|
181
|
+
switch (part.type) {
|
|
182
|
+
case "text":
|
|
183
|
+
case "reasoning":
|
|
184
|
+
return { type: part.type, text: part.text };
|
|
185
|
+
|
|
186
|
+
case "step-start":
|
|
187
|
+
return null;
|
|
188
|
+
|
|
189
|
+
case "dynamic-tool":
|
|
190
|
+
return convertDynamicToolPart(part);
|
|
191
|
+
|
|
192
|
+
default:
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const convertUserPart = (
|
|
198
|
+
part: EveMessagePart,
|
|
199
|
+
): ThreadUserMessagePart | null => {
|
|
200
|
+
switch (part.type) {
|
|
201
|
+
case "text":
|
|
202
|
+
return { type: "text", text: part.text };
|
|
203
|
+
|
|
204
|
+
default:
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Converts a single Eve message into an assistant-ui thread message.
|
|
211
|
+
*/
|
|
212
|
+
export const convertEveMessage = (
|
|
213
|
+
message: EveMessage,
|
|
214
|
+
index: number,
|
|
215
|
+
messages: readonly EveMessage[],
|
|
216
|
+
options: ConvertEveMessagesOptions = {},
|
|
217
|
+
): ThreadMessage => {
|
|
218
|
+
const createdAt = options.getCreatedAt?.(message) ?? new Date();
|
|
219
|
+
const metadata = {
|
|
220
|
+
...(message.metadata?.optimistic && { isOptimistic: true }),
|
|
221
|
+
custom: {
|
|
222
|
+
...(message.metadata ?? {}),
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const content =
|
|
227
|
+
message.role === "assistant"
|
|
228
|
+
? message.parts.map(convertAssistantPart).filter((part) => part !== null)
|
|
229
|
+
: message.parts.map(convertUserPart).filter((part) => part !== null);
|
|
230
|
+
|
|
231
|
+
const fallbackTextPart: TextMessagePart = {
|
|
232
|
+
type: "text",
|
|
233
|
+
text: "",
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
if (message.role === "user") {
|
|
237
|
+
return {
|
|
238
|
+
id: message.id,
|
|
239
|
+
createdAt,
|
|
240
|
+
role: "user",
|
|
241
|
+
content:
|
|
242
|
+
content.length > 0
|
|
243
|
+
? (content as readonly ThreadUserMessagePart[])
|
|
244
|
+
: [fallbackTextPart],
|
|
245
|
+
attachments: [],
|
|
246
|
+
metadata,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
id: message.id,
|
|
252
|
+
createdAt,
|
|
253
|
+
role: "assistant",
|
|
254
|
+
content:
|
|
255
|
+
content.length > 0
|
|
256
|
+
? (content as readonly ThreadAssistantMessagePart[])
|
|
257
|
+
: [],
|
|
258
|
+
status: toMessageStatus(message, index, messages, options),
|
|
259
|
+
metadata: {
|
|
260
|
+
unstable_state: null,
|
|
261
|
+
unstable_annotations: [],
|
|
262
|
+
unstable_data: [],
|
|
263
|
+
steps: [],
|
|
264
|
+
...metadata,
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Converts the full Eve message data object into assistant-ui thread messages.
|
|
271
|
+
*/
|
|
272
|
+
export const convertEveMessages = (
|
|
273
|
+
data: EveMessageData,
|
|
274
|
+
options: ConvertEveMessagesOptions = {},
|
|
275
|
+
): ThreadMessage[] =>
|
|
276
|
+
data.messages.map((message, index, messages) =>
|
|
277
|
+
convertEveMessage(message, index, messages, options),
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Converts an assistant-ui append message into the message payload accepted by
|
|
282
|
+
* Eve's `send` API.
|
|
283
|
+
*/
|
|
284
|
+
export const getEveMessageContent = (
|
|
285
|
+
message: AppendMessage,
|
|
286
|
+
): NonNullable<SendTurnPayload["message"]> => {
|
|
287
|
+
const content = [
|
|
288
|
+
...message.content,
|
|
289
|
+
...(message.attachments?.flatMap((attachment) => attachment.content) ?? []),
|
|
290
|
+
];
|
|
291
|
+
|
|
292
|
+
const parts = content.map((part) => {
|
|
293
|
+
const type = part.type;
|
|
294
|
+
switch (type) {
|
|
295
|
+
case "text":
|
|
296
|
+
return { type: "text" as const, text: part.text };
|
|
297
|
+
|
|
298
|
+
case "file":
|
|
299
|
+
return {
|
|
300
|
+
type: "file" as const,
|
|
301
|
+
data: part.data,
|
|
302
|
+
mediaType: part.mimeType,
|
|
303
|
+
...(part.filename && { filename: part.filename }),
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
case "image":
|
|
307
|
+
return {
|
|
308
|
+
type: "file" as const,
|
|
309
|
+
data: part.image,
|
|
310
|
+
mediaType: "image/*",
|
|
311
|
+
...(part.filename && { filename: part.filename }),
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
default: {
|
|
315
|
+
const _exhaustiveCheck:
|
|
316
|
+
| "audio"
|
|
317
|
+
| "data"
|
|
318
|
+
| "generative-ui"
|
|
319
|
+
| "reasoning"
|
|
320
|
+
| "source"
|
|
321
|
+
| "tool-call" = type;
|
|
322
|
+
throw new Error(
|
|
323
|
+
`Unsupported eve append message part type: ${_exhaustiveCheck}`,
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
if (parts.length === 1 && parts[0]?.type === "text") {
|
|
330
|
+
return parts[0].text;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return parts;
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Converts an assistant-ui tool approval response into an Eve input response.
|
|
338
|
+
*/
|
|
339
|
+
export const toEveInputResponse = (
|
|
340
|
+
response: RespondToToolApprovalOptions,
|
|
341
|
+
): InputResponse => ({
|
|
342
|
+
requestId: response.approvalId,
|
|
343
|
+
optionId: response.optionId ?? (response.approved ? "approve" : "deny"),
|
|
344
|
+
...(response.reason && { text: response.reason }),
|
|
345
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/// <reference types="@assistant-ui/core/react" />
|
|
2
|
+
|
|
3
|
+
export {
|
|
4
|
+
convertEveMessage,
|
|
5
|
+
convertEveMessages,
|
|
6
|
+
getEveMessageContent,
|
|
7
|
+
} from "./convertEveMessages";
|
|
8
|
+
export type { ConvertEveMessagesOptions } from "./convertEveMessages";
|
|
9
|
+
export { useEveAgentRuntime } from "./useEveAgentRuntime";
|
|
10
|
+
export type { UseEveAgentRuntimeOptions } from "./useEveAgentRuntime";
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useMemo, useRef, useState } from "react";
|
|
4
|
+
import {
|
|
5
|
+
pickExternalStoreSharedOptions,
|
|
6
|
+
type AttachmentAdapter,
|
|
7
|
+
type DictationAdapter,
|
|
8
|
+
type ExternalStoreSharedOptions,
|
|
9
|
+
type FeedbackAdapter,
|
|
10
|
+
type RealtimeVoiceAdapter,
|
|
11
|
+
type SpeechSynthesisAdapter,
|
|
12
|
+
type ToolExecutionStatus,
|
|
13
|
+
} from "@assistant-ui/core";
|
|
14
|
+
import {
|
|
15
|
+
useExternalStoreRuntime,
|
|
16
|
+
useRuntimeAdapters,
|
|
17
|
+
} from "@assistant-ui/core/react";
|
|
18
|
+
import {
|
|
19
|
+
useEveAgent,
|
|
20
|
+
type EveMessageData,
|
|
21
|
+
type UseEveAgentOptions,
|
|
22
|
+
} from "eve/react";
|
|
23
|
+
import {
|
|
24
|
+
convertEveMessages,
|
|
25
|
+
getEveMessageContent,
|
|
26
|
+
toEveInputResponse,
|
|
27
|
+
} from "./convertEveMessages";
|
|
28
|
+
|
|
29
|
+
export type UseEveAgentRuntimeOptions = Omit<
|
|
30
|
+
UseEveAgentOptions<EveMessageData>,
|
|
31
|
+
"reducer"
|
|
32
|
+
> &
|
|
33
|
+
ExternalStoreSharedOptions & {
|
|
34
|
+
readonly adapters?:
|
|
35
|
+
| {
|
|
36
|
+
readonly attachments?: AttachmentAdapter | undefined;
|
|
37
|
+
readonly speech?: SpeechSynthesisAdapter | undefined;
|
|
38
|
+
readonly dictation?: DictationAdapter | undefined;
|
|
39
|
+
readonly voice?: RealtimeVoiceAdapter | undefined;
|
|
40
|
+
readonly feedback?: FeedbackAdapter | undefined;
|
|
41
|
+
}
|
|
42
|
+
| undefined;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Connects Eve's `useEveAgent` hook to assistant-ui's runtime contract.
|
|
47
|
+
*
|
|
48
|
+
* The runtime renders Eve messages, forwards new user messages to the Eve
|
|
49
|
+
* session, supports cancellation, and maps Eve input requests to assistant-ui
|
|
50
|
+
* tool approval UI.
|
|
51
|
+
*/
|
|
52
|
+
export const useEveAgentRuntime = (options: UseEveAgentRuntimeOptions = {}) => {
|
|
53
|
+
const {
|
|
54
|
+
adapters,
|
|
55
|
+
isDisabled: _isDisabled,
|
|
56
|
+
isSendDisabled: _isSendDisabled,
|
|
57
|
+
suggestions: _suggestions,
|
|
58
|
+
unstable_capabilities: _unstable_capabilities,
|
|
59
|
+
...agentOptions
|
|
60
|
+
} = options;
|
|
61
|
+
true satisfies keyof typeof agentOptions &
|
|
62
|
+
keyof ExternalStoreSharedOptions extends never
|
|
63
|
+
? true
|
|
64
|
+
: never;
|
|
65
|
+
|
|
66
|
+
const agent = useEveAgent(agentOptions);
|
|
67
|
+
const runtimeAdapters = useRuntimeAdapters();
|
|
68
|
+
const [toolStatuses, setToolStatuses] = useState<
|
|
69
|
+
Record<string, ToolExecutionStatus>
|
|
70
|
+
>({});
|
|
71
|
+
const createdAtByMessageIdRef = useRef(new Map<string, Date>());
|
|
72
|
+
|
|
73
|
+
const hasExecutingTools = Object.values(toolStatuses).some(
|
|
74
|
+
(status) => status?.type === "executing",
|
|
75
|
+
);
|
|
76
|
+
const isRunning =
|
|
77
|
+
agent.status === "submitted" ||
|
|
78
|
+
agent.status === "streaming" ||
|
|
79
|
+
hasExecutingTools;
|
|
80
|
+
|
|
81
|
+
const messages = useMemo(() => {
|
|
82
|
+
const createdAtByMessageId = createdAtByMessageIdRef.current;
|
|
83
|
+
const messageIds = new Set(
|
|
84
|
+
agent.data.messages.map((message) => message.id),
|
|
85
|
+
);
|
|
86
|
+
for (const messageId of createdAtByMessageId.keys()) {
|
|
87
|
+
if (!messageIds.has(messageId)) createdAtByMessageId.delete(messageId);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return convertEveMessages(agent.data, {
|
|
91
|
+
isRunning,
|
|
92
|
+
getCreatedAt: (message) => {
|
|
93
|
+
const existing = createdAtByMessageId.get(message.id);
|
|
94
|
+
if (existing) return existing;
|
|
95
|
+
|
|
96
|
+
const createdAt = new Date();
|
|
97
|
+
createdAtByMessageId.set(message.id, createdAt);
|
|
98
|
+
return createdAt;
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
}, [agent.data, isRunning]);
|
|
102
|
+
|
|
103
|
+
return useExternalStoreRuntime({
|
|
104
|
+
...pickExternalStoreSharedOptions(options),
|
|
105
|
+
messages,
|
|
106
|
+
isRunning,
|
|
107
|
+
unstable_enableToolInvocations: true,
|
|
108
|
+
setToolStatuses,
|
|
109
|
+
adapters: {
|
|
110
|
+
attachments: adapters?.attachments ?? runtimeAdapters?.attachments,
|
|
111
|
+
speech: adapters?.speech,
|
|
112
|
+
dictation: adapters?.dictation,
|
|
113
|
+
voice: adapters?.voice,
|
|
114
|
+
feedback: adapters?.feedback,
|
|
115
|
+
},
|
|
116
|
+
onNew: async (message) => {
|
|
117
|
+
await agent.send({ message: getEveMessageContent(message) });
|
|
118
|
+
},
|
|
119
|
+
onCancel: () => {
|
|
120
|
+
agent.stop();
|
|
121
|
+
return Promise.resolve();
|
|
122
|
+
},
|
|
123
|
+
onRespondToToolApproval: async (response) => {
|
|
124
|
+
await agent.send({ inputResponses: [toEveInputResponse(response)] });
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
};
|