@economic/agents-react 1.5.0 → 1.6.1
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/README.md +14 -8
- package/dist/index.d.mts +2 -1
- package/dist/index.mjs +227 -38
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -123,22 +123,27 @@ function Chat({ userId, authToken }: { userId: string; authToken: string }) {
|
|
|
123
123
|
| Field | Type | Description |
|
|
124
124
|
| ----------------- | ----------------------- | --------------------------------------------------------------------------- |
|
|
125
125
|
| `status` | `AgentConnectionStatus` | Connection status of the assistant, or of the active chat once one is open. |
|
|
126
|
-
| `chats` | `ChatSummary[]` |
|
|
126
|
+
| `chats` | `ChatSummary[]` | Started chats plus the active empty draft, most recently updated first. |
|
|
127
127
|
| `currentChatName` | `string \| undefined` | Id of the active chat, if any. |
|
|
128
|
-
| `assistant` | `AssistantActions` | `getChats`, `createChat`, `openChat`, `deleteChat`.
|
|
128
|
+
| `assistant` | `AssistantActions` | `getChats`, `createChat`, `openChat`, `deleteChat`, `renameChat`. |
|
|
129
129
|
| `chat` | `useChat` result | The active chat — `chat.chat` is the message interface (see below). |
|
|
130
130
|
|
|
131
131
|
`assistant` actions:
|
|
132
132
|
|
|
133
|
-
| Action | Signature
|
|
134
|
-
| -------------------- |
|
|
135
|
-
| `createChat()` | `() => Promise<string>`
|
|
136
|
-
| `openChat(chatId)` | `(chatId: string) => void`
|
|
137
|
-
| `getChats()` | `() => Promise<ChatSummary[]>`
|
|
138
|
-
| `deleteChat(chatId)` | `(chatId: string) => Promise<void>`
|
|
133
|
+
| Action | Signature | Description |
|
|
134
|
+
| -------------------- | -------------------------------------------------- | ------------------------------------------------ |
|
|
135
|
+
| `createChat()` | `() => Promise<string>` | Creates a chat, makes it active, returns its id. |
|
|
136
|
+
| `openChat(chatId)` | `(chatId: string) => void` | Switches the active chat. |
|
|
137
|
+
| `getChats()` | `() => Promise<ChatSummary[]>` | Refreshes and returns the chat list. |
|
|
138
|
+
| `deleteChat(chatId)` | `(chatId: string) => Promise<void>` | Deletes a chat and clears the active selection. |
|
|
139
|
+
| `renameChat(chatId)` | `(chatId: string, title: string) => Promise<void>` | Renames a registered chat. |
|
|
139
140
|
|
|
140
141
|
> Titles and summaries are generated server-side a few seconds after the first message, so refetch with `getChats()` shortly after sending the opening message if you want the new title to appear.
|
|
141
142
|
|
|
143
|
+
The active empty chat is represented locally until its first turn registers it on the server.
|
|
144
|
+
|
|
145
|
+
After the active draft's first turn finishes, `useAssistant` refreshes the chat list automatically so the temporary entry is replaced by its persisted title and summary.
|
|
146
|
+
|
|
142
147
|
---
|
|
143
148
|
|
|
144
149
|
## Options
|
|
@@ -174,6 +179,7 @@ type AgentConnectionState = {
|
|
|
174
179
|
|
|
175
180
|
type ChatSummary = {
|
|
176
181
|
name: string;
|
|
182
|
+
isTemporary?: boolean; // true for the active chat before its first turn is registered
|
|
177
183
|
title?: string;
|
|
178
184
|
summary?: string;
|
|
179
185
|
created_at: number;
|
package/dist/index.d.mts
CHANGED
|
@@ -10,7 +10,8 @@ type Chat = ReturnType<typeof useAgentChat>;
|
|
|
10
10
|
type AgentConnectionStatus = "connecting" | "connected" | "disconnected" | "unauthorized" | "error";
|
|
11
11
|
type AgentConnectionType = "agent" | "chat" | "assistant";
|
|
12
12
|
type ChatSummary = {
|
|
13
|
-
name: string;
|
|
13
|
+
name: string; /** True when this entry only represents the active, unregistered chat facet. */
|
|
14
|
+
isTemporary?: boolean;
|
|
14
15
|
title?: string;
|
|
15
16
|
summary?: string;
|
|
16
17
|
title_set_by_user?: number;
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, useEffect, useRef, useState } from "react";
|
|
1
|
+
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
2
2
|
import { useAgent as useAgent$1 } from "agents/react";
|
|
3
3
|
import { getToolApproval, getToolCallId, getToolPartState, useAgentChat } from "@cloudflare/ai-chat/react";
|
|
4
4
|
//#region src/util.ts
|
|
@@ -11,6 +11,16 @@ function getTokenArgs(authToken) {
|
|
|
11
11
|
}
|
|
12
12
|
//#endregion
|
|
13
13
|
//#region src/useAgent.ts
|
|
14
|
+
function getAgentConnectionStatus(state, connectionId) {
|
|
15
|
+
return state.connectionId === connectionId ? state.status : "connecting";
|
|
16
|
+
}
|
|
17
|
+
function getNextAgentConnectionState(state, connectionId, status) {
|
|
18
|
+
if (status === "disconnected" && state.connectionId === connectionId && state.status === "error") return state;
|
|
19
|
+
return {
|
|
20
|
+
connectionId,
|
|
21
|
+
status
|
|
22
|
+
};
|
|
23
|
+
}
|
|
14
24
|
function getCloseReason(code) {
|
|
15
25
|
return {
|
|
16
26
|
1e3: "normal-closure",
|
|
@@ -29,46 +39,168 @@ function getCloseReason(code) {
|
|
|
29
39
|
1015: "tls-handshake-failure"
|
|
30
40
|
}[code] ?? "unknown";
|
|
31
41
|
}
|
|
42
|
+
function getDefaultHost() {
|
|
43
|
+
return globalThis.window?.location?.host || "dummy-domain.com";
|
|
44
|
+
}
|
|
45
|
+
function normalizeAgentName(name) {
|
|
46
|
+
if (name === name.toUpperCase() && name !== name.toLowerCase()) return name.toLowerCase().replace(/_/g, "-");
|
|
47
|
+
const kebabified = name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
|
|
48
|
+
return (kebabified.startsWith("-") ? kebabified.slice(1) : kebabified).replace(/_/g, "-").replace(/-$/, "");
|
|
49
|
+
}
|
|
50
|
+
function getSubPath(sub) {
|
|
51
|
+
if (!sub?.length) return void 0;
|
|
52
|
+
return sub.flatMap(({ agent, name }) => [
|
|
53
|
+
"sub",
|
|
54
|
+
normalizeAgentName(agent),
|
|
55
|
+
encodeURIComponent(name)
|
|
56
|
+
]).join("/");
|
|
57
|
+
}
|
|
58
|
+
function getAgentConnectionTarget(options) {
|
|
59
|
+
const effectiveName = options.actorId ? `${options.actorId}:${options.name}` : options.name;
|
|
60
|
+
return {
|
|
61
|
+
enabled: options.enabled ?? true,
|
|
62
|
+
host: options.host || getDefaultHost(),
|
|
63
|
+
agentName: normalizeAgentName(options.agentName),
|
|
64
|
+
name: effectiveName || "default",
|
|
65
|
+
subPath: getSubPath(options.sub),
|
|
66
|
+
authToken: options.authToken || void 0
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function getAgentSocketTarget(agent) {
|
|
70
|
+
const options = agent.partySocketOptions;
|
|
71
|
+
if (typeof options?.host !== "string" || typeof options.party !== "string" || typeof options.room !== "string") return;
|
|
72
|
+
const protocols = options.protocols;
|
|
73
|
+
const authToken = Array.isArray(protocols) && protocols[0] === "bearer" && typeof protocols[1] === "string" ? protocols[1] : void 0;
|
|
74
|
+
return {
|
|
75
|
+
host: options.host,
|
|
76
|
+
agentName: options.party,
|
|
77
|
+
name: options.room,
|
|
78
|
+
subPath: typeof options.path === "string" && options.path ? options.path : void 0,
|
|
79
|
+
authToken
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function isSameSocketTarget(a, b) {
|
|
83
|
+
return a.host === b.host && a.agentName === b.agentName && a.name === b.name && a.subPath === b.subPath && a.authToken === b.authToken;
|
|
84
|
+
}
|
|
85
|
+
function isSameConnectionTarget(a, b) {
|
|
86
|
+
return a.enabled === b.enabled && isSameSocketTarget(a, b);
|
|
87
|
+
}
|
|
88
|
+
function getNextAgentConnection(connection, target) {
|
|
89
|
+
return isSameConnectionTarget(connection.target, target) ? connection : {
|
|
90
|
+
id: connection.id + 1,
|
|
91
|
+
target
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function isCurrentAgentSocketTarget(eventConnection, connection) {
|
|
95
|
+
if (!connection.target.enabled || !eventConnection) return false;
|
|
96
|
+
const socketTarget = getAgentSocketTarget(eventConnection);
|
|
97
|
+
return socketTarget !== void 0 && isSameSocketTarget(socketTarget, connection.target);
|
|
98
|
+
}
|
|
32
99
|
function useAgent(options) {
|
|
33
100
|
const { enabled, host, agentName, actorId, name, sub, authToken, onError, onOpen, onClose, onMessage } = options;
|
|
34
|
-
const
|
|
35
|
-
const [
|
|
101
|
+
const target = getAgentConnectionTarget(options);
|
|
102
|
+
const [connection, setConnection] = useState({
|
|
103
|
+
id: 0,
|
|
104
|
+
target
|
|
105
|
+
});
|
|
106
|
+
const nextConnection = getNextAgentConnection(connection, target);
|
|
107
|
+
if (nextConnection !== connection) setConnection(nextConnection);
|
|
108
|
+
const connectionId = nextConnection.id;
|
|
109
|
+
const [connectionType, setConnectionType] = useState();
|
|
110
|
+
const type = connectionType?.connectionId === connectionId ? connectionType.type : void 0;
|
|
111
|
+
const [connectionStatus, setConnectionStatus] = useState({
|
|
112
|
+
connectionId,
|
|
113
|
+
status: "connecting"
|
|
114
|
+
});
|
|
115
|
+
const status = getAgentConnectionStatus(connectionStatus, connectionId);
|
|
116
|
+
const committedConnection = useRef({
|
|
117
|
+
connection: nextConnection,
|
|
118
|
+
callbacks: {
|
|
119
|
+
onOpen,
|
|
120
|
+
onClose,
|
|
121
|
+
onError,
|
|
122
|
+
onMessage
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
const setStatus = useCallback((nextStatus) => {
|
|
126
|
+
const committedConnectionId = committedConnection.current.connection.id;
|
|
127
|
+
setConnectionStatus((current) => getNextAgentConnectionState(current, committedConnectionId, nextStatus));
|
|
128
|
+
}, []);
|
|
129
|
+
const setType = useCallback((nextType) => {
|
|
130
|
+
setConnectionType({
|
|
131
|
+
connectionId: committedConnection.current.connection.id,
|
|
132
|
+
type: nextType
|
|
133
|
+
});
|
|
134
|
+
}, []);
|
|
135
|
+
const isCurrentConnectionEvent = useCallback((event) => {
|
|
136
|
+
const committed = committedConnection.current;
|
|
137
|
+
return isCurrentAgentSocketTarget(event.target, committed.connection);
|
|
138
|
+
}, []);
|
|
139
|
+
const handleOpen = useCallback((event) => {
|
|
140
|
+
if (!isCurrentConnectionEvent(event)) return;
|
|
141
|
+
committedConnection.current.callbacks.onOpen?.(event);
|
|
142
|
+
}, [isCurrentConnectionEvent]);
|
|
143
|
+
const handleClose = useCallback((event) => {
|
|
144
|
+
if (!isCurrentConnectionEvent(event)) return;
|
|
145
|
+
if (event.code >= 3e3) setStatus("unauthorized");
|
|
146
|
+
else {
|
|
147
|
+
setStatus("disconnected");
|
|
148
|
+
if (!event.wasClean) console.warn("[useAgent] WebSocket closed unexpectedly", {
|
|
149
|
+
code: event.code,
|
|
150
|
+
reason: event.reason,
|
|
151
|
+
closeReason: getCloseReason(event.code),
|
|
152
|
+
wasClean: event.wasClean
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
committedConnection.current.callbacks.onClose?.(event);
|
|
156
|
+
}, [isCurrentConnectionEvent, setStatus]);
|
|
157
|
+
const handleMessage = useCallback((event) => {
|
|
158
|
+
if (!isCurrentConnectionEvent(event)) return;
|
|
159
|
+
const msg = JSON.parse(event.data);
|
|
160
|
+
if (msg.type === "connection_status") {
|
|
161
|
+
if (msg.status === "unauthorized") event.target.close(3e3);
|
|
162
|
+
setStatus(msg.status);
|
|
163
|
+
} else if (msg.type === "agent_type") setType(msg.agentType);
|
|
164
|
+
committedConnection.current.callbacks.onMessage?.(event);
|
|
165
|
+
}, [
|
|
166
|
+
isCurrentConnectionEvent,
|
|
167
|
+
setStatus,
|
|
168
|
+
setType
|
|
169
|
+
]);
|
|
170
|
+
const handleError = useCallback((event) => {
|
|
171
|
+
if (!isCurrentConnectionEvent(event)) return;
|
|
172
|
+
setStatus("error");
|
|
173
|
+
committedConnection.current.callbacks.onError?.(event);
|
|
174
|
+
}, [isCurrentConnectionEvent, setStatus]);
|
|
36
175
|
const agent = useAgent$1({
|
|
37
176
|
enabled,
|
|
38
177
|
host,
|
|
39
178
|
agent: agentName,
|
|
40
179
|
name: actorId ? `${actorId}:${name}` : name,
|
|
41
180
|
sub,
|
|
42
|
-
onOpen:
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (event.code >= 3e3) setStatus("unauthorized");
|
|
47
|
-
else {
|
|
48
|
-
setStatus("disconnected");
|
|
49
|
-
if (!event.wasClean) console.warn("[useAgent] WebSocket closed unexpectedly", {
|
|
50
|
-
code: event.code,
|
|
51
|
-
reason: event.reason,
|
|
52
|
-
closeReason: getCloseReason(event.code),
|
|
53
|
-
wasClean: event.wasClean
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
onClose?.(event);
|
|
57
|
-
},
|
|
58
|
-
onMessage: (event) => {
|
|
59
|
-
const msg = JSON.parse(event.data);
|
|
60
|
-
if (msg.type === "connection_status") {
|
|
61
|
-
if (msg.status === "unauthorized") agent.close(3e3);
|
|
62
|
-
setStatus(msg.status);
|
|
63
|
-
} else if (msg.type === "agent_type") setType(msg.agentType);
|
|
64
|
-
onMessage?.(event);
|
|
65
|
-
},
|
|
66
|
-
onError: (event) => {
|
|
67
|
-
setStatus("error");
|
|
68
|
-
onError?.(event);
|
|
69
|
-
},
|
|
181
|
+
onOpen: handleOpen,
|
|
182
|
+
onClose: handleClose,
|
|
183
|
+
onMessage: handleMessage,
|
|
184
|
+
onError: handleError,
|
|
70
185
|
...getTokenArgs(authToken)
|
|
71
186
|
});
|
|
187
|
+
useLayoutEffect(() => {
|
|
188
|
+
committedConnection.current = {
|
|
189
|
+
connection: nextConnection,
|
|
190
|
+
callbacks: {
|
|
191
|
+
onOpen,
|
|
192
|
+
onClose,
|
|
193
|
+
onError,
|
|
194
|
+
onMessage
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
}, [
|
|
198
|
+
nextConnection,
|
|
199
|
+
onOpen,
|
|
200
|
+
onClose,
|
|
201
|
+
onError,
|
|
202
|
+
onMessage
|
|
203
|
+
]);
|
|
72
204
|
return {
|
|
73
205
|
agent,
|
|
74
206
|
status,
|
|
@@ -126,10 +258,25 @@ function useChat(options) {
|
|
|
126
258
|
}
|
|
127
259
|
//#endregion
|
|
128
260
|
//#region src/useAssistant.ts
|
|
261
|
+
function getAssistantConnectionStatus(currentChatName, isChatEnabled, assistantStatus, chatStatus) {
|
|
262
|
+
if (currentChatName === void 0) return assistantStatus;
|
|
263
|
+
if (!isChatEnabled) return assistantStatus === "connected" ? "connecting" : assistantStatus;
|
|
264
|
+
return chatStatus;
|
|
265
|
+
}
|
|
129
266
|
function useAssistant(options) {
|
|
130
267
|
const { toolContext, welcomeMessage, initialChatId, ...agentOptions } = options;
|
|
131
268
|
const [chatId, setChatId] = useState(initialChatId);
|
|
132
269
|
const [chats, setChats] = useState([]);
|
|
270
|
+
const [draftChat, setDraftChat] = useState(() => {
|
|
271
|
+
if (!initialChatId) return void 0;
|
|
272
|
+
const now = Date.now();
|
|
273
|
+
return {
|
|
274
|
+
name: initialChatId,
|
|
275
|
+
isTemporary: true,
|
|
276
|
+
created_at: now,
|
|
277
|
+
updated_at: now
|
|
278
|
+
};
|
|
279
|
+
});
|
|
133
280
|
const [subAgentName, setSubAgentName] = useState(void 0);
|
|
134
281
|
const { agent: assistant, status: assistantStatus } = useAgent({
|
|
135
282
|
...agentOptions,
|
|
@@ -148,8 +295,29 @@ function useAssistant(options) {
|
|
|
148
295
|
return c;
|
|
149
296
|
});
|
|
150
297
|
setChats(fetched);
|
|
298
|
+
setDraftChat((current) => {
|
|
299
|
+
if (current && fetched.some((chat) => chat.name === current.name)) return;
|
|
300
|
+
return current;
|
|
301
|
+
});
|
|
151
302
|
return fetched;
|
|
152
303
|
}, [assistant]);
|
|
304
|
+
const refreshChatsRef = useRef(refreshChats);
|
|
305
|
+
refreshChatsRef.current = refreshChats;
|
|
306
|
+
const draftRefreshTimeouts = useRef(/* @__PURE__ */ new Map());
|
|
307
|
+
const scheduleDraftRefresh = useCallback((draftId) => {
|
|
308
|
+
const existingTimeout = draftRefreshTimeouts.current.get(draftId);
|
|
309
|
+
if (existingTimeout) clearTimeout(existingTimeout);
|
|
310
|
+
const timeout = setTimeout(() => {
|
|
311
|
+
draftRefreshTimeouts.current.delete(draftId);
|
|
312
|
+
refreshChatsRef.current().catch(console.error);
|
|
313
|
+
}, 3e3);
|
|
314
|
+
draftRefreshTimeouts.current.set(draftId, timeout);
|
|
315
|
+
}, []);
|
|
316
|
+
useEffect(() => {
|
|
317
|
+
return () => {
|
|
318
|
+
for (const timeout of draftRefreshTimeouts.current.values()) clearTimeout(timeout);
|
|
319
|
+
};
|
|
320
|
+
}, []);
|
|
153
321
|
useEffect(() => {
|
|
154
322
|
if (assistantStatus === "connected") refreshChats().catch(console.error);
|
|
155
323
|
}, [assistantStatus, refreshChats]);
|
|
@@ -157,17 +325,26 @@ function useAssistant(options) {
|
|
|
157
325
|
return refreshChats();
|
|
158
326
|
};
|
|
159
327
|
const createChat = async () => {
|
|
328
|
+
if (draftChat && draftChat.name === chatId) scheduleDraftRefresh(draftChat.name);
|
|
160
329
|
const newChatId = await assistant.call("createChat");
|
|
330
|
+
const now = Date.now();
|
|
161
331
|
setChatId(newChatId);
|
|
162
|
-
|
|
332
|
+
setDraftChat({
|
|
333
|
+
name: newChatId,
|
|
334
|
+
isTemporary: true,
|
|
335
|
+
created_at: now,
|
|
336
|
+
updated_at: now
|
|
337
|
+
});
|
|
163
338
|
return newChatId;
|
|
164
339
|
};
|
|
165
|
-
const openChat = (
|
|
166
|
-
|
|
340
|
+
const openChat = (nextChatId) => {
|
|
341
|
+
if (draftChat && draftChat.name === chatId) scheduleDraftRefresh(draftChat.name);
|
|
342
|
+
setChatId(nextChatId);
|
|
167
343
|
};
|
|
168
344
|
const deleteChat = async (chatId) => {
|
|
169
345
|
await assistant.call("deleteChat", [chatId]);
|
|
170
346
|
setChatId(void 0);
|
|
347
|
+
setDraftChat((current) => current?.name === chatId ? void 0 : current);
|
|
171
348
|
await refreshChats();
|
|
172
349
|
};
|
|
173
350
|
const renameChat = async (chatId, title) => {
|
|
@@ -175,7 +352,6 @@ function useAssistant(options) {
|
|
|
175
352
|
await refreshChats();
|
|
176
353
|
};
|
|
177
354
|
const isChatEnabled = !!chatId && !!subAgentName;
|
|
178
|
-
const isChatReady = isChatEnabled && assistantStatus === "connected";
|
|
179
355
|
const chat = useChat({
|
|
180
356
|
...agentOptions,
|
|
181
357
|
enabled: isChatEnabled,
|
|
@@ -183,14 +359,27 @@ function useAssistant(options) {
|
|
|
183
359
|
agent: subAgentName,
|
|
184
360
|
name: chatId
|
|
185
361
|
}] : void 0,
|
|
186
|
-
getInitialMessages: isChatReady ? void 0 : null,
|
|
187
362
|
toolContext,
|
|
188
363
|
welcomeMessage
|
|
189
364
|
});
|
|
365
|
+
const previousChatStatus = useRef(chat.chat.status);
|
|
366
|
+
useEffect(() => {
|
|
367
|
+
const wasRunning = previousChatStatus.current === "submitted" || previousChatStatus.current === "streaming";
|
|
368
|
+
const hasFinished = chat.chat.status === "ready" || chat.chat.status === "error";
|
|
369
|
+
previousChatStatus.current = chat.chat.status;
|
|
370
|
+
if (!draftChat || draftChat.name !== chatId || !wasRunning || !hasFinished) return;
|
|
371
|
+
scheduleDraftRefresh(draftChat.name);
|
|
372
|
+
}, [
|
|
373
|
+
chat.chat.status,
|
|
374
|
+
chatId,
|
|
375
|
+
draftChat,
|
|
376
|
+
scheduleDraftRefresh
|
|
377
|
+
]);
|
|
378
|
+
const status = getAssistantConnectionStatus(chatId, isChatEnabled, assistantStatus, chat.status);
|
|
190
379
|
return {
|
|
191
380
|
currentChatName: chatId,
|
|
192
|
-
chats,
|
|
193
|
-
status
|
|
381
|
+
chats: draftChat && draftChat.name === chatId && !chats.some((persistedChat) => persistedChat.name === draftChat.name) ? [draftChat, ...chats] : chats,
|
|
382
|
+
status,
|
|
194
383
|
assistant: {
|
|
195
384
|
agent: assistant,
|
|
196
385
|
getChats,
|