@economic/agents-react 1.4.5 → 1.6.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/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[]` | The user's chats, most recently updated first. |
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 | 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. |
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,9 +10,11 @@ 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;
17
+ title_set_by_user?: number;
16
18
  created_at: number;
17
19
  updated_at: number;
18
20
  };
@@ -29,6 +31,7 @@ type GetChatsHandler = () => Promise<ChatSummary[]>;
29
31
  type CreateChatHandler = () => Promise<string>;
30
32
  type OpenChatHandler = (chatId: string) => void;
31
33
  type DeleteChatHandler = (chatId: string) => Promise<void>;
34
+ type RenameChatHandler = (chatId: string, title: string) => Promise<void>;
32
35
  //#endregion
33
36
  //#region src/useAgent.d.ts
34
37
  interface UseAgentReturn {
@@ -76,6 +79,7 @@ interface Assistant {
76
79
  createChat: CreateChatHandler;
77
80
  openChat: OpenChatHandler;
78
81
  deleteChat: DeleteChatHandler;
82
+ renameChat: RenameChatHandler;
79
83
  }
80
84
  interface UseAssistantReturn {
81
85
  currentChatName: string | undefined;
@@ -89,4 +93,4 @@ interface UseAssistantOptions<ToolContext extends Record<string, unknown> = Reco
89
93
  }
90
94
  declare function useAssistant<ToolContext extends Record<string, unknown>>(options: UseAssistantOptions<ToolContext>): UseAssistantReturn;
91
95
  //#endregion
92
- export { type Agent, type AgentConnectionStatus, type AgentConnectionType, type Assistant, type Chat, type ChatSummary, type CreateChatHandler, type DeleteChatHandler, type GetChatsHandler, type GetMessageFeedbackHandler, type MessageFeedback, type MessageFeedbackHandler, type OpenChatHandler, type UseAgentReturn, type UseAssistantReturn, type UseChatReturn, getToolApproval, getToolCallId, getToolPartState, useAgent, useAssistant, useChat };
96
+ export { type Agent, type AgentConnectionStatus, type AgentConnectionType, type Assistant, type Chat, type ChatSummary, type CreateChatHandler, type DeleteChatHandler, type GetChatsHandler, type GetMessageFeedbackHandler, type MessageFeedback, type MessageFeedbackHandler, type OpenChatHandler, type RenameChatHandler, type UseAgentReturn, type UseAssistantReturn, type UseChatReturn, getToolApproval, getToolCallId, getToolPartState, useAgent, useAssistant, useChat };
package/dist/index.mjs CHANGED
@@ -29,10 +29,56 @@ function getCloseReason(code) {
29
29
  1015: "tls-handshake-failure"
30
30
  }[code] ?? "unknown";
31
31
  }
32
+ 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;
35
+ }
32
36
  function useAgent(options) {
33
37
  const { enabled, host, agentName, actorId, name, sub, authToken, onError, onOpen, onClose, onMessage } = options;
34
- const [type, setType] = useState(void 0);
35
- const [status, setStatus] = useState("connecting");
38
+ const target = {
39
+ enabled,
40
+ host,
41
+ agentName,
42
+ actorId,
43
+ name,
44
+ sub,
45
+ authToken
46
+ };
47
+ const connection = useRef({
48
+ id: 0,
49
+ target
50
+ });
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;
58
+ const [connectionType, setConnectionType] = useState();
59
+ const type = connectionType?.connectionId === connectionId ? connectionType.type : void 0;
60
+ const [connectionStatus, setConnectionStatus] = useState({
61
+ connectionId,
62
+ status: "connecting"
63
+ });
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,
78
+ type: nextType
79
+ });
80
+ };
81
+ const currentAgent = useRef(null);
36
82
  const agent = useAgent$1({
37
83
  enabled,
38
84
  host,
@@ -43,7 +89,7 @@ function useAgent(options) {
43
89
  onOpen?.(event);
44
90
  },
45
91
  onClose: (event) => {
46
- if (event.code >= 3e3) setStatus("unauthorized");
92
+ if (!event.target || event.target === currentAgent.current) if (event.code >= 3e3) setStatus("unauthorized");
47
93
  else {
48
94
  setStatus("disconnected");
49
95
  if (!event.wasClean) console.warn("[useAgent] WebSocket closed unexpectedly", {
@@ -57,18 +103,19 @@ function useAgent(options) {
57
103
  },
58
104
  onMessage: (event) => {
59
105
  const msg = JSON.parse(event.data);
60
- if (msg.type === "connection_status") {
106
+ if (msg.type === "connection_status" && (!event.target || event.target === currentAgent.current)) {
61
107
  if (msg.status === "unauthorized") agent.close(3e3);
62
108
  setStatus(msg.status);
63
- } else if (msg.type === "agent_type") setType(msg.agentType);
109
+ } else if (msg.type === "agent_type" && (!event.target || event.target === currentAgent.current)) setType(msg.agentType);
64
110
  onMessage?.(event);
65
111
  },
66
112
  onError: (event) => {
67
- setStatus("error");
113
+ if (!event.target || event.target === currentAgent.current) setStatus("error");
68
114
  onError?.(event);
69
115
  },
70
116
  ...getTokenArgs(authToken)
71
117
  });
118
+ currentAgent.current = agent;
72
119
  return {
73
120
  agent,
74
121
  status,
@@ -130,6 +177,16 @@ function useAssistant(options) {
130
177
  const { toolContext, welcomeMessage, initialChatId, ...agentOptions } = options;
131
178
  const [chatId, setChatId] = useState(initialChatId);
132
179
  const [chats, setChats] = useState([]);
180
+ const [draftChat, setDraftChat] = useState(() => {
181
+ if (!initialChatId) return void 0;
182
+ const now = Date.now();
183
+ return {
184
+ name: initialChatId,
185
+ isTemporary: true,
186
+ created_at: now,
187
+ updated_at: now
188
+ };
189
+ });
133
190
  const [subAgentName, setSubAgentName] = useState(void 0);
134
191
  const { agent: assistant, status: assistantStatus } = useAgent({
135
192
  ...agentOptions,
@@ -148,8 +205,29 @@ function useAssistant(options) {
148
205
  return c;
149
206
  });
150
207
  setChats(fetched);
208
+ setDraftChat((current) => {
209
+ if (current && fetched.some((chat) => chat.name === current.name)) return;
210
+ return current;
211
+ });
151
212
  return fetched;
152
213
  }, [assistant]);
214
+ const refreshChatsRef = useRef(refreshChats);
215
+ refreshChatsRef.current = refreshChats;
216
+ const draftRefreshTimeouts = useRef(/* @__PURE__ */ new Map());
217
+ const scheduleDraftRefresh = useCallback((draftId) => {
218
+ const existingTimeout = draftRefreshTimeouts.current.get(draftId);
219
+ if (existingTimeout) clearTimeout(existingTimeout);
220
+ const timeout = setTimeout(() => {
221
+ draftRefreshTimeouts.current.delete(draftId);
222
+ refreshChatsRef.current().catch(console.error);
223
+ }, 3e3);
224
+ draftRefreshTimeouts.current.set(draftId, timeout);
225
+ }, []);
226
+ useEffect(() => {
227
+ return () => {
228
+ for (const timeout of draftRefreshTimeouts.current.values()) clearTimeout(timeout);
229
+ };
230
+ }, []);
153
231
  useEffect(() => {
154
232
  if (assistantStatus === "connected") refreshChats().catch(console.error);
155
233
  }, [assistantStatus, refreshChats]);
@@ -157,17 +235,30 @@ function useAssistant(options) {
157
235
  return refreshChats();
158
236
  };
159
237
  const createChat = async () => {
238
+ if (draftChat && draftChat.name === chatId) scheduleDraftRefresh(draftChat.name);
160
239
  const newChatId = await assistant.call("createChat");
240
+ const now = Date.now();
161
241
  setChatId(newChatId);
162
- await refreshChats();
242
+ setDraftChat({
243
+ name: newChatId,
244
+ isTemporary: true,
245
+ created_at: now,
246
+ updated_at: now
247
+ });
163
248
  return newChatId;
164
249
  };
165
- const openChat = (chatId) => {
166
- setChatId(chatId);
250
+ const openChat = (nextChatId) => {
251
+ if (draftChat && draftChat.name === chatId) scheduleDraftRefresh(draftChat.name);
252
+ setChatId(nextChatId);
167
253
  };
168
254
  const deleteChat = async (chatId) => {
169
255
  await assistant.call("deleteChat", [chatId]);
170
256
  setChatId(void 0);
257
+ setDraftChat((current) => current?.name === chatId ? void 0 : current);
258
+ await refreshChats();
259
+ };
260
+ const renameChat = async (chatId, title) => {
261
+ await assistant.call("renameChat", [chatId, title]);
171
262
  await refreshChats();
172
263
  };
173
264
  const isChatEnabled = !!chatId && !!subAgentName;
@@ -183,16 +274,31 @@ function useAssistant(options) {
183
274
  toolContext,
184
275
  welcomeMessage
185
276
  });
277
+ const previousChatStatus = useRef(chat.chat.status);
278
+ useEffect(() => {
279
+ const wasRunning = previousChatStatus.current === "submitted" || previousChatStatus.current === "streaming";
280
+ const hasFinished = chat.chat.status === "ready" || chat.chat.status === "error";
281
+ previousChatStatus.current = chat.chat.status;
282
+ if (!draftChat || draftChat.name !== chatId || !wasRunning || !hasFinished) return;
283
+ scheduleDraftRefresh(draftChat.name);
284
+ }, [
285
+ chat.chat.status,
286
+ chatId,
287
+ draftChat,
288
+ scheduleDraftRefresh
289
+ ]);
290
+ const status = isChatEnabled && chat.status !== "connecting" ? chat.status : assistantStatus;
186
291
  return {
187
292
  currentChatName: chatId,
188
- chats,
189
- status: isChatEnabled && chat.status !== "connecting" ? chat.status : assistantStatus,
293
+ chats: draftChat && draftChat.name === chatId && !chats.some((persistedChat) => persistedChat.name === draftChat.name) ? [draftChat, ...chats] : chats,
294
+ status,
190
295
  assistant: {
191
296
  agent: assistant,
192
297
  getChats,
193
298
  createChat,
194
299
  openChat,
195
- deleteChat
300
+ deleteChat,
301
+ renameChat
196
302
  },
197
303
  chat
198
304
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@economic/agents-react",
3
- "version": "1.4.5",
3
+ "version": "1.6.0",
4
4
  "license": "MIT",
5
5
  "files": [
6
6
  "dist"