@economic/agents 2.1.5 → 2.1.7

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.
Files changed (3) hide show
  1. package/README.md +244 -434
  2. package/dist/v2.mjs +0 -4
  3. package/package.json +4 -7
package/README.md CHANGED
@@ -1,584 +1,394 @@
1
1
  # @economic/agents
2
2
 
3
- A batteries-included toolkit for building AI agents on Cloudflare Workers. Provides Durable Object base classes for both chat and non-chat agents, with on-demand skill loading, automatic message compaction, conversation management, and audit logging to D1.
3
+ Our agents SDK for building AI agents on Cloudflare Workers. Each agent is a Durable Object running an LLM loop model, system prompt, tools, skills, auth, telemetry on [`@cloudflare/think`](https://www.npmjs.com/package/@cloudflare/think) and the [`ai`](https://www.npmjs.com/package/ai) SDK.
4
4
 
5
- For chat agents, extend `ChatAgentHarness` (recommended) or `ChatAgent` (lower-level). For headless agents, extend `Agent`.
5
+ React client: [`@economic/agents-react`](../react/README.md).
6
6
 
7
- For React integration, see [`@economic/agents-react`](../react/README.md).
7
+ > Covers **v2** (`@economic/agents/v2`). The v1 root API (`ChatAgentHarness`, `buildLLMParams`) is deprecated, kept only for migration, and removed in v3.
8
+
9
+ ## The three classes
10
+
11
+ - **`Agent`** — the core. Runs the agent loop and keeps message history. Drive it over a WebSocket or programmatically (schedule, alarm, RPC). Most agents stop here.
12
+ - **`ChatAgent`** — `Agent` plus conversation features: compaction and message feedback.
13
+ - **`Assistant`** — per-user shell over `ChatAgent`: create/list/delete conversations, titles, summaries, retention.
14
+
15
+ ```
16
+ Agent ← LLM + tools + skills (most agents stop here)
17
+ └─ ChatAgent ← one persistent conversation
18
+ └─ Assistant ← one user, many conversations
19
+ ```
8
20
 
9
21
  ## Install
10
22
 
11
23
  ```sh
12
- npm install @economic/agents @cloudflare/ai-chat ai agents
24
+ npm install @economic/agents ai
13
25
  ```
14
26
 
15
- ## Quick Start
27
+ `jose` is an optional peer dependency, needed only for JWT auth.
28
+
29
+ ## Quick start: an agent
16
30
 
17
- ### Server
31
+ Subclass `Agent` and implement `getModel` and `getSystemPrompt`. Add tools with `getTools`, skills with `getSkills`. Expose a `@callable` method to run a turn — `saveMessages` injects a message, runs the turn, and persists the result:
18
32
 
19
33
  ```typescript
20
34
  import { openai } from "@ai-sdk/openai";
21
- import { tool } from "ai";
35
+ import { callable } from "agents";
36
+ import { Agent, tool, type ToolSet } from "@economic/agents/v2";
22
37
  import { z } from "zod";
23
- import { ChatAgentHarness, type AgentToolContext, type Skill } from "@economic/agents";
24
-
25
- const searchSkill: Skill = {
26
- name: "search",
27
- description: "Web search tools",
28
- guidance: "Use search_web for any queries requiring up-to-date information.",
29
- tools: {
30
- search_web: tool({
31
- description: "Search the web",
32
- inputSchema: z.object({ query: z.string() }),
33
- execute: async ({ query }) => `Results for: ${query}`,
34
- }),
35
- },
36
- };
37
38
 
38
- export class MyAgent extends ChatAgentHarness<Env> {
39
- getModel(ctx: AgentToolContext) {
39
+ export class SupportAgent extends Agent {
40
+ getModel() {
40
41
  return openai("gpt-4o");
41
42
  }
42
43
 
43
- getFastModel() {
44
- return openai("gpt-4o-mini");
44
+ getSystemPrompt() {
45
+ return "You help customers with their orders. Be concise.";
45
46
  }
46
47
 
47
- getSystemPrompt(ctx: AgentToolContext) {
48
- return "You are a helpful assistant.";
48
+ getTools(): ToolSet {
49
+ return {
50
+ get_order: tool({
51
+ description: "Look up an order by id",
52
+ inputSchema: z.object({ orderId: z.string() }),
53
+ execute: async ({ orderId }) => fetchOrder(orderId),
54
+ }),
55
+ };
49
56
  }
50
57
 
51
- getSkills(ctx: AgentToolContext) {
52
- return [searchSkill];
58
+ @callable()
59
+ async checkOrder(orderId: string) {
60
+ await this.saveMessages([
61
+ {
62
+ id: crypto.randomUUID(),
63
+ role: "user",
64
+ parts: [{ type: "text", text: `What's the status of order ${orderId}?` }],
65
+ },
66
+ ]);
53
67
  }
54
68
  }
55
69
  ```
56
70
 
57
- For lower-level control (custom `onChatMessage` implementations), extend `ChatAgent` directly — see [ChatAgent](#chatagent).
71
+ Route requests and export the class:
72
+
73
+ ```typescript
74
+ import { routeAgentRequest } from "@economic/agents";
75
+
76
+ export { SupportAgent } from "./agent";
58
77
 
