@economic/agents-react 1.1.9 → 1.2.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
@@ -1,65 +1,199 @@
1
1
  # @economic/agents-react
2
2
 
3
- React hooks for connecting to AI chat agents.
3
+ React hooks for connecting to agents built with [`@economic/agents`](../agents/README.md). They wrap the Cloudflare `agents` and `@cloudflare/ai-chat` hooks and add typed connection state, message feedback, and the per-user multi-chat model used by `Assistant`.
4
4
 
5
- ## Installation
5
+ ## Install
6
6
 
7
- ```bash
7
+ ```sh
8
8
  npm install @economic/agents-react
9
9
  ```
10
10
 
11
- ## Usage
11
+ React 19 is a peer dependency.
12
+
13
+ ## Which hook do I use?
14
+
15
+ The package exports three hooks, layered on top of each other:
16
+
17
+ - **`useAgent`** — the thin connection primitive both build on. Reach for it only when you need raw access to the connection.
18
+ - **`useChat`** — connects directly to a **single** chat agent by name. Use it when you address one conversation yourself (e.g. one chat per route) rather than going through an `Assistant`.
19
+ - **`useAssistant`** — the high-level hook for the v2 model: **one assistant per user, many chats**. It manages the chat list (create / open / delete), connects to the right chat facet, and exposes the active chat. Use this when your server uses an `Assistant`.
20
+
21
+ ---
22
+
23
+ ## `useAgent`
24
+
25
+ The connection primitive `useChat` and `useAssistant` are built on. Connects to an agent by name and exposes its connection and `call` for invoking `@callable` methods.
12
26
 
13
27
  ```tsx
