@economic/agents 2.1.6 → 2.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 +244 -432
- package/dist/index.d.mts +197 -219
- package/dist/index.mjs +574 -784
- package/dist/v1.d.mts +278 -0
- package/dist/v1.mjs +931 -0
- package/package.json +5 -8
- package/dist/v2.d.mts +0 -256
- package/dist/v2.mjs +0 -725
package/README.md
CHANGED
|
@@ -1,584 +1,396 @@
|
|
|
1
1
|
# @economic/agents
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
5
|
+
React client: [`@economic/agents-react`](../react/README.md).
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
> The v1 (`@economic/agents/v1`) 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
|
|
24
|
+
npm install @economic/agents ai
|
|
13
25
|
```
|
|
14
26
|
|
|
15
|
-
|
|
27
|
+
`jose` is an optional peer dependency, needed only for JWT auth.
|
|
28
|
+
|
|
29
|
+
## Quick start: an agent
|
|
16
30
|
|
|
17
|
-
|
|
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 {
|
|
35
|
+
import { callable } from "agents";
|
|
36
|
+
import { Agent, tool, type ToolSet } from "@economic/agents";
|
|
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
|
|
39
|
-
getModel(
|
|
39
|
+
export class SupportAgent extends Agent {
|
|
40
|
+
getModel() {
|
|
40
41
|
return openai("gpt-4o");
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
|
|
44
|
-
return
|
|
44
|
+
getSystemPrompt() {
|
|
45
|
+
return "You help customers with their orders. Be concise.";
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
|
|
48
|
-
return
|
|
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
|
-
|
|
52
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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": "
|
|
91
|
+
"bindings": [{ "name": "SupportAgent", "class_name": "SupportAgent" }],
|
|
65
92
|
},
|
|
66
|
-
"migrations": [{ "tag": "v1", "new_sqlite_classes": ["
|
|
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`
|
|
71
|
-
|
|
72
|
-
### Client
|
|
100
|
+
Run `wrangler types` for a typed `Env`.
|
|
73
101
|
|
|
74
|
-
|
|
75
|
-
import { useAIChatAgent, type AgentConnectionStatus } from "@economic/agents-react";
|
|
76
|
-
import { useState } from "react";
|
|
102
|
+
### Calling it from the client
|
|
77
103
|
|
|
78
|
-
|
|
104
|
+
Connect with [`useAgent`](../react/README.md#useagent) and invoke any `@callable` method with `agent.call`:
|
|
79
105
|
|
|
80
|
-
|
|
81
|
-
|
|
106
|
+
```tsx
|
|
107
|
+
const agent = useAgent({
|
|
82
108
|
host: "localhost:8787",
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
connectionParams: { userId: "…" },
|
|
86
|
-
onConnectionStatusChange: setConnectionStatus,
|
|
109
|
+
agentName: "SupportAgent",
|
|
110
|
+
name: `${userId}:support`,
|
|
87
111
|
});
|
|
88
112
|
|
|
89
|
-
|
|
113
|
+
await agent.call("checkOrder", ["1234"]);
|
|
90
114
|
```
|
|
91
115
|
|
|
92
|
-
|
|
116
|
+
You can also drive turns server-side — call `saveMessages` from a schedule or alarm.
|
|
93
117
|
|
|
94
|
-
|
|
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 {
|
|
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";
|
|
125
|
+
import { weatherSkill } from "./skills/weather";
|
|
118
126
|
|
|
119
|
-
|
|
120
|
-
|
|
127
|
+
export class MyChatAgent extends ChatAgent {
|
|
128
|
+
getModel() {
|
|
129
|
+
return openai("gpt-4o");
|
|
121
130
|
}
|
|
122
131
|
|
|
123
|
-
getSystemPrompt(
|
|
132
|
+
getSystemPrompt() {
|
|
124
133
|
return "You are a helpful assistant.";
|
|
125
134
|
}
|
|
126
135
|
|
|
127
|
-
|
|
128
|
-
return
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
162
|
+
## Many chats per user: Assistant
|
|
177
163
|
|
|
178
|
-
|
|
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 {
|
|
184
|
-
|
|
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
|
-
}
|
|
168
|
+
import { Assistant } from "@economic/agents";
|
|
169
|
+
import { MyChatAgent } from "./chat-agent";
|
|
193
170
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
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
|
-
|
|
187
|
+
The `Assistant` keeps the chat list in its own SQLite (a `chats` table, created automatically) and exposes three callable methods:
|
|
218
188
|
|
|
219
|
-
|
|
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
|
-
|
|
222
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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). |
|
|
248
207
|
|
|
249
|
-
|
|
208
|
+
## Tools
|
|
209
|
+
|
|
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
|
|
213
|
+
import { tool, type ToolSet } from "@economic/agents";
|
|
214
|
+
import { z } from "zod";
|
|
253
215
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
`
|
|
270
|
-
|
|
271
|
-
---
|
|
228
|
+
`authorize` returning `false` hides the tool for that request.
|
|
272
229
|
|
|
273
|
-
###
|
|
230
|
+
### Tool context
|
|
274
231
|
|
|
275
|
-
|
|
232
|
+
The tool context is the second argument to `execute` — `experimental_context`:
|
|
276
233
|
|
|
277
234
|
```typescript
|
|
278
|
-
import type
|
|
279
|
-
import {
|
|
235
|
+
import { tool, type ToolContext } from "@economic/agents";
|
|
236
|
+
import { z } from "zod";
|
|
280
237
|
|
|
281
|
-
interface
|
|
282
|
-
|
|
283
|
-
userGuid: string;
|
|
284
|
-
agreementNumber: number;
|
|
238
|
+
interface RequestBody {
|
|
239
|
+
tokens: Record<string, string>;
|
|
285
240
|
}
|
|
286
241
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
return
|
|
293
|
-
|
|
294
|
-
|
|
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
|
-
|
|
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.
|
|
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`).
|
|
320
253
|
|
|
321
|
-
|
|
254
|
+
## Skills
|
|
322
255
|
|
|
323
|
-
|
|
324
|
-
|
|
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
|
-
|
|
331
|
-
|
|
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";
|
|
260
|
+
import { z } from "zod";
|
|
342
261
|
|
|
343
|
-
|
|
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
|
-
|
|
268
|
+
## When to use
|
|
269
|
+
Use this skill whenever the user asks about current conditions or a forecast.
|
|
346
270
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
-
|
|
360
|
-
description: "
|
|
361
|
-
inputSchema: z.object({
|
|
362
|
-
|
|
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
|
-
|
|
286
|
+
Return skills from `getSkills()`. Add `authorize(ctx)` to gate a skill (and its tools). `skill()` throws if `description` or `instructions` is missing.
|
|
415
287
|
|
|
416
|
-
###
|
|
288
|
+
### Remote skills
|
|
417
289
|
|
|
418
|
-
|
|
419
|
-
|
|
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
|
-
|
|
426
|
-
|
|
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
|
-
|
|
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
|
-
|
|
300
|
+
## Authentication
|
|
449
301
|
|
|
450
|
-
|
|
302
|
+
### JWT
|
|
451
303
|
|
|
452
|
-
|
|
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
|
|
456
|
-
|
|
457
|
-
return
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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
|
-
//
|
|
467
|
-
protected conversationRetentionDays = 30;
|
|
320
|
+
// ...
|
|
468
321
|
}
|
|
469
322
|
```
|
|
470
323
|
|
|
471
|
-
|
|
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
|
-
|
|
326
|
+
### User context
|
|
474
327
|
|
|
475
|
-
|
|
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
|
-
|
|
485
|
-
await
|
|
486
|
-
|
|
487
|
-
|
|
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
|
-
|
|
496
|
-
|
|
497
|
-
---
|
|
498
|
-
|
|
499
|
-
## API Reference
|
|
500
|
-
|
|
501
|
-
### `@economic/agents`
|
|
502
|
-
|
|
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` |
|
|
513
|
-
|
|
514
|
-
### `@economic/agents-react`
|
|
515
|
-
|
|
516
|
-
React hooks are in a separate package. See [`@economic/agents-react`](../react/README.md) for full documentation.
|
|
337
|
+
## Observability
|
|
517
338
|
|
|
518
|
-
|
|
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"` |
|
|
339
|
+
Emitted per turn from the `ai` SDK's OpenTelemetry spans, no setup beyond bindings:
|
|
523
340
|
|
|
524
|
-
|
|
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`.
|
|
525
343
|
|
|
526
|
-
|
|
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) |
|
|
344
|
+
## API reference
|
|
530
345
|
|
|
531
|
-
|
|
346
|
+
Imported from `@economic/agents` unless noted.
|
|
532
347
|
|
|
533
|
-
|
|
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
|
-
```
|
|
542
|
-
|
|
543
|
-
This will:
|
|
544
|
-
|
|
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
|
|
551
|
-
|
|
552
|
-
### Generate a Tool
|
|
553
|
-
|
|
554
|
-
```bash
|
|
555
|
-
npx @economic/agents generate tool geocode
|
|
556
|
-
```
|
|
348
|
+
### Classes
|
|
557
349
|
|
|
558
|
-
|
|
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. |
|
|
559
355
|
|
|
560
|
-
|
|
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
|
|
356
|
+
### Helpers and types
|
|
565
357
|
|
|
566
|
-
|
|
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. |
|
|
567
365
|
|
|
568
|
-
|
|
366
|
+
### Members
|
|
569
367
|
|
|
570
|
-
|
|
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. |
|
|
571
381
|
|
|
572
|
-
|
|
382
|
+
### Package root (`@economic/agents`)
|
|
573
383
|
|
|
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
|
|
393
|
+
npm run build
|
|
582
394
|
```
|
|
583
395
|
|
|
584
396
|
.
|