@economic/agents 0.0.1-alpha.11 → 0.0.1-alpha.13
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 +282 -143
- package/dist/index.d.mts +175 -90
- package/dist/index.mjs +3698 -127
- package/package.json +6 -2
- package/schema/audit_events.sql +16 -0
- package/schema/conversations.sql +15 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @economic/agents
|
|
2
2
|
|
|
3
|
-
Base class and
|
|
3
|
+
Base class and utilities for building LLM chat agents on Cloudflare's Agents SDK with lazy skill loading, optional message compaction, and built-in audit logging.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
npm install @economic/agents ai @cloudflare/ai-chat
|
|
@@ -12,8 +12,8 @@ npm install @economic/agents ai @cloudflare/ai-chat
|
|
|
12
12
|
|
|
13
13
|
`@economic/agents` provides:
|
|
14
14
|
|
|
15
|
-
- **`AIChatAgent`** — an abstract Cloudflare Durable Object base class. Implement `onChatMessage` and
|
|
16
|
-
- **`
|
|
15
|
+
- **`AIChatAgent`** — an abstract Cloudflare Durable Object base class. Implement `onChatMessage`, call `this.buildLLMParams()`, and pass the result to `streamText` from the AI SDK.
|
|
16
|
+
- **`buildLLMParams`** — the standalone version of the above, for use outside of `AIChatAgent` or in custom agent implementations.
|
|
17
17
|
|
|
18
18
|
Skills and compaction are AI SDK concerns — they control what goes to the LLM. The CF layer is responsible for WebSockets, Durable Objects, and message persistence. These are kept separate.
|
|
19
19
|
|
|
@@ -22,11 +22,12 @@ Skills and compaction are AI SDK concerns — they control what goes to the LLM.
|
|
|
22
22
|
## Quick start
|
|
23
23
|
|
|
24
24
|
```typescript
|
|
25
|
-
import {
|
|
26
|
-
import type { Skill } from "@economic/agents";
|
|
25
|
+
import { streamText } from "ai";
|
|
27
26
|
import { openai } from "@ai-sdk/openai";
|
|
28
|
-
import {
|
|
27
|
+
import { tool } from "ai";
|
|
29
28
|
import { z } from "zod";
|
|
29
|
+
import { AIChatAgent } from "@economic/agents";
|
|
30
|
+
import type { Skill } from "@economic/agents";
|
|
30
31
|
|
|
31
32
|
const searchSkill: Skill = {
|
|
32
33
|
name: "search",
|
|
@@ -42,18 +43,18 @@ const searchSkill: Skill = {
|
|
|
42
43
|
};
|
|
43
44
|
|
|
44
45
|
export class MyAgent extends AIChatAgent<Env> {
|
|
46
|
+
// Set fastModel to enable automatic compaction and future background summarization.
|
|
47
|
+
protected fastModel = openai("gpt-4o-mini");
|
|
48
|
+
|
|
45
49
|
async onChatMessage(onFinish, options) {
|
|
46
|
-
const
|
|
50
|
+
const params = await this.buildLLMParams({
|
|
51
|
+
options,
|
|
52
|
+
onFinish,
|
|
47
53
|
model: openai("gpt-4o"),
|
|
48
|
-
messages: await convertToModelMessages(this.messages),
|
|
49
54
|
system: "You are a helpful assistant.",
|
|
50
55
|
skills: [searchSkill],
|
|
51
|
-
activeSkills: await this.getLoadedSkills(),
|
|
52
|
-
stopWhen: stepCountIs(20),
|
|
53
|
-
abortSignal: options?.abortSignal,
|
|
54
|
-
onFinish,
|
|
55
56
|
});
|
|
56
|
-
return
|
|
57
|
+
return streamText(params).toUIMessageStreamResponse();
|
|
57
58
|
}
|
|
58
59
|
}
|
|
59
60
|
```
|
|
@@ -83,36 +84,61 @@ Run `wrangler types` after to generate typed `Env` bindings.
|
|
|
83
84
|
|
|
84
85
|
## `AIChatAgent`
|
|
85
86
|
|
|
86
|
-
Extend this class and implement `onChatMessage`. Call
|
|
87
|
+
Extend this class and implement `onChatMessage`. Call `this.buildLLMParams()` to prepare the call, then pass the result to `streamText` or `generateText`.
|
|
87
88
|
|
|
88
89
|
```typescript
|
|
89
|
-
import {
|
|
90
|
-
import
|
|
90
|
+
import { streamText } from "ai";
|
|
91
|
+
import { AIChatAgent } from "@economic/agents";
|
|
91
92
|
|
|
92
93
|
export class ChatAgent extends AIChatAgent<Env> {
|
|
93
94
|
async onChatMessage(onFinish, options) {
|
|
94
|
-
const body = (options?.body ?? {}) as
|
|
95
|
+
const body = (options?.body ?? {}) as { userTier: "free" | "pro" };
|
|
95
96
|
const model = body.userTier === "pro" ? openai("gpt-4o") : openai("gpt-4o-mini");
|
|
96
97
|
|
|
97
|
-
const
|
|
98
|
+
const params = await this.buildLLMParams({
|
|
99
|
+
options,
|
|
100
|
+
onFinish,
|
|
98
101
|
model,
|
|
99
|
-
messages: await convertToModelMessages(this.messages),
|
|
100
102
|
system: "You are a helpful assistant.",
|
|
101
103
|
skills: [searchSkill, calcSkill], // available for on-demand loading
|
|
102
|
-
|
|
103
|
-
tools: { alwaysOnTool }, // always active
|
|
104
|
-
stopWhen: stepCountIs(20),
|
|
105
|
-
abortSignal: options?.abortSignal,
|
|
106
|
-
onFinish,
|
|
104
|
+
tools: { alwaysOnTool }, // always active, regardless of loaded skills
|
|
107
105
|
});
|
|
108
|
-
return
|
|
106
|
+
return streamText(params).toUIMessageStreamResponse();
|
|
109
107
|
}
|
|
110
108
|
}
|
|
111
109
|
```
|
|
112
110
|
|
|
111
|
+
### `this.buildLLMParams(config)`
|
|
112
|
+
|
|
113
|
+
Protected method on `AIChatAgent`. Wraps the standalone `buildLLMParams` function with:
|
|
114
|
+
|
|
115
|
+
- `messages` pre-filled from `this.messages`
|
|
116
|
+
- `activeSkills` pre-filled from `await this.getLoadedSkills()`
|
|
117
|
+
- `fastModel` injected from `this.fastModel`
|
|
118
|
+
- `log` injected into `experimental_context` alongside `options.body`
|
|
119
|
+
- Automatic error logging for non-clean finish reasons
|
|
120
|
+
- Compaction threshold defaulting: when `maxMessagesBeforeCompaction` is not in the config, defaults to `30`. Pass `maxMessagesBeforeCompaction: undefined` explicitly to disable compaction.
|
|
121
|
+
|
|
122
|
+
Config is everything accepted by the standalone `buildLLMParams` except `messages`, `activeSkills`, and `fastModel`.
|
|
123
|
+
|
|
124
|
+
### `fastModel` property
|
|
125
|
+
|
|
126
|
+
Override `fastModel` on your subclass to enable automatic compaction and future background conversation summarization:
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
export class MyAgent extends AIChatAgent<Env> {
|
|
130
|
+
protected fastModel = openai("gpt-4o-mini");
|
|
131
|
+
// ...
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
When `fastModel` is set, compaction runs automatically with a default threshold of 30 messages. No per-call configuration is needed in the common case. You can still customise or disable it per-call via `maxMessagesBeforeCompaction`.
|
|
136
|
+
|
|
137
|
+
When `fastModel` is `undefined` (the default), compaction is disabled regardless of `maxMessagesBeforeCompaction`.
|
|
138
|
+
|
|
113
139
|
### `getLoadedSkills()`
|
|
114
140
|
|
|
115
|
-
Protected method on `AIChatAgent`. Returns skill names persisted from previous turns (read from DO SQLite).
|
|
141
|
+
Protected method on `AIChatAgent`. Returns skill names persisted from previous turns (read from DO SQLite). Used internally by `this.buildLLMParams()`.
|
|
116
142
|
|
|
117
143
|
### `persistMessages` (automatic)
|
|
118
144
|
|
|
@@ -120,8 +146,9 @@ When `persistMessages` runs at the end of each turn, it:
|
|
|
120
146
|
|
|
121
147
|
1. Scans `activate_skill` tool results for newly loaded skill state.
|
|
122
148
|
2. Writes the updated skill name list to DO SQLite (no D1 needed).
|
|
123
|
-
3.
|
|
124
|
-
4.
|
|
149
|
+
3. Logs a turn summary via `log()`.
|
|
150
|
+
4. Strips all `activate_skill` and `list_capabilities` messages from history.
|
|
151
|
+
5. Delegates to the CF base `persistMessages` for message storage and WS broadcast.
|
|
125
152
|
|
|
126
153
|
### `onConnect` (automatic)
|
|
127
154
|
|
|
@@ -129,64 +156,48 @@ Replays the full message history to newly connected clients — without this, a
|
|
|
129
156
|
|
|
130
157
|
---
|
|
131
158
|
|
|
132
|
-
## `
|
|
133
|
-
|
|
134
|
-
Drop-in replacement for `streamText` from `ai` with three extra params:
|
|
135
|
-
|
|
136
|
-
| Extra param | Type | Description |
|
|
137
|
-
| -------------- | ----------------------------------------------- | ------------------------------------------------------------------------------ |
|
|
138
|
-
| `messages` | `UIMessage[]` | Converted to `ModelMessage[]` internally. Pass `this.messages`. |
|
|
139
|
-
| `skills` | `Skill[]` | Skills available for on-demand loading. Wires up meta-tools automatically. |
|
|
140
|
-
| `activeSkills` | `string[]` | Names of skills loaded in previous turns. Pass `await this.getLoadedSkills()`. |
|
|
141
|
-
| `compact` | `{ model: LanguageModel; maxMessages: number }` | When provided, compacts old messages before sending to the model. |
|
|
159
|
+
## `buildLLMParams` (standalone)
|
|
142
160
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
- Registers `activate_skill` and `list_capabilities` meta-tools
|
|
146
|
-
- Sets initial `activeTools` (meta + always-on + loaded skill tools)
|
|
147
|
-
- Wires up `prepareStep` to update `activeTools` after each step
|
|
148
|
-
- Composes `system` with guidance from loaded skills
|
|
149
|
-
- Merges any `activeTools` / `prepareStep` you also pass (additive)
|
|
161
|
+
The standalone `buildLLMParams` builds the full parameter object for a Vercel AI SDK `streamText` or `generateText` call. Use this directly only if you are not extending `AIChatAgent`, or need fine-grained control.
|
|
150
162
|
|
|
151
163
|
```typescript
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
164
|
+
import { buildLLMParams } from "@economic/agents";
|
|
165
|
+
|
|
166
|
+
const params = await buildLLMParams({
|
|
167
|
+
options, // OnChatMessageOptions — extracts abortSignal and body
|
|
168
|
+
onFinish, // StreamTextOnFinishCallback<ToolSet>
|
|
169
|
+
model, // LanguageModel
|
|
170
|
+
messages: this.messages, // UIMessage[] — converted to ModelMessage[] internally
|
|
171
|
+
activeSkills: await this.getLoadedSkills(),
|
|
156
172
|
system: "You are a helpful assistant.",
|
|
157
173
|
skills: [searchSkill, codeSkill],
|
|
158
|
-
activeSkills: await this.getLoadedSkills(),
|
|
159
174
|
tools: { myAlwaysOnTool },
|
|
160
|
-
|
|
161
|
-
onFinish,
|
|
175
|
+
stopWhen: stepCountIs(20), // defaults to stepCountIs(20)
|
|
162
176
|
});
|
|
163
|
-
return stream.toUIMessageStreamResponse();
|
|
164
177
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
model: openai("gpt-4o"),
|
|
168
|
-
messages: await convertToModelMessages(this.messages),
|
|
169
|
-
tools: { myTool },
|
|
170
|
-
activeTools: ["myTool"],
|
|
171
|
-
onFinish,
|
|
172
|
-
});
|
|
173
|
-
return stream.toUIMessageStreamResponse();
|
|
178
|
+
return streamText(params).toUIMessageStreamResponse();
|
|
179
|
+
// or: generateText(params);
|
|
174
180
|
```
|
|
175
181
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
182
|
+
| Parameter | Type | Required | Description |
|
|
183
|
+
| ----------------------------- | ------------------------------------- | -------- | ------------------------------------------------------------------------------------------------- |
|
|
184
|
+
| `options` | `OnChatMessageOptions \| undefined` | Yes | CF options object. Extracts `abortSignal` and `experimental_context`. |
|
|
185
|
+
| `onFinish` | `StreamTextOnFinishCallback<ToolSet>` | Yes | Called when the stream completes. |
|
|
186
|
+
| `model` | `LanguageModel` | Yes | The language model to use. |
|
|
187
|
+
| `messages` | `UIMessage[]` | Yes | Conversation history. Converted to `ModelMessage[]` internally. |
|
|
188
|
+
| `activeSkills` | `string[]` | No | Names of skills loaded in previous turns. Pass `await this.getLoadedSkills()`. |
|
|
189
|
+
| `skills` | `Skill[]` | No | Skills available for on-demand loading. Wires up meta-tools automatically. |
|
|
190
|
+
| `system` | `string` | No | Base system prompt. |
|
|
191
|
+
| `tools` | `ToolSet` | No | Always-on tools, active every turn regardless of loaded skills. |
|
|
192
|
+
| `maxMessagesBeforeCompaction` | `number \| undefined` | No | Verbatim tail kept during compaction. Defaults to `30` when omitted. Pass `undefined` to disable. |
|
|
193
|
+
| `stopWhen` | `StopCondition` | No | Stop condition. Defaults to `stepCountIs(20)`. |
|
|
194
|
+
|
|
195
|
+
When `skills` are provided, `buildLLMParams`:
|
|
196
|
+
|
|
197
|
+
- Registers `activate_skill` and `list_capabilities` meta-tools.
|
|
198
|
+
- Sets initial `activeTools` (meta + always-on + loaded skill tools).
|
|
199
|
+
- Wires up `prepareStep` to update `activeTools` after each step.
|
|
200
|
+
- Composes `system` with guidance from loaded skills.
|
|
190
201
|
|
|
191
202
|
---
|
|
192
203
|
|
|
@@ -252,27 +263,62 @@ export const datetimeSkill: Skill = {
|
|
|
252
263
|
|
|
253
264
|
## Compaction
|
|
254
265
|
|
|
255
|
-
When `
|
|
266
|
+
When `fastModel` is set on the agent class, compaction runs automatically before each turn:
|
|
256
267
|
|
|
257
|
-
1. The message list is split into an older window and a recent verbatim tail
|
|
258
|
-
2.
|
|
268
|
+
1. The message list is split into an older window and a recent verbatim tail.
|
|
269
|
+
2. `fastModel` generates a concise summary of the older window.
|
|
259
270
|
3. That summary + the verbatim tail is what gets sent to the LLM.
|
|
260
271
|
4. Full history in DO SQLite is unaffected — compaction is in-memory only.
|
|
261
272
|
|
|
273
|
+
### Enabling compaction
|
|
274
|
+
|
|
275
|
+
Override `fastModel` on your subclass. Compaction runs automatically with a default threshold of 30 messages — no per-call config needed:
|
|
276
|
+
|
|
277
|
+
```typescript
|
|
278
|
+
export class MyAgent extends AIChatAgent<Env> {
|
|
279
|
+
protected fastModel = openai("gpt-4o-mini");
|
|
280
|
+
|
|
281
|
+
async onChatMessage(onFinish, options) {
|
|
282
|
+
const params = await this.buildLLMParams({
|
|
283
|
+
options,
|
|
284
|
+
onFinish,
|
|
285
|
+
model: openai("gpt-4o"),
|
|
286
|
+
system: "...",
|
|
287
|
+
// No compaction config needed — runs automatically with default threshold
|
|
288
|
+
});
|
|
289
|
+
return streamText(params).toUIMessageStreamResponse();
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### Customising the threshold
|
|
295
|
+
|
|
296
|
+
Pass `maxMessagesBeforeCompaction` to override the default of 30:
|
|
297
|
+
|
|
262
298
|
```typescript
|
|
263
|
-
const
|
|
299
|
+
const params = await this.buildLLMParams({
|
|
300
|
+
options,
|
|
301
|
+
onFinish,
|
|
264
302
|
model: openai("gpt-4o"),
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
303
|
+
maxMessagesBeforeCompaction: 50, // keep last 50 messages verbatim
|
|
304
|
+
});
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
### Disabling compaction
|
|
308
|
+
|
|
309
|
+
Pass `maxMessagesBeforeCompaction: undefined` explicitly to disable compaction for that call, even when `fastModel` is set:
|
|
310
|
+
|
|
311
|
+
```typescript
|
|
312
|
+
const params = await this.buildLLMParams({
|
|
313
|
+
options,
|
|
271
314
|
onFinish,
|
|
315
|
+
model: openai("gpt-4o"),
|
|
316
|
+
maxMessagesBeforeCompaction: undefined, // compaction off
|
|
272
317
|
});
|
|
273
|
-
return stream.toUIMessageStreamResponse();
|
|
274
318
|
```
|
|
275
319
|
|
|
320
|
+
Compaction is always off when `fastModel` is `undefined` (the base class default).
|
|
321
|
+
|
|
276
322
|
---
|
|
277
323
|
|
|
278
324
|
## Built-in meta tools
|
|
@@ -296,7 +342,21 @@ Returns a summary of active tools, loaded skills, and skills available to load.
|
|
|
296
342
|
|
|
297
343
|
## Passing request context to tools
|
|
298
344
|
|
|
299
|
-
Pass arbitrary data via the `body` option of `useAgentChat`. It arrives as `experimental_context` in tool `execute` functions
|
|
345
|
+
Pass arbitrary data via the `body` option of `useAgentChat`. It arrives as `experimental_context` in tool `execute` functions.
|
|
346
|
+
|
|
347
|
+
When using `this.buildLLMParams()`, the context is automatically composed: your body fields plus a `log` function for writing audit events. Use `AgentContext<TBody>` to type it:
|
|
348
|
+
|
|
349
|
+
```typescript
|
|
350
|
+
// types.ts
|
|
351
|
+
import type { AgentContext } from "@economic/agents";
|
|
352
|
+
|
|
353
|
+
interface AgentBody {
|
|
354
|
+
authorization: string;
|
|
355
|
+
userId: string;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export type ToolContext = AgentContext<AgentBody>;
|
|
359
|
+
```
|
|
300
360
|
|
|
301
361
|
```typescript
|
|
302
362
|
// Client
|
|
@@ -304,90 +364,169 @@ useAgentChat({ body: { authorization: token, userId: "u_123" } });
|
|
|
304
364
|
|
|
305
365
|
// Tool
|
|
306
366
|
execute: async (args, { experimental_context }) => {
|
|
307
|
-
const
|
|
367
|
+
const ctx = experimental_context as ToolContext;
|
|
368
|
+
await ctx.log("tool called", { userId: ctx.userId });
|
|
369
|
+
const data = await fetchSomething(ctx.authorization);
|
|
370
|
+
return data;
|
|
308
371
|
};
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
`log` is a no-op when `AGENT_DB` is not bound — so no changes are needed in tools when running without a D1 database.
|
|
375
|
+
|
|
376
|
+
---
|
|
377
|
+
|
|
378
|
+
## Audit logging — D1 setup
|
|
379
|
+
|
|
380
|
+
`AIChatAgent` writes audit events to a Cloudflare D1 database when `AGENT_DB` is bound on the environment. Each agent worker has its own dedicated D1 database.
|
|
381
|
+
|
|
382
|
+
### 1. Create the D1 database
|
|
383
|
+
|
|
384
|
+
In the [Cloudflare dashboard](https://dash.cloudflare.com) → **Workers & Pages** → **D1** → **Create database**. Note the database name and ID.
|
|
385
|
+
|
|
386
|
+
### 2. Create the schema
|
|
387
|
+
|
|
388
|
+
Open the database in the D1 dashboard, select **Console**, and run the contents of [`schema/audit_events.sql`](schema/audit_events.sql):
|
|
389
|
+
|
|
390
|
+
```sql
|
|
391
|
+
CREATE TABLE IF NOT EXISTS audit_events (
|
|
392
|
+
id TEXT PRIMARY KEY,
|
|
393
|
+
durable_object_id TEXT NOT NULL,
|
|
394
|
+
user_id TEXT NOT NULL,
|
|
395
|
+
message TEXT NOT NULL,
|
|
396
|
+
payload TEXT,
|
|
397
|
+
created_at TEXT NOT NULL
|
|
398
|
+
);
|
|
399
|
+
CREATE INDEX IF NOT EXISTS audit_events_user ON audit_events(user_id);
|
|
400
|
+
CREATE INDEX IF NOT EXISTS audit_events_do ON audit_events(durable_object_id);
|
|
401
|
+
CREATE INDEX IF NOT EXISTS audit_events_ts ON audit_events(created_at);
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
Safe to re-run — all statements use `IF NOT EXISTS`.
|
|
405
|
+
|
|
406
|
+
### 3. Bind it in `wrangler.jsonc`
|
|
407
|
+
|
|
408
|
+
```jsonc
|
|
409
|
+
"d1_databases": [
|
|
410
|
+
{ "binding": "AGENT_DB", "database_name": "agents", "database_id": "YOUR_DB_ID" }
|
|
411
|
+
]
|
|
412
|
+
```
|
|
309
413
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
414
|
+
Then run `wrangler types` to regenerate the `Env` type.
|
|
415
|
+
|
|
416
|
+
### 4. Seed local development
|
|
417
|
+
|
|
418
|
+
```bash
|
|
419
|
+
npm run db:setup
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
This runs the schema SQL against the local D1 SQLite file (`.wrangler/state/`). Re-running is harmless.
|
|
423
|
+
|
|
424
|
+
If `AGENT_DB` is not bound, all `log()` calls are silent no-ops — the agent works without it.
|
|
425
|
+
|
|
426
|
+
### Providing `userId`
|
|
427
|
+
|
|
428
|
+
The `user_id` column is `NOT NULL`. The base class reads `userId` automatically from `options.body` — no subclass override is needed. The client must include it in the `body` passed to `useAgentChat`:
|
|
429
|
+
|
|
430
|
+
```typescript
|
|
431
|
+
useAgentChat({
|
|
432
|
+
agent,
|
|
433
|
+
body: {
|
|
434
|
+
userId: "148583_matt", // compose from agreement number + user identifier
|
|
435
|
+
// ...other fields
|
|
436
|
+
},
|
|
314
437
|
});
|
|
315
|
-
return stream.toUIMessageStreamResponse();
|
|
316
438
|
```
|
|
317
439
|
|
|
440
|
+
If the client omits `userId`, the audit insert is skipped and a `console.error` is emitted. This will be visible in Wrangler's output during local development and in Workers Logs in production.
|
|
441
|
+
|
|
318
442
|
---
|
|
319
443
|
|
|
320
|
-
##
|
|
444
|
+
## Conversations — D1 setup
|
|
321
445
|
|
|
322
|
-
`
|
|
446
|
+
`AIChatAgent` maintains a `conversations` table in `AGENT_DB` alongside `audit_events`. One row is kept per Durable Object instance (i.e. per conversation). The row is upserted automatically after every turn — no subclass code needed.
|
|
323
447
|
|
|
324
|
-
|
|
325
|
-
import { createSkills, filterEphemeralMessages } from "@economic/agents";
|
|
448
|
+
### Schema
|
|
326
449
|
|
|
327
|
-
|
|
328
|
-
tools: alwaysOnTools,
|
|
329
|
-
skills: permittedSkills,
|
|
330
|
-
initialLoadedSkills: await getLoadedSkills(), // from storage
|
|
331
|
-
systemPrompt: "You are a helpful assistant.",
|
|
332
|
-
});
|
|
450
|
+
Run the contents of [`schema/conversations.sql`](schema/conversations.sql) in the D1 dashboard console (same database as `audit_events`):
|
|
333
451
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
452
|
+
```sql
|
|
453
|
+
CREATE TABLE IF NOT EXISTS conversations (
|
|
454
|
+
durable_object_id TEXT PRIMARY KEY,
|
|
455
|
+
user_id TEXT NOT NULL,
|
|
456
|
+
title TEXT,
|
|
457
|
+
summary TEXT,
|
|
458
|
+
created_at TEXT NOT NULL,
|
|
459
|
+
updated_at TEXT NOT NULL
|
|
460
|
+
);
|
|
461
|
+
CREATE INDEX IF NOT EXISTS conversations_user ON conversations(user_id);
|
|
462
|
+
CREATE INDEX IF NOT EXISTS conversations_ts ON conversations(updated_at);
|
|
342
463
|
```
|
|
343
464
|
|
|
465
|
+
Safe to re-run — all statements use `IF NOT EXISTS`.
|
|
466
|
+
|
|
467
|
+
### Upsert behaviour
|
|
468
|
+
|
|
469
|
+
- **First turn**: a new row is inserted with `created_at` and `updated_at` both set to now. `title` and `summary` are `NULL`.
|
|
470
|
+
- **Subsequent turns**: only `user_id` and `updated_at` are updated. `created_at`, `title`, and `summary` are never overwritten by the upsert.
|
|
471
|
+
- `title` and `summary` are populated automatically after the conversation goes idle (see below).
|
|
472
|
+
|
|
473
|
+
### Automatic title and summary generation
|
|
474
|
+
|
|
475
|
+
After every turn, `AIChatAgent` schedules a `generateSummary` callback to fire 30 minutes in the future. If another message arrives before the timer fires, the schedule is cancelled and reset — so the callback only runs once the conversation has been idle for 30 minutes.
|
|
476
|
+
|
|
477
|
+
When `generateSummary` fires it:
|
|
478
|
+
|
|
479
|
+
1. Fetches the current summary from D1 (if any).
|
|
480
|
+
2. Takes the last 30 messages (`SUMMARY_CONTEXT_MESSAGES`) to keep the prompt bounded.
|
|
481
|
+
3. Calls `fastModel` with `Output.object()` to generate a structured `{ title, summary }`.
|
|
482
|
+
4. If a previous summary exists, it is included in the prompt so the model can detect direction changes.
|
|
483
|
+
5. Writes the result back to the `conversations` row.
|
|
484
|
+
|
|
485
|
+
No subclass code is needed — this runs automatically when `AGENT_DB` is bound and `fastModel` is set on the class.
|
|
486
|
+
|
|
487
|
+
### Querying conversation lists
|
|
488
|
+
|
|
489
|
+
To fetch all conversations for a user, ordered by most recent:
|
|
490
|
+
|
|
491
|
+
```sql
|
|
492
|
+
SELECT durable_object_id, title, summary, created_at, updated_at
|
|
493
|
+
FROM conversations
|
|
494
|
+
WHERE user_id = '148583_matt'
|
|
495
|
+
ORDER BY updated_at DESC;
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
If `userId` is not set on the request body, the upsert is skipped and a `console.error` is emitted — the same behaviour as audit logging.
|
|
499
|
+
|
|
344
500
|
---
|
|
345
501
|
|
|
346
502
|
## API reference
|
|
347
503
|
|
|
348
504
|
### Classes
|
|
349
505
|
|
|
350
|
-
| Export | Description
|
|
351
|
-
| ------------- |
|
|
352
|
-
| `AIChatAgent` | Abstract CF Durable Object base class. Implement `onChatMessage`. Manages skill state
|
|
506
|
+
| Export | Description |
|
|
507
|
+
| ------------- | --------------------------------------------------------------------------------------------------------------------- |
|
|
508
|
+
| `AIChatAgent` | Abstract CF Durable Object base class. Implement `onChatMessage`. Manages skill state, history replay, and audit log. |
|
|
353
509
|
|
|
354
510
|
### Functions
|
|
355
511
|
|
|
356
|
-
| Export
|
|
357
|
-
|
|
|
358
|
-
| `
|
|
359
|
-
| `generateText` | `async (params: GenerateTextParams) => GenerateTextResult` | Wraps AI SDK `generateText`; same extra params as `streamText`. |
|
|
360
|
-
| `createSkills` | `(config: SkillsConfig) => SkillsResult` | Lower-level factory for building the skill loading system. |
|
|
361
|
-
| `filterEphemeralMessages` | `(messages: UIMessage[]) => UIMessage[]` | Strips all `activate_skill` and `list_capabilities` tool calls. |
|
|
362
|
-
| `injectGuidance` | `(messages: ModelMessage[], guidance: string, prev?: string) => ModelMessage[]` | Inserts guidance just before the last user message. **Deprecated** — use `system` in the wrappers instead. |
|
|
363
|
-
| `compactIfNeeded` | `(messages, model, tailSize) => Promise<UIMessage[]>` | Compacts if token estimate exceeds threshold; no-op if model is `undefined`. |
|
|
364
|
-
| `compactMessages` | `(messages, model, tailSize) => Promise<UIMessage[]>` | Summarises the older window and returns `[summaryMsg, ...verbatimTail]`. |
|
|
365
|
-
| `estimateMessagesTokens` | `(messages: UIMessage[]) => number` | Character-count heuristic (÷ 3.5) over text, reasoning, and tool parts. |
|
|
366
|
-
|
|
367
|
-
### Constants
|
|
368
|
-
|
|
369
|
-
| Export | Value | Description |
|
|
370
|
-
| ------------------------- | --------- | ---------------------------------------------------- |
|
|
371
|
-
| `COMPACT_TOKEN_THRESHOLD` | `140_000` | Token count above which compaction is triggered. |
|
|
372
|
-
| `SKILL_STATE_SENTINEL` | `string` | Delimiter used to embed skill state in tool results. |
|
|
512
|
+
| Export | Signature | Description |
|
|
513
|
+
| ---------------- | -------------------------------------- | -------------------------------------------------------------------- |
|
|
514
|
+
| `buildLLMParams` | `async (config) => Promise<LLMParams>` | Builds the full parameter object for `streamText` or `generateText`. |
|
|
373
515
|
|
|
374
516
|
### Types
|
|
375
517
|
|
|
376
|
-
| Export
|
|
377
|
-
|
|
|
378
|
-
| `Skill`
|
|
379
|
-
| `
|
|
380
|
-
| `
|
|
381
|
-
| `StreamTextParams` | Params for the `streamText` wrapper. |
|
|
382
|
-
| `GenerateTextParams` | Params for the `generateText` wrapper. |
|
|
383
|
-
| `CompactOptions` | `{ model: LanguageModel; maxMessages: number }` |
|
|
518
|
+
| Export | Description |
|
|
519
|
+
| ---------------------- | ------------------------------------------------------------------------------- |
|
|
520
|
+
| `Skill` | A named group of tools with optional guidance. |
|
|
521
|
+
| `AgentContext<TBody>` | Request body type merged with `log`. Use as the type of `experimental_context`. |
|
|
522
|
+
| `BuildLLMParamsConfig` | Config type for the standalone `buildLLMParams` function. |
|
|
384
523
|
|
|
385
524
|
---
|
|
386
525
|
|
|
387
526
|
## Development
|
|
388
527
|
|
|
389
528
|
```bash
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
529
|
+
npm install # install dependencies
|
|
530
|
+
npm test # run tests
|
|
531
|
+
npm pack # build
|
|
393
532
|
```
|