14
- import { useAIChatAgent } from "@economic/agents-react";
15
-
16
- const { agent, chat } = useAIChatAgent({
17
- agent: "my-agent",
18
- host: "https://my-agent.workers.dev",
19
- chatId: "session-123",
20
- toolContext: {
21
- currentDateTime: new Date().toISOString(),
22
- },
23
- connectionParams: {
24
- userId: "user-id",
25
- token: "auth-token",
26
- },
27
- onConnectionStatusChange: (status) => {
28
- console.log("Connection status:", status);
29
- },
28
+ import { useAgent } from "@economic/agents-react";
29
+
30
+ const agent = useAgent({
31
+ host: "localhost:8787",
32
+ agentName: "SupportAgent",
33
+ name: "support",
34
+ actorId: userId, // when set, the DO name becomes `${actorId}:${name}`
35
+ authToken,
30
36
  });
37
+
38
+ await agent.call("checkOrder", ["1234"]);
39
+ console.log(agent.state?.status);
40
+ ```
41
+
42
+ ---
43
+
44
+ ## `useChat`
45
+
46
+ Connects to a single chat agent by name — no `Assistant` needed. Returns the connection, the message interface, and message-feedback helpers.
47
+
48
+ ```tsx
49
+ import { useChat } from "@economic/agents-react";
50
+
51
+ const { status, chat, submitMessageFeedback, getMessageFeedback } = useChat({
52
+ host: "localhost:8787",
53
+ agentName: "MyChatAgent",
54
+ name: `${userId}:${chatId}`,
55
+ authToken,
56
+ toolContext: { locale: "en-DK" },
57
+ });
58
+
59
+ // send a message
60
+ chat.sendMessage({ role: "user", parts: [{ type: "text", text: "Hello" }] });
61
+
62
+ // rate a message (1 = up, -1 = down) and read all feedback
63
+ await submitMessageFeedback(messageId, 1, "Helpful!");
64
+ const feedback = await getMessageFeedback();
31
65
  ```
32
66
 
33
- ## API
67
+ ### Returns
68
+
69
+ | Field | Type | Description |
70
+ | ----------------------- | ------------------------------------------------ | ------------------------------------------------------------- |
71
+ | `status` | `AgentConnectionStatus` | Connection status. |
72
+ | `agent` | connection object | The underlying `useAgent` connection. |
73
+ | `chat` | `useAgentChat` result | `messages`, `sendMessage`, `setMessages`, `status`, `stop`, … |
74
+ | `submitMessageFeedback` | `(id, rating, comment?) => Promise<void>` | Submit thumbs up/down feedback for a message. |
75
+ | `getMessageFeedback` | `() => Promise<Record<string, MessageFeedback>>` | All feedback for the chat, keyed by message id. |
76
+
77
+ ---
78
+
79
+ ## `useAssistant`
80
+
81
+ For the per-user, many-chats model: connects to the user's `Assistant` DO, keeps the chat list in sync, and manages which chat is active.
82
+
83
+ ```tsx
84
+ import { useAssistant } from "@economic/agents-react";
85
+
86
+ function Chat({ userId, authToken }: { userId: string; authToken: string }) {
87
+ const { status, chats, currentChatName, assistant, chat } = useAssistant({
88
+ host: "localhost:8787",
89
+ agentName: "MyAssistant", // matches the Assistant DO binding/class name
90
+ name: userId, // the Assistant is keyed by user
91
+ authToken,
92
+ toolContext: { locale: "en-DK" }, // forwarded to server tools as the request body
93
+ welcomeMessage: "Hi! How can I help?",
94
+ });
95
+
96
+ return (
97
+ <div>
98
+ <button onClick={() => assistant.createChat()}>New chat</button>
99
+
100
+ <ul>
101
+ {chats.map((c) => (
102
+ <li key={c.name}>
103
+ <button onClick={() => assistant.openChat(c.name)}>{c.title ?? "Untitled"}</button>
104
+ <button onClick={() => assistant.deleteChat(c.name)}>Delete</button>
105
+ </li>
106
+ ))}
107
+ </ul>
108
+
109
+ {chat.chat.messages.map((m) => (
110
+ <Message key={m.id} message={m} />
111
+ ))}
112
+
113
+ <Composer
114
+ onSend={(text) => chat.chat.sendMessage({ role: "user", parts: [{ type: "text", text }] })}
115
+ />
116
+ </div>
117
+ );
118
+ }
119
+ ```
34
120
 
35
- ### `useAIChatAgent(options)`
121
+ ### Returns
36
122
 
37
- Returns `{ agent, chat }` where:
123
+ | Field | Type | Description |
124
+ | ----------------- | ----------------------- | --------------------------------------------------------------------------- |
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. |
127
+ | `currentChatName` | `string \| undefined` | Id of the active chat, if any. |
128
+ | `assistant` | `AssistantActions` | `getChats`, `createChat`, `openChat`, `deleteChat`. |
129
+ | `chat` | `useChat` result | The active chat — `chat.chat` is the message interface (see below). |
38
130
 
39
- - `agent` - The agent connection from `useAgent`
40
- - `chat` - The chat interface from `useAgentChat`
131
+ `assistant` actions:
41
132
 
42
- #### Options
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. |
43
139
 
44
- | Option | Type | Description |
45
- | -------------------------- | ----------------------------------------- | ---------------------------------------- |
46
- | `agent` | `string` | Agent name |
47
- | `host` | `string` | Agent host URL |
48
- | `chatId` | `string` | Unique chat session ID |
49
- | `basePath` | `string?` | Optional base path |
50
- | `toolContext` | `Record<string, unknown>?` | Optional context passed to tools |
51
- | `connectionParams` | `Record<string, string>?` | Optional query parameters for connection |
52
- | `onConnectionStatusChange` | `(status: AgentConnectionStatus) => void` | Callback for connection status changes |
53
- | `onOpen` | `(event: Event) => void` | WebSocket open handler |
54
- | `onClose` | `(event: CloseEvent) => void` | WebSocket close handler |
55
- | `onError` | `(event: ErrorEvent) => void` | WebSocket error handler |
140
+ > 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.
56
141
 
57
- ### `AgentConnectionStatus`
142
+ ---
143
+
144
+ ## Options
145
+
146
+ All hooks share these connection options (`useAssistant` omits `actorId`):
147
+
148
+ | Option | Type | Description |
149
+ | -------------------------------- | -------------------------- | ----------------------------------------------------------------------------- |
150
+ | `host` | `string` | Worker host, e.g. `localhost:8787` or `my-agent.workers.dev`. |
151
+ | `agentName` | `string` | The agent's DO binding/class name. |
152
+ | `name` | `string` | DO name. For `useAssistant` this is the user id; for `useChat`, the chat key. |
153
+ | `actorId` | `string?` | If set, the DO name becomes `${actorId}:${name}` (`useAgent` / `useChat`). |
154
+ | `authToken` | `string?` | Bearer token; sent as an `Authorization` header and WebSocket subprotocol. |
155
+ | `enabled` | `boolean?` | Gate the connection (defaults to enabled when a `name` is present). |
156
+ | `sub` | `{ agent; name }[]?` | Sub-agent routing (used internally by `useAssistant`). |
157
+ | `toolContext` | `Record<string, unknown>?` | Forwarded to server tools as the request body (`useChat` / `useAssistant`). |
158
+ | `welcomeMessage` | `string?` | Seeds an initial assistant message when the chat is empty. |
159
+ | `onOpen` / `onClose` / `onError` | event handlers | WebSocket lifecycle callbacks. |
160
+
161
+ ---
162
+
163
+ ## Types
58
164
 
59
165
  ```ts
60
166
  type AgentConnectionStatus = "connecting" | "connected" | "disconnected" | "unauthorized";
167
+ type AgentConnectionType = "agent" | "chat" | "assistant";
168
+
169
+ type AgentConnectionState = {
170
+ status: AgentConnectionStatus;
171
+ type: AgentConnectionType;
172
+ subAgentName?: string;
173
+ };
174
+
175
+ type ChatSummary = {
176
+ name: string;
177
+ title?: string;
178
+ summary?: string;
179
+ created_at: number;
180
+ updated_at: number;
181
+ };
182
+
183
+ type MessageFeedback = {
184
+ message_id: string;
185
+ rating: number; // 1 = up, -1 = down
186
+ comment?: string;
187
+ created_at: number;
188
+ updated_at: number;
189
+ };
61
190
  ```
62
191
 
63
- Initial publish.
192
+ The package also exports the handler types `AssistantActions`, `GetChatsHandler`, `CreateChatHandler`, `OpenChatHandler`, `DeleteChatHandler`, `MessageFeedbackHandler`, and `GetMessageFeedbackHandler`.
193
+
194
+ ---
195
+
196
+ ## Authentication
64
197
 
198
+ 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"`.
65
199
  .
package/dist/index.d.mts CHANGED
@@ -317,6 +317,7 @@ declare function useChat<ToolContext extends Record<string, unknown>>(options: U
317
317
  interface UseAssistantOptions<ToolContext extends Record<string, unknown> = Record<string, unknown>> extends Omit<UseChatOptions<ToolContext>, "actorId" | "getInitialMessages"> {}
318
318
  declare function useAssistant<ToolContext extends Record<string, unknown>>(options: UseAssistantOptions<ToolContext>): {
319
319
  currentChatName: string | undefined;
320
+ chats: ChatSummary[];
320
321
  status: AgentConnectionStatus;
321
322
  assistant: {
322
323
  getChats: () => Promise<ChatSummary[]>;
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { useAgent as useAgent$1 } from "agents/react";
2
- import { useEffect, useRef, useState } from "react";
2
+ import { useCallback, useEffect, useRef, useState } from "react";
3
3
  import { useAgentChat } from "@cloudflare/ai-chat/react";
4
4
  //#region src/util.ts
5
5
  function getTokenArgs(authToken) {
@@ -80,22 +80,31 @@ function useChat(options) {
80
80
  function useAssistant(options) {
81
81
  const { toolContext, welcomeMessage, ...agentOptions } = options;
82
82
  const [chatId, setChatId] = useState();
83
+ const [chats, setChats] = useState([]);
83
84
  const assistant = useAgent(agentOptions);
84
- const getChats = async () => {
85
- return (await assistant.call("getChats")).map((chat) => {
85
+ const refreshChats = useCallback(async () => {
86
+ setChats((await assistant.call("getChats")).map((chat) => {
86
87
  const c = {
87
88
  ...chat,
88
89
  name: chat.durable_object_name
89
90
  };
90
91
  delete c.durable_object_name;
91
92
  return c;
92
- });
93
+ }));
94
+ }, [assistant]);
95
+ const assistantStatus = assistant.state?.status;
96
+ useEffect(() => {
97
+ if (assistantStatus === "connected") refreshChats().catch(console.error);
98
+ }, [assistantStatus, refreshChats]);
99
+ const getChats = async () => {
100
+ await refreshChats();
101
+ return chats;
93
102
  };
94
103
  const createChat = async () => {
95
- const chatId = await assistant.call("createChat");
96
- setChatId(chatId);
97
- await getChats();
98
- return chatId;
104
+ const newChatId = await assistant.call("createChat");
105
+ setChatId(newChatId);
106
+ await refreshChats();
107
+ return newChatId;
99
108
  };
100
109
  const openChat = (chatId) => {
101
110
  setChatId(chatId);
@@ -103,7 +112,7 @@ function useAssistant(options) {
103
112
  const deleteChat = async (chatId) => {
104
113
  await assistant.call("deleteChat", [chatId]);
105
114
  setChatId(void 0);
106
- await getChats();
115
+ await refreshChats();
107
116
  };
108
117
  const isChatReady = !!chatId && assistant.state?.status === "connected";
109
118
  const chat = useChat({
@@ -119,6 +128,7 @@ function useAssistant(options) {
119
128
  });
120
129
  return {
121
130
  currentChatName: chatId,
131
+ chats,
122
132
  status: (chatId ? chat.agent.state?.status : assistant.state?.status) ?? "connecting",
123
133
  assistant: {
124
134
  getChats,
package/dist/v1.d.mts ADDED
@@ -0,0 +1,30 @@
1
+ import { useAgent } from "agents/react";
2
+ import { useAgentChat } from "@cloudflare/ai-chat/react";
3
+
4
+ //#region src/v1/index.d.ts
5
+ type AgentConnectionStatus = "connecting" | "connected" | "disconnected" | "unauthorized";
6
+ interface UseAIChatAgentOptions {
7
+ agent: string;
8
+ host: string;
9
+ basePath?: string;
10
+ chatId: string;
11
+ /** When set with `subAgentName`, used as the coordinator agent instance name instead of `chatId`. */
12
+ userId?: string;
13
+ subAgentName?: string;
14
+ authToken?: string;
15
+ toolContext?: Record<string, unknown>;
16
+ connectionParams?: Record<string, string>;
17
+ welcomeMessage?: string;
18
+ onConnectionStatusChange?: (status: AgentConnectionStatus) => void;
19
+ onOpen?: () => void;
20
+ onClose?: (event: CloseEvent) => void;
21
+ onError?: (event: ErrorEvent) => void;
22
+ }
23
+ type UseAIChatAgentResult = {
24
+ agent: ReturnType<typeof useAgent>;
25
+ chat: ReturnType<typeof useAgentChat>;
26
+ connectionStatus: AgentConnectionStatus;
27
+ };
28
+ declare function useAIChatAgent(options: UseAIChatAgentOptions): UseAIChatAgentResult;
29
+ //#endregion
30
+ export { AgentConnectionStatus, UseAIChatAgentOptions, useAIChatAgent };
package/dist/v1.mjs ADDED
@@ -0,0 +1,121 @@
1
+ import { useAgent } from "agents/react";
2
+ import React from "react";
3
+ import { useAgentChat } from "@cloudflare/ai-chat/react";
4
+ //#region src/v1/index.ts
5
+ function agentNameToKebab(name) {
6
+ if (name === name.toUpperCase() && name !== name.toLowerCase()) return name.toLowerCase().replace(/_/g, "-");
7
+ let result = name.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
8
+ result = result.startsWith("-") ? result.slice(1) : result;
9
+ return result.replace(/_/g, "-").replace(/-$/, "");
10
+ }
11
+ function buildAgentHttpBaseUrl(options) {
12
+ const hostPart = options.host.replace(/^https?:\/\//, "").replace(/\/$/, "");
13
+ const protocol = hostPart.startsWith("localhost:") || hostPart.startsWith("127.0.0.1:") || hostPart.startsWith("192.168.") || hostPart.startsWith("10.") ? "http" : "https";
14
+ let pathname = `/agents/${agentNameToKebab(options.agent)}/${encodeURIComponent(options.name)}`;
15
+ for (const step of options.sub ?? []) pathname += `/sub/${agentNameToKebab(step.agent)}/${encodeURIComponent(step.name)}`;
16
+ return `${protocol}://${hostPart}${pathname}`;
17
+ }
18
+ function useAIChatAgent(options) {
19
+ const { agent: agentName, host, basePath, chatId, userId, subAgentName, authToken, toolContext, connectionParams, welcomeMessage, onConnectionStatusChange, onOpen: onOpenProp, onClose: onCloseProp, onError: onErrorProp } = options;
20
+ const [connectionStatus, setConnectionStatus] = React.useState("connecting");
21
+ const updateConnectionStatus = React.useCallback((status) => {
22
+ setConnectionStatus(status);
23
+ onConnectionStatusChange?.(status);
24
+ }, [onConnectionStatusChange]);
25
+ let headers = {};
26
+ let protocols = [];
27
+ if (authToken) {
28
+ headers["Authorization"] = `Bearer ${authToken}`;
29
+ protocols.push("bearer", authToken);
30
+ }
31
+ const instanceName = userId ?? chatId;
32
+ const subChain = React.useMemo(() => subAgentName ? [{
33
+ agent: subAgentName,
34
+ name: chatId
35
+ }] : [], [subAgentName, chatId]);
36
+ const messagesHttpBaseUrl = React.useMemo(() => buildAgentHttpBaseUrl({
37
+ host,
38
+ agent: agentName,
39
+ name: instanceName,
40
+ sub: subChain.length > 0 ? subChain : void 0
41
+ }), [
42
+ host,
43
+ agentName,
44
+ instanceName,
45
+ subChain
46
+ ]);
47
+ const agent = useAgent({
48
+ agent: agentName,
49
+ host,
50
+ basePath,
51
+ name: instanceName,
52
+ sub: subChain.length > 0 ? subChain : void 0,
53
+ query: connectionParams ?? {},
54
+ queryDeps: Object.keys(connectionParams ?? {}),
55
+ onStateUpdate(state) {
56
+ if (state.status === "connected") {
57
+ updateConnectionStatus("connected");
58
+ onOpenProp?.();
59
+ }
60
+ },
61
+ onClose: (event) => {
62
+ if (event.code >= 3e3) {
63
+ updateConnectionStatus("unauthorized");
64
+ agent.close();
65
+ onCloseProp?.(event);
66
+ return;
67
+ }
68
+ updateConnectionStatus("disconnected");
69
+ onCloseProp?.(event);
70
+ },
71
+ onError: onErrorProp,
72
+ protocols
73
+ });
74
+ const fetchInitialMessages = React.useCallback(async (_options) => {
75
+ const url = `${messagesHttpBaseUrl}/get-messages`;
76
+ const response = await fetch(url, { headers });
77
+ if (!response.ok) {
78
+ console.warn(`[useAIChatAgent] Failed to fetch initial messages: ${response.status} ${response.statusText}`);
79
+ return [];
80
+ }
81
+ const text = await response.text();
82
+ if (!text.trim()) return [];
83
+ try {
84
+ return JSON.parse(text);
85
+ } catch (error) {
86
+ console.warn("[useAIChatAgent] Failed to parse initial messages JSON:", error);
87
+ return [];
88
+ }
89
+ }, [headers, messagesHttpBaseUrl]);
90
+ const chat = useAgentChat({
91
+ agent,
92
+ body: toolContext ?? {},
93
+ headers,
94
+ getInitialMessages: fetchInitialMessages
95
+ });
96
+ const hasSentWelcome = React.useRef(false);
97
+ React.useEffect(() => {
98
+ if (welcomeMessage && connectionStatus === "connected" && chat.messages.length === 0 && !hasSentWelcome.current) {
99
+ hasSentWelcome.current = true;
100
+ chat.setMessages([{
101
+ id: "welcome-message",
102
+ role: "assistant",
103
+ parts: [{
104
+ type: "text",
105
+ text: welcomeMessage
106
+ }]
107
+ }]);
108
+ }
109
+ }, [
110
+ connectionStatus,
111
+ chat.messages.length,
112
+ welcomeMessage
113
+ ]);
114
+ return {
115
+ agent,
116
+ chat,
117
+ connectionStatus
118
+ };
119
+ }
120
+ //#endregion
121
+ export { useAIChatAgent };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@economic/agents-react",
3
- "version": "1.1.9",
3
+ "version": "1.2.0",
4
4
  "license": "MIT",
5
5
  "files": [
6
6
  "dist"
@@ -9,6 +9,7 @@
9
9
  "types": "./dist/index.d.mts",
10
10
  "exports": {
11
11
  ".": "./dist/index.mjs",
12
+ "./v1": "./dist/v1.mjs",
12
13
  "./package.json": "./package.json"
13
14
  },
14
15
  "scripts": {