59
- ### Wrangler Config
78
+ export default {
79
+ async fetch(request: Request, env: Env): Promise<Response> {
80
+ return (await routeAgentRequest(request, env)) ?? new Response("Not found", { status: 404 });
81
+ },
82
+ };
83
+ ```
60
84
 
61
85
  ```jsonc
86
+ // wrangler.jsonc — binding name must match the class name
62
87
  {
88
+ "compatibility_date": "2026-04-16",
89
+ "compatibility_flags": ["nodejs_compat", "experimental"],
63
90
  "durable_objects": {
64
- "bindings": [{ "name": "MyAgent", "class_name": "MyAgent" }],
91
+ "bindings": [{ "name": "SupportAgent", "class_name": "SupportAgent" }],
65
92
  },
66
- "migrations": [{ "tag": "v1", "new_sqlite_classes": ["MyAgent"] }],
93
+ "migrations": [{ "tag": "v1", "new_sqlite_classes": ["SupportAgent"] }],
94
+ // see "Bindings"
95
+ "r2_buckets": [{ "binding": "AGENTS_AUDIT_LOGS", "bucket_name": "agents-audit-logs" }],
96
+ "analytics_engine_datasets": [{ "binding": "AGENTS_ANALYTICS", "dataset": "agents-analytics" }],
67
97
  }
68
98
  ```
69
99
 
70
- Run `wrangler types` after to generate typed `Env` bindings.
71
-
72
- ### Client
100
+ Run `wrangler types` for a typed `Env`.
73
101
 
74
- ```typescript
75
- import { useAIChatAgent, type AgentConnectionStatus } from "@economic/agents-react";
76
- import { useState } from "react";
102
+ ### Calling it from the client
77
103
 
