@economic/agents-react 1.6.0 → 1.6.3

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 CHANGED
@@ -111,7 +111,12 @@ function Chat({ userId, authToken }: { userId: string; authToken: string }) {
111
111
  ))}
112
112
 
113
113
  <Composer
114
- onSend={(text) => chat.chat.sendMessage({ role: "user", parts: [{ type: "text", text }] })}
114
+ onSend={(text) =>
115
+ chat.chat.sendMessage({
116
+ role: "user",
117
+ parts: [{ type: "text", text }],
118
+ })
119
+ }
115
120
  />
116
121
  </div>
117
122
  );
@@ -168,7 +173,13 @@ All hooks share these connection options (`useAssistant` omits `actorId`):
168
173
  ## Types
169
174
 
170
175
  ```ts
171
- type AgentConnectionStatus = "connecting" | "connected" | "disconnected" | "unauthorized" | "error";
176
+ type AgentConnectionStatus =
177
+ | "connecting"
178
+ | "reconnecting"
179
+ | "connected"
180
+ | "disconnected"
181
+ | "unauthorized"
182
+ | "error";
172
183
  type AgentConnectionType = "agent" | "chat" | "assistant";
173
184
 
174
185
  type AgentConnectionState = {
@@ -202,5 +213,3 @@ The package also exports the handler types `AssistantActions`, `GetChatsHandler`
202
213
  ## Authentication
203
214
 
204
215
  Pass a JWT as `authToken`. It is sent both as an `Authorization: Bearer …` header and as a WebSocket subprotocol (`["bearer", token]`), matching the server-side [`getJwtAuthConfig`](../agents/README.md#authentication). When verification fails, the connection status becomes `"unauthorized"`.
205
-
206
- .
package/dist/index.d.mts CHANGED
@@ -7,7 +7,7 @@ type Agent = Omit<_CfAgent, "call"> & {
7
7
  call<T = unknown>(method: string, args?: unknown[], options?: unknown): Promise<T>;
8
8
  };
9
9
  type Chat = ReturnType<typeof useAgentChat>;
10
- type AgentConnectionStatus = "connecting" | "connected" | "disconnected" | "unauthorized" | "error";
10
+ type AgentConnectionStatus = "connecting" | "reconnecting" | "connected" | "disconnected" | "unauthorized" | "error";
11
11
  type AgentConnectionType = "agent" | "chat" | "assistant";
12
12
  type ChatSummary = {
13
13
  name: string; /** True when this entry only represents the active, unregistered chat facet. */
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,20 @@ 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" || status === "reconnecting") && state.connectionId === connectionId && state.status === "error") return state;
19
+ return {
20
+ connectionId,
21
+ status
22
+ };
23
+ }
24
+ function getAgentCloseStatus(code, shouldReconnect) {
25
+ if (code >= 3e3) return "unauthorized";
26
+ return shouldReconnect ? "reconnecting" : "disconnected";
27
+ }
14
28
  function getCloseReason(code) {
15
29
  return {
16
30
  1e3: "normal-closure",
@@ -29,93 +43,166 @@ function getCloseReason(code) {
29
43
  1015: "tls-handshake-failure"
30
44
  }[code] ?? "unknown";
31
45
  }
46
+ function getDefaultHost() {
47
+ return globalThis.window?.location?.host || "dummy-domain.com";
48
+ }
49
+ function normalizeAgentName(name) {
50
+ if (name === name.toUpperCase() && name !== name.toLowerCase()) return name.toLowerCase().replace(/_/g, "-");
51
+ const kebabified = name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
52
+ return (kebabified.startsWith("-") ? kebabified.slice(1) : kebabified).replace(/_/g, "-").replace(/-$/, "");
53
+ }
54
+ function getSubPath(sub) {
55
+ if (!sub?.length) return void 0;
56
+ return sub.flatMap(({ agent, name }) => [
57
+ "sub",
58
+ normalizeAgentName(agent),
59
+ encodeURIComponent(name)
60
+ ]).join("/");
61
+ }
62
+ function getAgentConnectionTarget(options) {
63
+ const effectiveName = options.actorId ? `${options.actorId}:${options.name}` : options.name;
64
+ return {
65
+ enabled: options.enabled ?? true,
66
+ host: options.host || getDefaultHost(),
67
+ agentName: normalizeAgentName(options.agentName),
68
+ name: effectiveName || "default",
69
+ subPath: getSubPath(options.sub),
70
+ authToken: options.authToken || void 0
71
+ };
72
+ }
73
+ function getAgentSocketTarget(agent) {
74
+ const options = agent.partySocketOptions;
75
+ if (typeof options?.host !== "string" || typeof options.party !== "string" || typeof options.room !== "string") return;
76
+ const protocols = options.protocols;
77
+ const authToken = Array.isArray(protocols) && protocols[0] === "bearer" && typeof protocols[1] === "string" ? protocols[1] : void 0;
78
+ return {
79
+ host: options.host,
80
+ agentName: options.party,
81
+ name: options.room,
82
+ subPath: typeof options.path === "string" && options.path ? options.path : void 0,
83
+ authToken
84
+ };
85
+ }
86
+ function isSameSocketTarget(a, b) {
87
+ return a.host === b.host && a.agentName === b.agentName && a.name === b.name && a.subPath === b.subPath && a.authToken === b.authToken;
88
+ }
32
89
  function isSameConnectionTarget(a, b) {
33
- if (a.enabled !== b.enabled || a.host !== b.host || a.agentName !== b.agentName || a.actorId !== b.actorId || a.name !== b.name || a.authToken !== b.authToken || a.sub?.length !== b.sub?.length) return false;
34
- return a.sub?.every((subAgent, index) => subAgent.agent === b.sub?.[index]?.agent && subAgent.name === b.sub?.[index]?.name) ?? true;
90
+ return a.enabled === b.enabled && isSameSocketTarget(a, b);
91
+ }
92
+ function getNextAgentConnection(connection, target) {
93
+ return isSameConnectionTarget(connection.target, target) ? connection : {
94
+ id: connection.id + 1,
95
+ target
96
+ };
97
+ }
98
+ function isCurrentAgentSocketTarget(eventConnection, connection) {
99
+ if (!connection.target.enabled || !eventConnection) return false;
100
+ const socketTarget = getAgentSocketTarget(eventConnection);
101
+ return socketTarget !== void 0 && isSameSocketTarget(socketTarget, connection.target);
35
102
  }
36
103
  function useAgent(options) {
37
104
  const { enabled, host, agentName, actorId, name, sub, authToken, onError, onOpen, onClose, onMessage } = options;
38
- const target = {
39
- enabled,
40
- host,
41
- agentName,
42
- actorId,
43
- name,
44
- sub,
45
- authToken
46
- };
47
- const connection = useRef({
105
+ const target = getAgentConnectionTarget(options);
106
+ const [connection, setConnection] = useState({
48
107
  id: 0,
49
108
  target
50
109
  });
51
- if (!isSameConnectionTarget(connection.current.target, target)) connection.current = {
52
- id: connection.current.id + 1,
53
- target
54
- };
55
- const connectionId = connection.current.id;
56
- const currentConnectionId = useRef(connectionId);
57
- currentConnectionId.current = connectionId;
110
+ const nextConnection = getNextAgentConnection(connection, target);
111
+ if (nextConnection !== connection) setConnection(nextConnection);
112
+ const connectionId = nextConnection.id;
58
113
  const [connectionType, setConnectionType] = useState();
59
114
  const type = connectionType?.connectionId === connectionId ? connectionType.type : void 0;
60
115
  const [connectionStatus, setConnectionStatus] = useState({
61
116
  connectionId,
62
117
  status: "connecting"
63
118
  });
64
- const status = connectionStatus.connectionId === connectionId ? connectionStatus.status : "connecting";
65
- const connectedConnectionId = useRef(void 0);
66
- const setStatus = (nextStatus) => {
67
- if (currentConnectionId.current !== connectionId) return;
68
- if (nextStatus === "connected") connectedConnectionId.current = connectionId;
69
- else if (nextStatus === "disconnected" && connectedConnectionId.current !== connectionId) return;
70
- setConnectionStatus({
71
- connectionId,
72
- status: nextStatus
73
- });
74
- };
75
- const setType = (nextType) => {
76
- if (currentConnectionId.current === connectionId) setConnectionType({
77
- connectionId,
119
+ const status = getAgentConnectionStatus(connectionStatus, connectionId);
120
+ const committedConnection = useRef({
121
+ connection: nextConnection,
122
+ callbacks: {
123
+ onOpen,
124
+ onClose,
125
+ onError,
126
+ onMessage
127
+ }
128
+ });
129
+ const setStatus = useCallback((nextStatus) => {
130
+ const committedConnectionId = committedConnection.current.connection.id;
131
+ setConnectionStatus((current) => getNextAgentConnectionState(current, committedConnectionId, nextStatus));
132
+ }, []);
133
+ const setType = useCallback((nextType) => {
134
+ setConnectionType({
135
+ connectionId: committedConnection.current.connection.id,
78
136
  type: nextType
79
137
  });
80
- };
81
- const currentAgent = useRef(null);
138
+ }, []);
139
+ const isCurrentConnectionEvent = useCallback((event) => {
140
+ const committed = committedConnection.current;
141
+ return isCurrentAgentSocketTarget(event.target, committed.connection);
142
+ }, []);
143
+ const handleOpen = useCallback((event) => {
144
+ if (!isCurrentConnectionEvent(event)) return;
145
+ committedConnection.current.callbacks.onOpen?.(event);
146
+ }, [isCurrentConnectionEvent]);
147
+ const handleClose = useCallback((event) => {
148
+ if (!isCurrentConnectionEvent(event)) return;
149
+ const shouldReconnect = event.target.shouldReconnect;
150
+ setStatus(getAgentCloseStatus(event.code, shouldReconnect));
151
+ if (event.code < 3e3 && !event.wasClean) console.warn("[useAgent] WebSocket closed unexpectedly", {
152
+ code: event.code,
153
+ reason: event.reason,
154
+ closeReason: getCloseReason(event.code),
155
+ wasClean: event.wasClean
156
+ });
157
+ committedConnection.current.callbacks.onClose?.(event);
158
+ }, [isCurrentConnectionEvent, setStatus]);
159
+ const handleMessage = useCallback((event) => {
160
+ if (!isCurrentConnectionEvent(event)) return;
161
+ const msg = JSON.parse(event.data);
162
+ if (msg.type === "connection_status") {
163
+ if (msg.status === "unauthorized") event.target.close(3e3);
164
+ setStatus(msg.status);
165
+ } else if (msg.type === "agent_type") setType(msg.agentType);
166
+ committedConnection.current.callbacks.onMessage?.(event);
167
+ }, [
168
+ isCurrentConnectionEvent,
169
+ setStatus,
170
+ setType
171
+ ]);
172
+ const handleError = useCallback((event) => {
173
+ if (!isCurrentConnectionEvent(event)) return;
174
+ setStatus(event.target.shouldReconnect ? "reconnecting" : "error");
175
+ committedConnection.current.callbacks.onError?.(event);
176
+ }, [isCurrentConnectionEvent, setStatus]);
82
177
  const agent = useAgent$1({
83
178
  enabled,
84
179
  host,
85
180
  agent: agentName,
86
181
  name: actorId ? `${actorId}:${name}` : name,
87
182
  sub,
88
- onOpen: (event) => {
89
- onOpen?.(event);
90
- },
91
- onClose: (event) => {
92
- if (!event.target || event.target === currentAgent.current) if (event.code >= 3e3) setStatus("unauthorized");
93
- else {
94
- setStatus("disconnected");
95
- if (!event.wasClean) console.warn("[useAgent] WebSocket closed unexpectedly", {
96
- code: event.code,
97
- reason: event.reason,
98
- closeReason: getCloseReason(event.code),
99
- wasClean: event.wasClean
100
- });
101
- }
102
- onClose?.(event);
103
- },
104
- onMessage: (event) => {
105
- const msg = JSON.parse(event.data);
106
- if (msg.type === "connection_status" && (!event.target || event.target === currentAgent.current)) {
107
- if (msg.status === "unauthorized") agent.close(3e3);
108
- setStatus(msg.status);
109
- } else if (msg.type === "agent_type" && (!event.target || event.target === currentAgent.current)) setType(msg.agentType);
110
- onMessage?.(event);
111
- },
112
- onError: (event) => {
113
- if (!event.target || event.target === currentAgent.current) setStatus("error");
114
- onError?.(event);
115
- },
183
+ onOpen: handleOpen,
184
+ onClose: handleClose,
185
+ onMessage: handleMessage,
186
+ onError: handleError,
116
187
  ...getTokenArgs(authToken)
117
188
  });
118
- currentAgent.current = agent;
189
+ useLayoutEffect(() => {
190
+ committedConnection.current = {
191
+ connection: nextConnection,
192
+ callbacks: {
193
+ onOpen,
194
+ onClose,
195
+ onError,
196
+ onMessage
197
+ }
198
+ };
199
+ }, [
200
+ nextConnection,
201
+ onOpen,
202
+ onClose,
203
+ onError,
204
+ onMessage
205
+ ]);
119
206
  return {
120
207
  agent,
121
208
  status,
@@ -173,6 +260,11 @@ function useChat(options) {
173
260
  }
174
261
  //#endregion
175
262
  //#region src/useAssistant.ts
263
+ function getAssistantConnectionStatus(currentChatName, isChatEnabled, assistantStatus, chatStatus) {
264
+ if (currentChatName === void 0) return assistantStatus;
265
+ if (!isChatEnabled) return assistantStatus === "connected" ? "connecting" : assistantStatus;
266
+ return chatStatus;
267
+ }
176
268
  function useAssistant(options) {
177
269
  const { toolContext, welcomeMessage, initialChatId, ...agentOptions } = options;
178
270
  const [chatId, setChatId] = useState(initialChatId);
@@ -262,7 +354,6 @@ function useAssistant(options) {
262
354
  await refreshChats();
263
355
  };
264
356
  const isChatEnabled = !!chatId && !!subAgentName;
265
- const isChatReady = isChatEnabled && assistantStatus === "connected";
266
357
  const chat = useChat({
267
358
  ...agentOptions,
268
359
  enabled: isChatEnabled,
@@ -270,7 +361,6 @@ function useAssistant(options) {
270
361
  agent: subAgentName,
271
362
  name: chatId
272
363
  }] : void 0,
273
- getInitialMessages: isChatReady ? void 0 : null,
274
364
  toolContext,
275
365
  welcomeMessage
276
366
  });
@@ -287,7 +377,7 @@ function useAssistant(options) {
287
377
  draftChat,
288
378
  scheduleDraftRefresh
289
379
  ]);
290
- const status = isChatEnabled && chat.status !== "connecting" ? chat.status : assistantStatus;
380
+ const status = getAssistantConnectionStatus(chatId, isChatEnabled, assistantStatus, chat.status);
291
381
  return {
292
382
  currentChatName: chatId,
293
383
  chats: draftChat && draftChat.name === chatId && !chats.some((persistedChat) => persistedChat.name === draftChat.name) ? [draftChat, ...chats] : chats,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@economic/agents-react",
3
- "version": "1.6.0",
3
+ "version": "1.6.3",
4
4
  "license": "MIT",
5
5
  "files": [
6
6
  "dist"