@economic/agents 0.0.1-alpha.12 → 0.0.1-alpha.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -43,6 +43,9 @@ const searchSkill: Skill = {
43
43
  };
44
44
 
45
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
+
46
49
  async onChatMessage(onFinish, options) {
47
50
  const params = await this.buildLLMParams({
48
51
  options,
@@ -111,10 +114,27 @@ Protected method on `AIChatAgent`. Wraps the standalone `buildLLMParams` functio
111
114
 
112
115
  - `messages` pre-filled from `this.messages`
113
116
  - `activeSkills` pre-filled from `await this.getLoadedSkills()`
117
+ - `fastModel` injected from `this.fastModel`
114
118
  - `log` injected into `experimental_context` alongside `options.body`
115
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
+ ```
116
134
 
117
- Config is everything accepted by the standalone `buildLLMParams` except `messages` and `activeSkills`.
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`.
118
138
 
119
139
  ### `getLoadedSkills()`
120
140
 
@@ -152,7 +172,6 @@ const params = await buildLLMParams({
152
172
  system: "You are a helpful assistant.",
153
173
  skills: [searchSkill, codeSkill],
154
174
  tools: { myAlwaysOnTool },
155
- compact: { model: openai("gpt-4o-mini"), maxMessages: 30 },
156
175
  stopWhen: stepCountIs(20), // defaults to stepCountIs(20)
157
176
  });
158
177
 
@@ -160,18 +179,18 @@ return streamText(params).toUIMessageStreamResponse();
160
179
  // or: generateText(params);
161
180
  ```
162
181
 
163
- | Parameter | Type | Required | Description |
164
- | -------------- | ----------------------------------------------- | -------- | ------------------------------------------------------------------------------ |
165
- | `options` | `OnChatMessageOptions \| undefined` | Yes | CF options object. Extracts `abortSignal` and `experimental_context`. |
166
- | `onFinish` | `StreamTextOnFinishCallback<ToolSet>` | Yes | Called when the stream completes. |
167
- | `model` | `LanguageModel` | Yes | The language model to use. |
168
- | `messages` | `UIMessage[]` | Yes | Conversation history. Converted to `ModelMessage[]` internally. |
169
- | `activeSkills` | `string[]` | No | Names of skills loaded in previous turns. Pass `await this.getLoadedSkills()`. |
170
- | `skills` | `Skill[]` | No | Skills available for on-demand loading. Wires up meta-tools automatically. |
171
- | `system` | `string` | No | Base system prompt. |
172
- | `tools` | `ToolSet` | No | Always-on tools, active every turn regardless of loaded skills. |
173
- | `compact` | `{ model: LanguageModel; maxMessages: number }` | No | When provided, compacts old messages before sending to the model. |
174
- | `stopWhen` | `StopCondition` | No | Stop condition. Defaults to `stepCountIs(20)`. |
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)`. |
175
194
 
176
195
  When `skills` are provided, `buildLLMParams`:
177
196
 
@@ -244,27 +263,62 @@ export const datetimeSkill: Skill = {
244
263
 
245
264
  ## Compaction
246
265
 
247
- When `compact` is provided to `buildLLMParams`, it compacts `messages` before converting and sending to the model:
266
+ When `fastModel` is set on the agent class, compaction runs automatically before each turn:
248
267
 
249
- 1. The message list is split into an older window and a recent verbatim tail (`maxMessages`).
250
- 2. A model call generates a concise summary of the older window.
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.
251
270
  3. That summary + the verbatim tail is what gets sent to the LLM.
252
271
  4. Full history in DO SQLite is unaffected — compaction is in-memory only.
253
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
+
254
298
  ```typescript
255
299
  const params = await this.buildLLMParams({
256
300
  options,
257
301
  onFinish,
258
302
  model: openai("gpt-4o"),
259
- system: "...",
260
- compact: {
261
- model: openai("gpt-4o-mini"), // cheaper model for summarisation
262
- maxMessages: 30, // keep last 30 messages verbatim
263
- },
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,
314
+ onFinish,
315
+ model: openai("gpt-4o"),
316
+ maxMessagesBeforeCompaction: undefined, // compaction off
264
317
  });
265
- return streamText(params).toUIMessageStreamResponse();
266
318
  ```
267
319
 
320
+ Compaction is always off when `fastModel` is `undefined` (the base class default).
321
+
268
322
  ---
269
323
 
270
324
  ## Built-in meta tools
@@ -317,13 +371,13 @@ execute: async (args, { experimental_context }) => {
317
371
  };
318
372
  ```
319
373
 
320
- `log` is a no-op when `AUDIT_DB` is not bound — so no changes are needed in tools when running without a D1 database.
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.
321
375
 
322
376
  ---
323
377
 
324
378
  ## Audit logging — D1 setup
325
379
 
326
- `AIChatAgent` writes audit events to a Cloudflare D1 database when `AUDIT_DB` is bound on the environment. The table is shared across all agent workers — create it once.
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.
327
381
 
328
382
  ### 1. Create the D1 database
329
383
 
@@ -331,19 +385,31 @@ In the [Cloudflare dashboard](https://dash.cloudflare.com) → **Workers & Pages
331
385
 
332
386
  ### 2. Create the schema
333
387
 
334
- Open the database in the D1 dashboard, select **Console**, and run the contents of [`schema/audit_events.sql`](schema/audit_events.sql):
388
+ Open the database in the D1 dashboard, select **Console**, and run the contents of [`schema/schema.sql`](schema/schema.sql) — this creates both the `audit_events` and `conversations` tables in one step:
335
389
 
336
390
  ```sql
337
391
  CREATE TABLE IF NOT EXISTS audit_events (
338
392
  id TEXT PRIMARY KEY,
339
- agent_name TEXT NOT NULL,
340
393
  durable_object_id TEXT NOT NULL,
394
+ user_id TEXT NOT NULL,
341
395
  message TEXT NOT NULL,
342
396
  payload TEXT,
343
397
  created_at TEXT NOT NULL
344
398
  );
345
- CREATE INDEX IF NOT EXISTS audit_events_do ON audit_events(durable_object_id);
346
- CREATE INDEX IF NOT EXISTS audit_events_ts ON audit_events(created_at);
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
+ CREATE TABLE IF NOT EXISTS conversations (
404
+ durable_object_id TEXT PRIMARY KEY,
405
+ user_id TEXT NOT NULL,
406
+ title TEXT,
407
+ summary TEXT,
408
+ created_at TEXT NOT NULL,
409
+ updated_at TEXT NOT NULL
410
+ );
411
+ CREATE INDEX IF NOT EXISTS conversations_user ON conversations(user_id);
412
+ CREATE INDEX IF NOT EXISTS conversations_ts ON conversations(updated_at);
347
413
  ```
348
414
 
349
415
  Safe to re-run — all statements use `IF NOT EXISTS`.
@@ -352,7 +418,7 @@ Safe to re-run — all statements use `IF NOT EXISTS`.
352
418
 
353
419
  ```jsonc
354
420
  "d1_databases": [
355
- { "binding": "AUDIT_DB", "database_name": "agents", "database_id": "YOUR_DB_ID" }
421
+ { "binding": "AGENT_DB", "database_name": "agents", "database_id": "YOUR_DB_ID" }
356
422
  ]
357
423
  ```
358
424
 
@@ -366,7 +432,64 @@ npm run db:setup
366
432
 
367
433
  This runs the schema SQL against the local D1 SQLite file (`.wrangler/state/`). Re-running is harmless.
368
434
 
369
- If `AUDIT_DB` is not bound, all `log()` calls are silent no-ops — the agent works without it.
435
+ If `AGENT_DB` is not bound, all `log()` calls are silent no-ops — the agent works without it.
436
+
437
+ ### Providing `userId`
438
+
439
+ 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`:
440
+
441
+ ```typescript
442
+ useAgentChat({
443
+ agent,
444
+ body: {
445
+ userId: "148583_matt", // compose from agreement number + user identifier
446
+ // ...other fields
447
+ },
448
+ });
449
+ ```
450
+
451
+ 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.
452
+
453
+ ---
454
+
455
+ ## Conversations — D1 setup
456
+
457
+ `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.
458
+
459
+ The `conversations` table is created by the same `schema/schema.sql` file used for audit events — no separate setup step needed.
460
+
461
+ ### Upsert behaviour
462
+
463
+ - **First turn**: a new row is inserted with `created_at` and `updated_at` both set to now. `title` and `summary` are `NULL`.
464
+ - **Subsequent turns**: only `user_id` and `updated_at` are updated. `created_at`, `title`, and `summary` are never overwritten by the upsert.
465
+ - `title` and `summary` are populated automatically after the conversation goes idle (see below).
466
+
467
+ ### Automatic title and summary generation
468
+
469
+ 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.
470
+
471
+ When `generateSummary` fires it:
472
+
473
+ 1. Fetches the current summary from D1 (if any).
474
+ 2. Takes the last 30 messages (`SUMMARY_CONTEXT_MESSAGES`) to keep the prompt bounded.
475
+ 3. Calls `fastModel` with `Output.object()` to generate a structured `{ title, summary }`.
476
+ 4. If a previous summary exists, it is included in the prompt so the model can detect direction changes.
477
+ 5. Writes the result back to the `conversations` row.
478
+
479
+ No subclass code is needed — this runs automatically when `AGENT_DB` is bound and `fastModel` is set on the class.
480
+
481
+ ### Querying conversation lists
482
+
483
+ To fetch all conversations for a user, ordered by most recent:
484
+
485
+ ```sql
486
+ SELECT durable_object_id, title, summary, created_at, updated_at
487
+ FROM conversations
488
+ WHERE user_id = '148583_matt'
489
+ ORDER BY updated_at DESC;
490
+ ```
491
+
492
+ 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.
370
493
 
371
494
  ---
372
495
 
@@ -389,7 +512,6 @@ If `AUDIT_DB` is not bound, all `log()` calls are silent no-ops — the agent wo
389
512
  | Export | Description |
390
513
  | ---------------------- | ------------------------------------------------------------------------------- |
391
514
  | `Skill` | A named group of tools with optional guidance. |
392
- | `CompactOptions` | `{ model: LanguageModel; maxMessages: number }` |
393
515
  | `AgentContext<TBody>` | Request body type merged with `log`. Use as the type of `experimental_context`. |
394
516
  | `BuildLLMParamsConfig` | Config type for the standalone `buildLLMParams` function. |
395
517
 
package/dist/index.d.mts CHANGED
@@ -22,20 +22,32 @@ interface Skill {
22
22
  tools: ToolSet;
23
23
  }
24
24
  //#endregion
25
- //#region src/features/compaction/index.d.ts
26
- type CompactOptions = {
27
- /** Model used to generate the compaction summary */model: LanguageModel; /** Number of recent messages to keep verbatim; older messages are summarised */
28
- maxMessages: number;
29
- };
30
- //#endregion
31
25
  //#region src/llm.d.ts
32
26
  type LLMParams = Parameters<typeof streamText>[0] & Parameters<typeof generateText>[0];
33
27
  type BuildLLMParamsConfig = Omit<LLMParams, "messages" | "experimental_context" | "abortSignal"> & {
34
28
  /** CF options object — extracts `abortSignal` and `experimental_context` (from `body`). */options: OnChatMessageOptions | undefined; /** Conversation history (`this.messages`). Converted to `ModelMessage[]` internally. */
35
29
  messages: UIMessage[]; /** Skill names loaded in previous turns. Pass `await this.getLoadedSkills()`. */
36
30
  activeSkills?: string[]; /** Skills available for on-demand loading this turn. */
37
- skills?: Skill[]; /** When provided, compacts old messages before sending to the model. */
38
- compact?: CompactOptions;
31
+ skills?: Skill[];
32
+ /**
33
+ * Number of recent messages to keep verbatim during compaction. Older messages
34
+ * beyond this count are summarised by `fastModel` before being sent to the LLM.
35
+ *
36
+ * Defaults to `DEFAULT_MAX_MESSAGES_BEFORE_COMPACTION` (30) when not provided.
37
+ * Set explicitly to `undefined` to disable compaction entirely.
38
+ *
39
+ * Compaction only runs when `fastModel` is also set on the agent class.
40
+ *
41
+ * @internal Injected by `AIChatAgent.buildLLMParams` — do not set this directly.
42
+ */
43
+ maxMessagesBeforeCompaction?: number;
44
+ /**
45
+ * The fast/cheap model used for compaction and background summarization.
46
+ * Provided automatically from `AIChatAgent.fastModel` — do not set this directly.
47
+ *
48
+ * @internal
49
+ */
50
+ fastModel?: LanguageModel;
39
51
  };
40
52
  /**
41
53
  * Builds the parameter object for a Vercel AI SDK `streamText` or `generateText` call.
@@ -67,7 +79,32 @@ declare function buildLLMParams(config: BuildLLMParamsConfig): Promise<LLMParams
67
79
  */
68
80
  declare abstract class AIChatAgent<Env extends Cloudflare.Env = Cloudflare.Env> extends AIChatAgent$1<Env> {
69
81
  /**
70
- * Writes an audit event to D1 if `AUDIT_DB` is bound on the environment,
82
+ * Composed user identifier extracted from `options.body.userId` during
83
+ * `buildLLMParams`. Expected format: `{agreementNumber}_{userId}`, e.g. `148583_matt`.
84
+ * Undefined if the client did not include `userId` in the request body.
85
+ */
86
+ protected _userId: string | undefined;
87
+ /**
88
+ * Fast/cheap language model used for background tasks: compaction and conversation summarization.
89
+ *
90
+ * Declare this on every subclass:
91
+ *
92
+ * ```typescript
93
+ * protected fastModel = google("gemini-2.0-flash");
94
+ * ```
95
+ *
96
+ * To disable compaction for a specific call, pass `maxMessagesBeforeCompaction: undefined`
97
+ * to `buildLLMParams` rather than omitting or nulling out `fastModel`.
98
+ */
99
+ protected abstract fastModel: LanguageModel;
100
+ /**
101
+ * Resolves the D1 database binding and userId required for all D1 writes.
102
+ * Returns null and silently no-ops if AGENT_DB is not bound.
103
+ * Returns null and logs an error if userId is missing from the request body.
104
+ */
105
+ private resolveD1Context;
106
+ /**
107
+ * Writes an audit event to D1 if `AGENT_DB` is bound on the environment,
71
108
  * otherwise silently does nothing.
72
109
  *
73
110
  * Called automatically after every turn (from `persistMessages`) and on
@@ -75,17 +112,53 @@ declare abstract class AIChatAgent<Env extends Cloudflare.Env = Cloudflare.Env>
75
112
  * `experimental_context.log` in tool `execute` functions.
76
113
  */
77
114
  protected log(message: string, payload?: Record<string, unknown>): Promise<void>;
115
+ /**
116
+ * Records this conversation in the `conversations` D1 table and resets
117
+ * the idle summarization timer. Called automatically from `persistMessages`
118
+ * after every turn.
119
+ *
120
+ * After each upsert, any pending `generateSummary` schedule is cancelled
121
+ * and a new one is set for 30 minutes from now. If the user sends another
122
+ * message before the timer fires, the schedule is cancelled and reset again
123
+ * (debounce). When the conversation goes idle, `generateSummary` fires and
124
+ * writes the LLM-generated title and summary to D1.
125
+ */
126
+ private recordConversation;
127
+ /**
128
+ * Generates a title and summary for the conversation after 30 minutes of
129
+ * inactivity. Invoked automatically by the Cloudflare Agents SDK scheduler
130
+ * — do not call this directly.
131
+ *
132
+ * Delegates to `generateConversationSummary` in `features/conversations`,
133
+ * which fetches the previous summary, slices to the last
134
+ * `SUMMARY_CONTEXT_MESSAGES` messages, calls `fastModel` with a structured
135
+ * output schema, and writes the result back to D1.
136
+ */
137
+ generateSummary(): Promise<void>;
78
138
  /**
79
139
  * Builds the parameter object for a `streamText` or `generateText` call,
80
- * pre-filling `messages` and `activeSkills` from this agent instance.
140
+ * pre-filling `messages`, `activeSkills`, and `fastModel` from this agent instance.
81
141
  * Injects `log` into `experimental_context` and logs non-clean finish reasons.
82
142
  *
143
+ * **Compaction** runs automatically when `fastModel` is set on the class, using
144
+ * `DEFAULT_MAX_MESSAGES_BEFORE_COMPACTION` (30) as the threshold. Override the
145
+ * threshold by passing `maxMessagesBeforeCompaction`. Disable compaction entirely
146
+ * by passing `maxMessagesBeforeCompaction: undefined` explicitly.
147
+ *
83
148
  * ```typescript
149
+ * // Compaction on (default threshold):
84
150
  * const params = await this.buildLLMParams({ options, onFinish, model, system: "..." });
151
+ *
152
+ * // Compaction with custom threshold:
153
+ * const params = await this.buildLLMParams({ options, onFinish, model, maxMessagesBeforeCompaction: 50 });
154
+ *
155
+ * // Compaction off:
156
+ * const params = await this.buildLLMParams({ options, onFinish, model, maxMessagesBeforeCompaction: undefined });
157
+ *
85
158
  * return streamText(params).toUIMessageStreamResponse();
86
159
  * ```
87
160
  */
88
- protected buildLLMParams(config: Omit<BuildLLMParamsConfig, "messages" | "activeSkills">): ReturnType<typeof buildLLMParams>;
161
+ protected buildLLMParams(config: Omit<BuildLLMParamsConfig, "messages" | "activeSkills" | "fastModel">): ReturnType<typeof buildLLMParams>;
89
162
  /**
90
163
  * Skill names persisted from previous turns, read from DO SQLite.
91
164
  * Returns an empty array if no skills have been loaded yet.
@@ -108,9 +181,16 @@ declare abstract class AIChatAgent<Env extends Cloudflare.Env = Cloudflare.Env>
108
181
  persistMessages(messages: UIMessage[], excludeBroadcastIds?: string[], options?: {
109
182
  _deleteStaleRows?: boolean;
110
183
  }): Promise<void>;
111
- private ensureSkillTableExists;
112
184
  }
113
185
  //#endregion
186
+ //#region src/features/compaction/index.d.ts
187
+ /**
188
+ * Number of recent messages to keep verbatim when compaction runs.
189
+ * Older messages beyond this count are summarised into a single system message.
190
+ * Used as the default when `maxMessagesBeforeCompaction` is not provided to `buildLLMParams`.
191
+ */
192
+ declare const DEFAULT_MAX_MESSAGES_BEFORE_COMPACTION = 30;
193
+ //#endregion
114
194
  //#region src/types.d.ts
115
195
  /**
116
196
  * The context object available throughout an agent's lifetime — passed via
@@ -127,4 +207,4 @@ type AgentContext<TBody = Record<string, unknown>> = TBody & {
127
207
  log: (message: string, payload?: Record<string, unknown>) => void | Promise<void>;
128
208
  };
129
209
  //#endregion
130
- export { AIChatAgent, type AgentContext, type BuildLLMParamsConfig, type CompactOptions, type Skill, buildLLMParams };
210
+ export { AIChatAgent, type AgentContext, type BuildLLMParamsConfig, DEFAULT_MAX_MESSAGES_BEFORE_COMPACTION, type Skill, buildLLMParams };