78
- const [connectionStatus, setConnectionStatus] = useState<AgentConnectionStatus>("connecting");
104
+ Connect with [`useAgent`](../react/README.md#useagent) and invoke any `@callable` method with `agent.call`:
79
105
 
80
- const { agent, chat } = useAIChatAgent({
81
- agent: "MyAgent",
106
+ ```tsx
107
+ const agent = useAgent({
82
108
  host: "localhost:8787",
83
- chatId: "user_123:session-1",
84
- toolContext: {},
85
- connectionParams: { userId: "…" },
86
- onConnectionStatusChange: setConnectionStatus,
109
+ agentName: "SupportAgent",
110
+ name: `${userId}:support`,
87
111
  });
88
112
 
89
- const { messages, sendMessage, status, stop } = chat;
113
+ await agent.call("checkOrder", ["1234"]);
90
114
  ```
91
115
 
92
- `chatId` is the Durable Object nameuse `userId:uniqueChatId` (see [Providing userId](#providing-userid)).
116
+ You can also drive turns server-sidecall `saveMessages` from a schedule or alarm.
93
117
 
94
- > **Note:** React hooks are in a separate package. Install with `npm install @economic/agents-react`.
118
+ ## A chat agent
95
119
 
96
- ---
97
-
98
- ## Harnesses
99
-
100
- The server-side API is built around Durable Object base classes — `Agent` for headless workflows and `ChatAgent`/`ChatAgentHarness` for conversational UIs — plus a skill system that lets the LLM load tools on demand.
101
-
102
- ### ChatAgentHarness
103
-
104
- The recommended starting point for chat agents. Extends `ChatAgent` with an opinionated structure: implement abstract methods for model selection, system prompt, tools, and skills. The harness handles `onChatMessage` for you.
120
+ For a chat UI, extend `ChatAgent` instead of `Agent`. It adds compaction and message feedback, and connects to a browser with [`useChat`](../react/README.md#usechat) — no `Assistant` required. The DO name is the conversation; address one per user, ticket, or whatever fits.
105
121
 
106
122
  ```typescript
107
123
  import { openai } from "@ai-sdk/openai";
108
- import { ChatAgentHarness, type AgentToolContext, type Skill } from "@economic/agents";
109
-
110
- interface RequestBody {
111
- userTier: "free" | "pro";
112
- }
113
-
114
- export class MyAgent extends ChatAgentHarness<Env, RequestBody> {
115
- getModel(ctx: AgentToolContext<RequestBody>) {
116
- return ctx.userTier === "pro" ? openai("gpt-4o") : openai("gpt-4o-mini");
117
- }
124
+ import { ChatAgent } from "@economic/agents/v2";
125
+ import { weatherSkill } from "./skills/weather";
118
126
 
119
- getFastModel() {
120
- return openai("gpt-4o-mini");
127
+ export class MyChatAgent extends ChatAgent {
128
+ getModel() {
129
+ return openai("gpt-4o");
121
130
  }
122
131
 
123
- getSystemPrompt(ctx: AgentToolContext<RequestBody>) {
132
+ getSystemPrompt() {
124
133
  return "You are a helpful assistant.";
125
134
  }
126
135
 
127
- getTools(ctx: AgentToolContext<RequestBody>) {
128
- return { myTool };
129
- }
130
-
131
- getSkills(ctx: AgentToolContext<RequestBody>) {
132
- return [searchSkill, calculatorSkill];
136
+ getSkills() {
137
+ return [weatherSkill];
133
138
  }
134
139
  }
135
140
  ```
136
141
 
137
- - `getModel(ctx)` — returns the primary language model. Context includes request body for tier-based model selection.
138
- - `getFastModel()` returns a fast/cheap model for compaction and conversation summarization.
139
- - `getSystemPrompt(ctx)` — returns the system prompt.
140
- - `getTools(ctx)` — returns always-on tools (optional, defaults to `{}`).
141
- - `getSkills(ctx)` — returns skills available for on-demand loading (optional, defaults to `[]`).
142
- - `conversationRetentionDays` — defaults to 90. Set to `undefined` to disable auto-deletion.
143
-
144
- #### Binding Name
145
-
146
- `ChatAgentHarness` automatically derives the Durable Object binding name from the class name. **The binding name in your `wrangler.jsonc` must exactly match your class name:**
147
-
148
- ```typescript
149
- // Class name is "MyAgent"
150
- export class MyAgent extends ChatAgentHarness<Env> {
151
- /* ... */
152
- }
142
+ ```tsx
143
+ const { chat } = useChat({
144
+ host: "localhost:8787",
145
+ agentName: "MyChatAgent",
146
+ name: `${userId}:weather`,
147
+ });
153
148
  ```
154
149
 
155
- ```jsonc
156
- // wrangler.jsonc — binding name must be "MyAgent" to match
157
- {
158
- "durable_objects": {
159
- "bindings": [{ "name": "MyAgent", "class_name": "MyAgent" }],
160
- },
161
- }
162
- ```
150
+ What `ChatAgent` adds over `Agent`:
163
151
 
164
- If the names don't match, the harness won't be able to resolve the binding and will throw at runtime. If you need a different binding name, override the `binding` getter:
152
+ - **Compaction** past 100,000 tokens, older messages are summarised (with `getModel()`) while recent ones are kept verbatim. Storage keeps the full history.
153
+ - **Message feedback** — thumbs up/down with an optional comment, in the conversation's SQLite (`assistant_messages_feedback`, created automatically):
165
154
 
166
- ````typescript
167
- export class MyAgent extends ChatAgentHarness<Env> {
168
- protected get binding() {
169
- return this.env.CUSTOM_BINDING_NAME;
170
- }
171
- // ...
172
- }
155
+ | Method | Description |
156
+ | ---------------------------------------------------- | ------------------------------------------------------------ |
157
+ | `submitMessageFeedback(messageId, rating, comment?)` | `rating` is `1` (up) or `-1` (down). Upserts on the message. |
158
+ | `getMessageFeedback()` | All feedback for the conversation, keyed by message id. |
173
159
 
174
- ---
160
+ Surfaced by the client as `chat.submitMessageFeedback` / `chat.getMessageFeedback`.
175
161
 
176
- ### ChatAgent
162
+ ## Many chats per user: Assistant
177
163
 
178
- Lower-level base class for chat agents. Use when you need full control over `onChatMessage` custom streaming, multiple LLM calls per turn, or non-standard response formats.
164
+ When one user needs a list of conversations a sidebar, titles, summaries — add an `Assistant`. It owns the chat list and routes to a `ChatAgent` per conversation.
179
165
 
180
166
  ```typescript
181
- import { streamText } from "ai";
182
167
  import { openai } from "@ai-sdk/openai";
183
- import { ChatAgent } from "@economic/agents";
168
+ import { Assistant } from "@economic/agents/v2";
169
+ import { MyChatAgent } from "./chat-agent";
184
170
 
185
- export class MyAgent extends ChatAgent<Env> {
186
- protected get binding() {
187
- return this.env.MyAgent;
188
- }
189
-
190
- protected getFastModel() {
191
- return openai("gpt-4o-mini");
192
- }
193
-
194
- async onChatMessage(onFinish, options) {
195
- const params = await this.buildLLMParams({
196
- options,
197
- onFinish,
198
- model: openai("gpt-4o"),
199
- system: "You are a helpful assistant.",
200
- skills: [searchSkill],
201
- tools: { alwaysOnTool },
202
- });
203
- return streamText(params).toUIMessageStreamResponse();
204
- }
171
+ export class MyAssistant extends Assistant {
172
+ protected agent = MyChatAgent;
173
+ protected fastModel = openai("gpt-4o-mini"); // titles and summaries
205
174
  }
206
- ````
175
+ ```
207
176
 
208
- - `binding` abstract getter returning the DO namespace binding. Required on every subclass.
209
- - `getFastModel()` — abstract method returning the fast model for compaction and summarization.
210
- - `maxMessagesBeforeCompaction` — class property to override the default threshold (15). Set to `undefined` to disable.
211
- - `conversationRetentionDays` — class property to auto-delete inactive conversations after N days.
212
- - `this.buildLLMParams()` — pre-fills `messages`, `activeSkills`, and injects `logEvent` into `experimental_context`.
213
- - `getConversations()` / `deleteConversation(id)` — callable methods for listing/deleting a user's conversations.
177
+ Bind both classes (binding name = class name) with a `new_sqlite_classes` migration each. On the client, use [`useAssistant`](../react/README.md#useassistant) it manages the chat list and connects to the active conversation:
214
178
 
215
- ---
179
+ ```tsx
180
+ const { status, chats, assistant, chat } = useAssistant({
181
+ host: "localhost:8787",
182
+ agentName: "MyAssistant",
183
+ name: userId, // the Assistant is keyed by user
184
+ });
185
+ ```
216
186
 
217
- ### Agent
187
+ The `Assistant` keeps the chat list in its own SQLite (a `chats` table, created automatically) and exposes three callable methods:
218
188
 
219
- Abstract Durable Object base for non-chat agents. Use for headless workflows driven from HTTP handlers, schedules, or alarms.
189
+ | Method | Returns | Description |
190
+ | ---------------- | -------- | ------------------------------------------------------ |
191
+ | `createChat()` | `string` | Creates a conversation and returns its id. |
192
+ | `getChats()` | `Chat[]` | The user's conversations, most recently updated first. |
193
+ | `deleteChat(id)` | `void` | Deletes a conversation and its record. |
220
194
 
221
- ```typescript
222
- import { generateText } from "ai";
223
- import { openai } from "@ai-sdk/openai";
224
- import { callable } from "agents";
225
- import { Agent } from "@economic/agents";
226
-
227
- export class MyAgent extends Agent<Env> {
228
- @callable
229
- async summarize(document: string) {
230
- const params = await this.buildLLMParams({
231
- model: openai("gpt-4o"),
232
- messages: [{ role: "user", content: `Summarise: ${document}` }],
233
- system: "You are a helpful assistant.",
234
- skills: [searchSkill],
235
- });
236
- const result = await generateText(params);
237
- return result.text;
238
- }
239
- }
240
- ```
195
+ - **Titles and summaries** — generated with `fastModel` after each turn (on the first turn, then refreshed as the conversation grows).
196
+ - **Retention** inactive conversations are deleted after 90 days, via a Durable Object alarm.
241
197
 
242
- - `this.buildLLMParams()` pre-fills `activeSkills` from DO SQLite and injects `logEvent` into `experimental_context`.
243
- - `this.logEvent(message, payload?)` writes audit events to D1 when `AGENT_DB` is bound, silent no-op otherwise.
198
+ ## Bindings
244
199
 
245
- ---
200
+ Checked in `Agent.onStart` when a connection opens:
246
201
 
247
- ### Tool context
202
+ | Binding | Type | Required | Notes |
203
+ | ------------------- | ---------------- | -------- | ------------------------------------------------------------------------- |
204
+ | `AGENTS_AUDIT_LOGS` | R2 | Yes | Connection rejected if missing. One [audit log](#observability) per turn. |
205
+ | `AGENTS_ANALYTICS` | Analytics Engine | No | Per-turn/per-tool [analytics](#observability). Warns if missing. |
206
+ | `SKILLS_BUCKET` | R2 | No | Source for [remote skills](#remote-skills). |
207
+
208
+ ## Tools
248
209
 
249
- Pass data via the `body` option of `useAgentChat` (with `useAIChatAgent`, use `toolContext` it is forwarded as `body`). It arrives as `experimental_context` in tool `execute` functions. Use `AgentToolContext<TBody>` to type it:
210
+ `tool()` wraps an `ai` SDK tool with an optional `authorize(ctx)`. Return always-on tools from `getTools()`:
250
211
 
251
212
  ```typescript
252
- import type { AgentToolContext } from "@economic/agents";
213
+ import { tool, type ToolSet } from "@economic/agents/v2";
214
+ import { z } from "zod";
253
215
 
254
- interface AgentBody {
255
- authorization: string;
256
- userId: string;
216
+ getTools(): ToolSet {
217
+ return {
218
+ search_web: tool({
219
+ description: "Search the web",
220
+ inputSchema: z.object({ query: z.string() }),
221
+ execute: async ({ query }) => search(query),
222
+ authorize: (ctx) => ctx._userContext?.canSearch !== false,
223
+ }),
224
+ };
257
225
  }
258
-
259
- type ToolContext = AgentToolContext<AgentBody>;
260
-
261
- // Tool
262
- execute: async (args, { experimental_context }) => {
263
- const ctx = experimental_context as ToolContext;
264
- await ctx.logEvent("tool called", { userId: ctx.userId });
265
- return await fetchSomething(ctx.authorization);
266
- };
267
226
  ```
268
227
 
269
- `logEvent` is a no-op when `AGENT_DB` is not bound.
228
+ `authorize` returning `false` hides the tool for that request.
270
229
 
271
- ---
272
-
273
- ### JWT Authentication
230
+ ### Tool context
274
231
 
275
- Authenticate WebSocket connections by implementing `getJwtAuthConfig` on your agent. When defined, JWT verification runs in `onConnect` — failed auth closes the connection, successful auth stores claims in `session`.
232
+ The tool context is the second argument to `execute` — `experimental_context`:
276
233
 
277
234
  ```typescript
278
- import type { JWTPayload } from "jose";
279
- import { ChatAgentHarness, type AgentToolContext } from "@economic/agents";
235
+ import { tool, type ToolContext } from "@economic/agents/v2";
236
+ import { z } from "zod";
280
237
 
281
- interface Session {
282
- clientId: string;
283
- userGuid: string;
284
- agreementNumber: number;
238
+ interface RequestBody {
239
+ tokens: Record<string, string>;
285
240
  }
286
241
 
287
- export class MyAgent extends ChatAgentHarness<Env, RequestBody> {
288
- getJwtAuthConfig(request: Request) {
289
- const origin = request.headers.get("Origin") ?? "";
290
- const isStaging = origin.includes("staging");
291
-
292
- return {
293
- allowedIssuers: isStaging
294
- ? [/^https:\/\/auth\.staging\.example\.com$/]
295
- : ["https://auth.example.com"],
296
- audience: "my-api",
297
- requiredScopes: ["read"],
298
- getClaims: (payload: JWTPayload): Session => ({
299
- clientId: payload.client_id as string,
300
- userGuid: payload.user_guid as string,
301
- agreementNumber: payload.agreement_number as number,
302
- }),
303
- };
304
- }
305
-
306
- // Session is available in tool context
307
- getModel(ctx: AgentToolContext<RequestBody>) {
308
- console.log(ctx.session); // { clientId, userGuid, agreementNumber }
309
- return openai("gpt-4o");
310
- }
311
- }
242
+ const call_api = tool({
243
+ description: "Call the API for the user",
244
+ inputSchema: z.object({ path: z.string() }),
245
+ execute: async ({ path }, { experimental_context }) => {
246
+ const ctx = experimental_context as ToolContext<RequestBody>;
247
+ return fetchWithAuth(ctx.tokens, path);
248
+ },
249
+ });
312
250
  ```
313
251
 
314
- - `allowedIssuers` array of strings or RegExp patterns for trusted issuers
315
- - `audience` — expected `aud` claim
316
- - `requiredScopes` — optional array of required OAuth scopes
317
- - `getClaims(payload)` — extract claims from verified JWT payload
318
-
319
- Claims are available as `ctx.session` in `getModel`, `getSystemPrompt`, `getTools`, `getSkills`, and tool `execute` functions.
320
-
321
- If `getJwtAuthConfig` is not implemented, no authentication is performed and `ctx.session` is `undefined`.
252
+ `ToolContext<RequestContext, UserContext>` is `RequestContext & { _userContext?: UserContext }`. Request context comes from the client (`toolContext` in the hooks); `_userContext` comes from [`getUserContext`](#user-context) when using JWT Authentication (via `getJwtAuthConfig`).
322
253
 
323
- ---
254
+ ## Skills
324
255
 
325
- ### Source URLs from Tools
326
-
327
- Any tool can surface source URLs into the message stream by including a `sources` array in its return value. Detected automatically by `buildLLMParams` — no additional wiring needed.
256
+ A skill bundles markdown `instructions` with optional tools. Only `description` sits in the system prompt; the model loads the instructions and tools on demand via the built-in `load_context` tool.
328
257
 
329
258
  ```typescript
330
- execute: async ({ query }) => {
331
- const data = await fetchResults(query);
332
- return {
333
- results: data.results,
334
- sources: data.results.map((r) => ({ url: r.url, title: r.title })),
335
- };
336
- };
337
- ```
338
-
339
- Each source entry: `{ url: string, title?: string }`.
340
-
341
- ---
259
+ import { skill, tool } from "@economic/agents/v2";
260
+ import { z } from "zod";
342
261
 
343
- ### Skills
262
+ export const weatherSkill = skill({
263
+ name: "weather",
264
+ description: "Look up current weather and forecasts. Use for any weather question.",
265
+ instructions: `
266
+ # Weather
344
267
 
345
- Named groups of tools loaded on demand by the LLM. The agent starts with only always-on tools active. When the LLM needs more, it calls `activate_skill`.
268
+ ## When to use
269
+ Use this skill whenever the user asks about current conditions or a forecast.
346
270
 
347
- ```typescript
348
- import { tool } from "ai";
349
- import { z } from "zod";
350
- import type { Skill } from "@economic/agents";
351
-
352
- export const calculatorSkill: Skill = {
353
- name: "calculator",
354
- description: "Mathematical calculation and expression evaluation",
355
- guidance:
356
- "Use the calculate tool for any arithmetic or algebraic expressions. " +
357
- "Always show the expression you are evaluating.",
271
+ ## Workflow
272
+ 1. Resolve the location to coordinates if needed.
273
+ 2. Call \`get_forecast\` with the coordinates.
274
+ 3. Summarise the result; never invent values.
275
+ `,
358
276
  tools: {
359
- calculate: tool({
360
- description: "Evaluate a mathematical expression",
361
- inputSchema: z.object({
362
- expression: z.string().describe('e.g. "2 + 2", "Math.sqrt(144)"'),
363
- }),
364
- execute: async ({ expression }) => {
365
- const result = new Function(`"use strict"; return (${expression})`)();
366
- return `${expression} = ${result}`;
367
- },
277
+ get_forecast: tool({
278
+ description: "Get the forecast for a set of coordinates",
279
+ inputSchema: z.object({ lat: z.number(), lon: z.number() }),
280
+ execute: async ({ lat, lon }) => fetchForecast(lat, lon),
368
281
  }),
369
282
  },
370
- };
371
- ```
372
-
373
- When `skills` are provided to `buildLLMParams`, two meta-tools are registered automatically:
374
-
375
- - **`activate_skill`** — loads skills by name, making their tools available for the rest of the conversation. Idempotent. State is persisted to DO SQLite.
376
- - **`list_capabilities`** — returns active tools, loaded skills, and skills available to load.
377
-
378
- The `activate_skill` and `list_capabilities` meta-tools are stripped from message history before persistence.
379
-
380
- ---
381
-
382
- ### Audit Logging (D1)
383
-
384
- All agent base classes write audit events to a D1 database when `AGENT_DB` is bound. If not bound, `logEvent` is a no-op.
385
-
386
- #### D1 Setup
387
-
388
- 1. Create a D1 database in the Cloudflare dashboard.
389
- 2. Run the schema in the D1 console. For `Agent`, use [`schema/agent.sql`](schema/agent.sql). For `ChatAgent`/`ChatAgentHarness`, use [`schema/chat.sql`](schema/chat.sql) (includes the conversations table).
390
- 3. Bind it in `wrangler.jsonc`:
391
-
392
- ```jsonc
393
- "d1_databases": [
394
- { "binding": "AGENT_DB", "database_name": "agents", "database_id": "YOUR_DB_ID" }
395
- ]
396
- ```
397
-
398
- 4. For local dev, apply the schema to your local D1 (from your app’s directory), e.g. `wrangler d1 execute <database_name> --local --file=node_modules/@economic/agents/schema/chat.sql`. You can wrap that in a `db:setup` npm script if you prefer.
399
-
400
- #### Providing userId
401
-
402
- The client's `chatId` becomes the Durable Object name. Use `userId:uniqueChatId` so the first segment is your stable user id (audit and conversations key off `getUserId()`, i.e. the substring before the first `:`). If that segment is empty (e.g. `:chat-1`), the connection is rejected. Same idea as [Quick Start](#quick-start) (`chatId`).
403
-
404
- ```typescript
405
- import { useAIChatAgent } from "@economic/agents-react";
406
-
407
- const { agent, chat } = useAIChatAgent({
408
- agent: "MyAgent",
409
- host: "localhost:8787",
410
- chatId: "148583_matt:conversation-1",
411
283
  });
412
284
  ```
413
285
 
414
- ---
415
-
416
- ### Chat Features
286
+ Return skills from `getSkills()`. Add `authorize(ctx)` to gate a skill (and its tools). `skill()` throws if `description` or `instructions` is missing.
417
287
 
418
- Compaction and the conversation list (below) require `getFastModel()` on your subclass.
288
+ ### Remote skills
419
289
 
420
- #### Compaction
421
-
422
- Compaction summarises older messages before each turn. Full history in DO SQLite is unaffected — compaction is in-memory only. The default threshold is **15** recent messages (`maxMessagesBeforeCompaction` on the class).
290
+ Store skills in R2 to edit without redeploying and share across agents. Return keys from `getRemoteSkills()`:
423
291
 
424
292
  ```typescript
425
- export class MyAgent extends ChatAgentHarness<Env> {
426
- getModel() {
427
- return openai("gpt-4o");
428
- }
429
-
430
- getFastModel() {
431
- return openai("gpt-4o-mini");
432
- }
433
-
434
- getSystemPrompt() {
435
- return "You are a helpful assistant.";
436
- }
437
-
438
- // Optional: keep more messages verbatim before summarising (default 15).
439
- // protected maxMessagesBeforeCompaction = 50;
440
-
441
- // Optional: disable compaction (still uses fastModel for conversation title/summary).
442
- // protected maxMessagesBeforeCompaction = undefined;
293
+ protected getRemoteSkills() {
294
+ return ["billing.md", "tax.md"];
443
295
  }
444
296
  ```
445
297
 
446
- #### Conversations (D1)
298
+ Loaded from `SKILLS_BUCKET` under the `skills/` prefix — the object's `description` metadata shows up front, the body loads on demand. Skipped (with a logged error) if `SKILLS_BUCKET` isn't bound.
447
299
 
448
- `ChatAgent` and `ChatAgentHarness` maintain a `conversations` table in `AGENT_DB`. One row per Durable Object instance, upserted automatically after every turn. Requires [`schema/chat.sql`](schema/chat.sql).
300
+ ## Authentication
449
301
 
450
- **Automatic title and summary** — On the first turn, title and summary are generated and inserted. On subsequent turns, only `updated_at` is refreshed. Title and summary are regenerated periodically as the conversation grows.
302
+ ### JWT
451
303
 
452
- **Retention** Set `conversationRetentionDays` to auto-delete inactive conversations:
304
+ Override the static `getJwtAuthConfig(env)` to verify a JWT on connect (static so an `Assistant` can check before routing). Return `undefined` to skip.
453
305
 
454
306
  ```typescript
455
- export class MyAgent extends ChatAgentHarness<Env> {
456
- getModel() {
457
- return openai("gpt-4o");
458
- }
459
- getFastModel() {
460
- return openai("gpt-4o-mini");
461
- }
462
- getSystemPrompt() {
463
- return "You are a helpful assistant.";
307
+ export class SupportAgent extends Agent {
308
+ static getJwtAuthConfig(env: Cloudflare.Env) {
309
+ return {
310
+ allowedIssuers: [env.IDENTITY_ENDPOINT], // strings or RegExp
311
+ audience: "my-api",
312
+ requiredScopes: ["support.read"],
313
+ getClaims: (payload) => ({
314
+ userGuid: payload.user_guid as string,
315
+ agreementNumber: payload.agreement_number as number,
316
+ }),
317
+ };
464
318
  }
465
319
 
466
- // ChatAgentHarness defaults to 90 days. Override or set to undefined to disable.
467
- protected conversationRetentionDays = 30;
320
+ // ...
468
321
  }
469
322
  ```
470
323
 
471
- When the retention period expires, the D1 row is deleted, WebSocket connections are closed, and the DO's SQLite storage is wiped.
324
+ On failure the socket closes (`4001` unauthorized, `4003` forbidden) and status becomes `"unauthorized"`. The token is read from `Authorization: Bearer …`, then the `Sec-WebSocket-Protocol: bearer, <token>` header.
472
325
 
473
- **Querying** From a connected client:
326
+ ### User context
474
327
 
475
- ```typescript
476
- const conversations = await agent.call("getConversations");
477
- ```
478
-
479
- #### Message Ratings (D1)
480
-
481
- Users can rate individual messages with thumbs up/down. Ratings are stored in D1 and can be updated if the user changes their mind.
328
+ Implement `getUserContext(jwtToken)` to load per-user data after auth. It's exposed as `ctx._userContext` in `getModel`, `getSystemPrompt`, tools, and skill `authorize`. Runs only when JWT auth is configured.
482
329
 
483
330
  ```typescript
484
- // Rate a message (1 = thumbs up, -1 = thumbs down)
485
- await agent.call("rateMessage", [messageId, 1]);
486
-
487
- // Change rating
488
- await agent.call("rateMessage", [messageId, -1]);
489
-
490
- // Get all ratings for the current conversation
491
- const ratings = await agent.call("getMessageRatings");
492
- // Returns: { "message_id_1": 1, "message_id_2": -1, ... }
331
+ protected async getUserContext(jwtToken: string) {
332
+ const profile = await fetchProfile(jwtToken);
333
+ return { role: profile.role, tokens: profile.tokens };
334
+ }
493
335
  ```
494
336
 
495
- Ratings are stored per message and upserted on conflict — calling `rateMessage` on an already-rated message updates the rating. Requires [`schema/chat.sql`](schema/chat.sql) which includes the `message_ratings` table.
496
-
497
- ---
337
+ ## Observability
498
338
 
499
- ## API Reference
339
+ Emitted per turn from the `ai` SDK's OpenTelemetry spans, no setup beyond bindings:
500
340
 
501
- ### `@economic/agents`
341
+ - **Audit logs** → one JSON object per turn in `AGENTS_AUDIT_LOGS` (model, actor, IP, prompt, response, tool calls).
342
+ - **Analytics** → per-turn and per-tool data points in `AGENTS_ANALYTICS`.
502
343
 
503
- | Export | Description |
504
- | ---------------------- | -------------------------------------------------------------------------- |
505
- | `Agent` | Abstract DO base for non-chat agents with audit logging and buildLLMParams |
506
- | `ChatAgent` | Abstract chat DO with compaction, conversations, and custom onChatMessage |
507
- | `ChatAgentHarness` | Opinionated chat harness with getModel/getSystemPrompt/getTools/getSkills |
508
- | `buildLLMParams` | Standalone function to build streamText/generateText params |
509
- | `Skill` | Type: named group of tools with optional guidance |
510
- | `AgentToolContext` | Type: request body merged with `session` and `logEvent` for tool context |
511
- | `OnChatMessageOptions` | Type: options passed to `onChatMessage` |
512
- | `BuildLLMParamsConfig` | Type: config for standalone `buildLLMParams` |
344
+ ## API reference
513
345
 
514
- ### `@economic/agents-react`
346
+ Imported from `@economic/agents/v2` unless noted.
515
347
 
516
- React hooks are in a separate package. See [`@economic/agents-react`](../react/README.md) for full documentation.
348
+ ### Classes
517
349
 
518
- | Export | Description |
519
- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------- |
520
- | `useAIChatAgent` | React hook wrapping `useAgent` + `useAgentChat` |
521
- | `UseAIChatAgentOptions` | Type: options for `useAIChatAgent` (`agent`, `host`, `chatId`, optional `basePath`, `toolContext`, `connectionParams`, …) |
522
- | `AgentConnectionStatus` | Type: `"connecting" \| "connected" \| "disconnected" \| "unauthorized"` |
350
+ | Export | Description |
351
+ | ----------- | -------------------------------------------------------- |
352
+ | `Agent` | Core agent base. Implement `getModel`/`getSystemPrompt`. |
353
+ | `ChatAgent` | `Agent` + compaction and message feedback. |
354
+ | `Assistant` | Per-user manager of `ChatAgent` conversations. |
523
355
 
524
- ### CLI
356
+ ### Helpers and types
525
357
 
526
- | Command | Description |
527
- | -------------------------------------------- | ---------------------------------------- |
528
- | `npx @economic/agents generate skill <name>` | Scaffold a new skill with tools |
529
- | `npx @economic/agents generate tool <name>` | Scaffold a new tool (global or in skill) |
358
+ | Export | Description |
359
+ | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------- |
360
+ | `tool(def)` / `Tool` / `ToolSet` | Define a tool with an optional `authorize(ctx)` guard. |
361
+ | `skill(def)` / `Skill` | Define a skill (`name`, `description`, `instructions`, `tools?`, `authorize?`). |
362
+ | `ToolContext` | `RequestContext & { _userContext?: UserContext }`. |
363
+ | `AgentEnv` | The bindings the runtime expects. |
364
+ | `AgentConnectionState` / `AgentConnectionStatus` / `AgentConnectionType` | Connection state shared with the client. |
530
365
 
531
- ---
532
-
533
- ## CLI
534
-
535
- The package includes a CLI for scaffolding skills and tools.
536
-
537
- ### Generate a Skill
538
-
539
- ```bash
540
- npx @economic/agents generate skill weather
541
- ```
366
+ ### Members
542
367
 
543
- This will:
368
+ | Member | On | Description |
369
+ | ---------------------------------------------- | ----------- | ------------------------------------------- |
370
+ | `getModel(ctx?)` | `Agent` | Required. Model for inference. |
371
+ | `getSystemPrompt(ctx?)` | `Agent` | Required. System prompt. |
372
+ | `getTools()` | `Agent` | Always-on tools (default `{}`). |
373
+ | `getSkills()` | `Agent` | Local skills (default `[]`). |
374
+ | `getRemoteSkills()` | `Agent` | R2 skill keys (default `[]`). |
375
+ | `static getJwtAuthConfig(env)` | `Agent` | Optional JWT verification config. |
376
+ | `getUserContext(jwtToken)` | `Agent` | Optional per-user context after auth. |
377
+ | `submitMessageFeedback` / `getMessageFeedback` | `ChatAgent` | Callable message feedback. |
378
+ | `agent` | `Assistant` | Required. Your `ChatAgent` subclass. |
379
+ | `fastModel` | `Assistant` | Required. Cheap model for titles/summaries. |
380
+ | `createChat` / `getChats` / `deleteChat` | `Assistant` | Callable conversation management. |
544
381
 
545
- 1. Prompt for a skill description
546
- 2. Ask for initial tool names (comma-separated)
547
- 3. Prompt for each tool's description and whether it needs `AgentToolContext`
548
- 4. Create the skill file at `src/skills/weather/weather.ts`
549
- 5. Create tool files at `src/skills/weather/tools/*.ts`
550
- 6. Auto-register the skill in your agent's `getSkills()` method
382
+ ### Package root (`@economic/agents`)
551
383
 
552
- ### Generate a Tool
553
-
554
- ```bash
555
- npx @economic/agents generate tool geocode
556
- ```
557
-
558
- This will:
559
-
560
- 1. Prompt for a tool description
561
- 2. Ask where to create it (global `src/tools/` or within an existing skill)
562
- 3. Ask whether it needs `AgentToolContext`
563
- 4. Create the tool file
564
- 5. Auto-register the tool in your agent's `getTools()` or the skill's `tools` object
565
-
566
- ### Auto-registration
567
-
568
- The CLI automatically detects agent files by scanning `src/` for classes extending `ChatAgentHarness`, `ChatAgent`, or `Agent` from `@economic/agents`. If one agent is found, it's used automatically. If multiple are found, you'll be prompted to select one.
569
-
570
- For `ChatAgentHarness`, the CLI modifies `getSkills()` or `getTools()` methods. For `ChatAgent` or `Agent`, it modifies `buildLLMParams()` calls.
571
-
572
- If the CLI detects complex patterns (spread operators, function calls, variables), it will print manual registration instructions instead.
573
-
574
- ---
384
+ | Export | Description |
385
+ | ------------------- | ------------------------------------------------------------------------ |
386
+ | `routeAgentRequest` | Routes a request to the right Durable Object; echoes the WS subprotocol. |
575
387
 
576
388
  ## Development
577
389
 
578
390
  ```bash
579
391
  npm install
580
392
  npm test
581
- npm pack
393
+ npm run build
582
394
  ```
583
-
584
- .
package/dist/v2.mjs CHANGED
@@ -86,10 +86,6 @@ var Agent = class extends Think {
86
86
  status: "connecting"
87
87
  });
88
88
  let hasCorrectBindings = true;
89
- if (!this.env.AGENT_DB) {
90
- hasCorrectBindings = false;
91
- console.error("[Agent] Connection rejected: no AGENT_DB bound");
92
- }
93
89
  if (!this.env.AGENTS_AUDIT_LOGS) {
94
90
  hasCorrectBindings = false;
95
91
  console.error("[Agent] Connection rejected: no AGENTS_AUDIT_LOGS bound. Audit logs are required.");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@economic/agents",
3
- "version": "2.1.5",
3
+ "version": "2.1.7",
4
4
  "description": "A starter for creating a TypeScript package.",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -27,16 +27,16 @@
27
27
  },
28
28
  "dependencies": {
29
29
  "@clack/prompts": "^1.2.0",
30
+ "@cloudflare/ai-chat": "^0.7.2",
31
+ "@cloudflare/think": "^0.7.3",
30
32
  "@opentelemetry/sdk-trace-base": "^2.7.1",
33
+ "agents": "^0.13.3",
31
34
  "nanoid": "^5.1.11"
32
35
  },
33
36
  "devDependencies": {
34
- "@cloudflare/ai-chat": "^0.7.2",
35
- "@cloudflare/think": "^0.7.3",
36
37
  "@cloudflare/workers-types": "^4.20260527.1",
37
38
  "@types/node": "^25.6.0",
38
39
  "@typescript/native-preview": "7.0.0-dev.20260412.1",
39
- "agents": "^0.13.3",
40
40
  "ai": "^6.0.175",
41
41
  "jose": "^6.2.2",
42
42
  "tsdown": "^0.22.0",
@@ -44,9 +44,6 @@
44
44
  "vitest": "^4.1.4"
45
45
  },
46
46
  "peerDependencies": {
47
- "@cloudflare/ai-chat": ">=0.7.2 <1.0.0",
48
- "@cloudflare/think": ">=0.7.3 <1.0.0",
49
- "agents": ">=0.13.3 <1.0.0",
50
47
  "ai": "^6.0.0",
51
48
  "jose": "^6.0.0"
52
49
  },