@firtoz/chat-agent 1.0.1 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,39 +1,79 @@
1
1
  # @firtoz/chat-agent
2
2
 
3
- DB-agnostic ChatAgent for Cloudflare Durable Objects with OpenRouter - a simplified alternative to `@cloudflare/ai-chat`.
3
+ [![npm version](https://img.shields.io/npm/v/%40firtoz%2Fchat-agent.svg)](https://www.npmjs.com/package/@firtoz/chat-agent)
4
+ [![npm downloads](https://img.shields.io/npm/dm/%40firtoz%2Fchat-agent.svg)](https://www.npmjs.com/package/@firtoz/chat-agent)
5
+ [![license](https://img.shields.io/npm/l/%40firtoz%2Fchat-agent.svg)](https://github.com/firtoz/fullstack-toolkit/blob/main/LICENSE)
6
+
7
+ [![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
8
+ [![Cloudflare](https://img.shields.io/badge/Cloudflare-Durable_Objects-F38020?logo=cloudflare&logoColor=white)](https://developers.cloudflare.com/durable-objects/)
9
+ [![OpenRouter](https://img.shields.io/badge/OpenRouter-AI-6366f1)](https://openrouter.ai/)
10
+
11
+ **Wire protocol (Zod 4), `defineTool`, and `ChatAgentBase` for Durable Objects + OpenRouter** — streaming chat, tools, and multi-tab sync; plug in Drizzle or raw SQL via sibling packages.
12
+
13
+ **Persistence is separate:** install one of:
14
+
15
+ - **`@firtoz/chat-agent-drizzle`** — Drizzle ORM (recommended)
16
+ - **`@firtoz/chat-agent-sql`** — raw `this.sql`
17
+
18
+ ## Upgrading from v1
19
+
20
+ v1 shipped `DrizzleChatAgent`, `SqlChatAgent`, and `ChatAgent` (alias) from this package. v2 keeps only protocol + base here.
21
+
22
+ | v1 | v2 |
23
+ |----|-----|
24
+ | `import { DrizzleChatAgent, defineTool } from "@firtoz/chat-agent"` | `import { defineTool } from "@firtoz/chat-agent"` and `import { DrizzleChatAgent } from "@firtoz/chat-agent-drizzle"` |
25
+ | `import { SqlChatAgent } from "@firtoz/chat-agent"` | `import { SqlChatAgent } from "@firtoz/chat-agent-sql"` |
26
+ | `import { … } from "@firtoz/chat-agent/db/schema"` | `import { … } from "@firtoz/chat-agent-drizzle/db/schema"` |
27
+ | `import { ChatAgent } from "@firtoz/chat-agent"` (Drizzle alias) | `import { DrizzleChatAgent } from "@firtoz/chat-agent-drizzle"` (or a local `type ChatAgent = DrizzleChatAgent`) |
28
+
29
+ Add `bun add @firtoz/chat-agent-drizzle` (and `drizzle-orm`) or `bun add @firtoz/chat-agent-sql` as needed.
4
30
 
5
31
  ## Overview
6
32
 
7
- Three classes for different database preferences:
33
+ - **`ChatAgentBase`** Abstract class with chat + streaming + tools + multi-tab broadcast logic
34
+ - **Concrete agents** — `DrizzleChatAgent` (`@firtoz/chat-agent-drizzle`), `SqlChatAgent` (`@firtoz/chat-agent-sql`)
8
35
 
9
- - **`ChatAgentBase`** - Abstract base class with all chat logic
10
- - **`DrizzleChatAgent`** - Type-safe implementation using Drizzle ORM (recommended)
11
- - **`SqlChatAgent`** - Raw SQL implementation using `this.sql` template tags
36
+ Shared behavior (all implementations):
12
37
 
13
- All implementations share the same API and features:
14
38
  - OpenRouter API integration (simpler than AI SDK)
15
39
  - Resumable streaming with chunk buffering
16
- - Server-side and client-side tool execution
40
+ - **Multi-tab sync**: `messageStart`, chunks, `messageEnd`, `messageUpdated`, `streamResume`, and post-mutation `history` are **broadcast** to every WebSocket on the same Durable Object (merge by message `id` / `streamId` on the client)
41
+ - Serialized chat turns with batched `toolResult` + `autoContinue`
42
+ - Server-side and client-side tools
17
43
  - Cloudflare AI Gateway support
18
- - Message persistence in SQLite
44
+ - Optional `maxPersistedMessages` and `sanitizeMessageForPersistence()`
45
+ - `waitUntilStable()`, `resetTurnState()`, `hasPendingInteraction()` (subclasses)
46
+ - Tool approval (`needsApproval` → `toolApprovalRequest` / `toolApprovalResponse`)
47
+ - Regenerate / client sync (`sendMessage` with `trigger`, optional `messages`)
48
+ - **Provider metadata** on tool calls for upstream round-trips
49
+ - Wire schemas use **Zod 4** (`zod/v4`)
50
+
51
+ ### Long-running streams (Durable Object keep-alive)
52
+
53
+ Streaming uses Partyserver’s `experimental_waitUntil`. Enable **`enable_ctx_exports`** in `wrangler.jsonc` (required for `experimental_waitUntil` on `Server` / `Agent`).
19
54
 
20
55
  ## Installation
21
56
 
22
57
  ```bash
23
58
  bun add @firtoz/chat-agent @openrouter/sdk agents
59
+ ```
24
60
 
25
- # For Drizzle implementation:
26
- bun add drizzle-orm
61
+ For a runnable Durable Object agent, add a persistence package:
62
+
63
+ ```bash
64
+ # Drizzle (recommended)
65
+ bun add @firtoz/chat-agent-drizzle drizzle-orm
27
66
  bun add -d drizzle-kit
28
- ```
29
67
 
30
- ## Quick Start
68
+ # Or raw SQL only
69
+ bun add @firtoz/chat-agent-sql
70
+ ```
31
71
 
32
- ### Using DrizzleChatAgent (Recommended)
72
+ ## Quick start (Drizzle)
33
73
 
34
74
  ```typescript
35
- import { DrizzleChatAgent, defineTool } from "@firtoz/chat-agent";
36
- import type { AgentContext } from "agents";
75
+ import { defineTool, type ToolDefinition } from "@firtoz/chat-agent";
76
+ import { DrizzleChatAgent } from "@firtoz/chat-agent-drizzle";
37
77
 
38
78
  interface Env {
39
79
  OPENROUTER_API_KEY: string;
@@ -48,14 +88,14 @@ class MyAgent extends DrizzleChatAgent<Env> {
48
88
  return "anthropic/claude-sonnet-4.5";
49
89
  }
50
90
 
51
- protected override getTools() {
91
+ protected override getTools(): ToolDefinition[] {
52
92
  return [
53
93
  defineTool({
54
94
  name: "get_time",
55
95
  description: "Get current time",
56
96
  parameters: { type: "object", properties: {} },
57
- execute: async () => ({ time: new Date().toISOString() })
58
- })
97
+ execute: async () => ({ time: new Date().toISOString() }),
98
+ }),
59
99
  ];
60
100
  }
61
101
  }
@@ -63,381 +103,99 @@ class MyAgent extends DrizzleChatAgent<Env> {
63
103
  export { MyAgent };
64
104
  ```
65
105
 
66
- ### Using SqlChatAgent
67
-
68
- ```typescript
69
- import { SqlChatAgent } from "@firtoz/chat-agent";
70
-
71
- class MyAgent extends SqlChatAgent<Env> {
72
- // Same API as DrizzleChatAgent
73
- protected override getSystemPrompt(): string {
74
- return "You are a helpful assistant.";
75
- }
76
- }
77
- ```
78
-
79
- ## Setup Instructions
80
-
81
- ### DrizzleChatAgent Setup
82
-
83
- #### 1. Add Wrangler Rules
84
-
85
- **IMPORTANT:** Add this to your `wrangler.jsonc` to import SQL migration files:
86
-
87
- ```jsonc
88
- {
89
- /**
90
- * Rules to import SQL migration files as text
91
- * Required for Drizzle ORM migrations in Durable Objects
92
- * @see https://orm.drizzle.team/docs/connect-cloudflare-do
93
- */
94
- "rules": [
95
- {
96
- "type": "Text",
97
- "globs": ["**/*.sql"],
98
- "fallthrough": true
99
- }
100
- ]
101
- }
102
- ```
103
-
104
- #### 2. Create Drizzle Config
105
-
106
- Create `drizzle.config.ts` in your package:
107
-
108
- ```typescript
109
- import { defineConfig } from "drizzle-kit";
110
-
111
- export default defineConfig({
112
- schema: "./node_modules/@firtoz/chat-agent/src/db/schema.ts",
113
- out: "./drizzle",
114
- dialect: "sqlite",
115
- driver: "durable-sqlite",
116
- });
117
- ```
106
+ See **`@firtoz/chat-agent-drizzle`** for Wrangler rules, migrations, and `drizzle.config.ts`.
118
107
 
119
- Or if you're in the chat-agent package itself:
108
+ ## Quick start (SQL)
120
109
 
121
110
  ```typescript
122
- import { defineConfig } from "drizzle-kit";
123
-
124
- export default defineConfig({
125
- schema: "./src/db/schema.ts",
126
- out: "./drizzle",
127
- dialect: "sqlite",
128
- driver: "durable-sqlite",
129
- });
130
- ```
111
+ import { defineTool } from "@firtoz/chat-agent";
112
+ import { SqlChatAgent } from "@firtoz/chat-agent-sql";
131
113
 
132
- #### 3. Add Script to package.json
133
-
134
- ```json
135
- {
136
- "scripts": {
137
- "db:generate": "bunx drizzle-kit generate"
138
- }
114
+ class MyAgent extends SqlChatAgent<Env> {
115
+ // Same overrides as Drizzle; tables are created in dbInitialize()
139
116
  }
140
117
  ```
141
118
 
142
- #### 4. Generate Migrations
143
-
144
- ```bash
145
- bun run db:generate
146
- ```
147
-
148
- This creates:
149
- - `drizzle/migrations.js` - Runtime migration imports
150
- - `drizzle/0000_*.sql` - Migration SQL files
151
- - `drizzle/meta/` - Journal and snapshots
152
-
153
- The migrations are automatically run when `DrizzleChatAgent` initializes.
154
-
155
- ### SqlChatAgent Setup
156
-
157
- No additional setup needed! The SQL version creates tables automatically using `this.sql` template tags in `dbInitialize()`.
158
-
159
- ## ChatAgentBase (Abstract Class)
160
-
161
- The base class handles all chat logic and defines the abstract database interface that implementations must provide.
119
+ ## ChatAgentBase (abstract)
162
120
 
163
- ### Abstract Methods
121
+ Subclasses must implement the database interface (see Drizzle/SQL packages for examples).
164
122
 
165
- Subclasses must implement these database operations:
123
+ ### Abstract methods
166
124
 
167
125
  ```typescript
168
- // Initialization
169
126
  protected abstract dbInitialize(): void;
170
-
171
- // Messages
172
127
  protected abstract dbLoadMessages(): ChatMessage[];
173
128
  protected abstract dbSaveMessage(msg: ChatMessage): void;
174
129
  protected abstract dbClearAll(): void;
175
-
176
- // Stream metadata
177
- protected abstract dbFindActiveStream(): {
178
- id: string;
179
- messageId: string;
180
- createdAt: Date
130
+ protected abstract dbFindActiveStream(): {
131
+ id: string;
132
+ messageId: string;
133
+ createdAt: Date;
181
134
  } | null;
182
135
  protected abstract dbDeleteStreamWithChunks(streamId: string): void;
183
136
  protected abstract dbInsertStreamMetadata(streamId: string, messageId: string): void;
184
- protected abstract dbUpdateStreamStatus(streamId: string, status: 'completed' | 'error'): void;
137
+ protected abstract dbUpdateStreamStatus(
138
+ streamId: string,
139
+ status: "completed" | "error",
140
+ ): void;
185
141
  protected abstract dbDeleteOldCompletedStreams(cutoffMs: number): void;
186
-
187
- // Stream chunks
188
142
  protected abstract dbFindMaxChunkIndex(streamId: string): number | null;
189
- protected abstract dbInsertChunks(chunks: Array<{
190
- id: string;
191
- streamId: string;
192
- content: string;
193
- chunkIndex: number
194
- }>): void;
143
+ protected abstract dbInsertChunks(
144
+ chunks: Array<{
145
+ id: string;
146
+ streamId: string;
147
+ content: string;
148
+ chunkIndex: number;
149
+ }>,
150
+ ): void;
195
151
  protected abstract dbGetChunks(streamId: string): string[];
196
152
  protected abstract dbDeleteChunks(streamId: string): void;
153
+ protected abstract dbIsStreamKnown(streamId: string): boolean;
154
+ protected abstract dbReplaceAllMessages(messages: ChatMessage[]): void;
155
+ protected abstract dbTrimMessagesToMax(maxMessages: number): void;
197
156
  ```
198
157
 
199
- ### Override Methods
200
-
201
- Customize your agent's behavior:
158
+ ### Common overrides
202
159
 
203
160
  ```typescript
204
- protected getSystemPrompt(): string {
205
- return "Your custom system prompt";
206
- }
207
-
208
- protected getModel(): string {
209
- // Popular OpenRouter models:
210
- // - anthropic/claude-opus-4.5 (most capable)
211
- // - anthropic/claude-sonnet-4.5 (balanced, default)
212
- // - anthropic/claude-haiku-3.5 (fastest, cheapest)
213
- return "anthropic/claude-sonnet-4.5";
214
- }
215
-
216
- protected getTools(): ToolDefinition[] {
217
- return [
218
- // Your tools here
219
- ];
220
- }
221
- ```
222
-
223
- ## DrizzleChatAgent
224
-
225
- Type-safe implementation using Drizzle ORM.
226
-
227
- ### Database Schema
228
-
229
- The package provides three tables:
230
-
231
- ```typescript
232
- // From @firtoz/chat-agent/db/schema
233
-
234
- export const messagesTable = sqliteTable("messages", {
235
- id: text("id").primaryKey(),
236
- role: text("role", { enum: ["user", "assistant", "tool"] }).notNull(),
237
- messageJson: text("message_json").notNull(),
238
- createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull(),
239
- });
240
-
241
- export const streamChunksTable = sqliteTable("stream_chunks", {
242
- id: text("id").primaryKey(),
243
- streamId: text("stream_id").notNull(),
244
- content: text("content").notNull(),
245
- chunkIndex: integer("chunk_index").notNull(),
246
- createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull(),
247
- });
248
-
249
- export const streamMetadataTable = sqliteTable("stream_metadata", {
250
- id: text("id").primaryKey(),
251
- messageId: text("message_id").notNull(),
252
- status: text("status", { enum: ["streaming", "completed", "error"] }).notNull(),
253
- createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull(),
254
- completedAt: integer("completed_at", { mode: "timestamp_ms" }),
255
- });
256
- ```
257
-
258
- ### Implementation Details
259
-
260
- ```typescript
261
- import { DrizzleChatAgent } from "@firtoz/chat-agent";
262
-
263
- // Automatically:
264
- // - Creates Drizzle DB instance in dbInitialize()
265
- // - Runs migrations from drizzle/migrations.js
266
- // - Uses type-safe query builder for all operations
267
- ```
268
-
269
- ## SqlChatAgent
270
-
271
- Raw SQL implementation following `@cloudflare/ai-chat` pattern.
272
-
273
- ### Table Structure
274
-
275
- Creates these tables automatically:
276
-
277
- ```sql
278
- CREATE TABLE IF NOT EXISTS cf_ai_chat_agent_messages (
279
- id TEXT PRIMARY KEY,
280
- message TEXT NOT NULL,
281
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP
282
- );
283
-
284
- CREATE TABLE IF NOT EXISTS cf_ai_chat_stream_chunks (
285
- id TEXT PRIMARY KEY,
286
- stream_id TEXT NOT NULL,
287
- body TEXT NOT NULL,
288
- chunk_index INTEGER NOT NULL,
289
- created_at INTEGER NOT NULL
290
- );
291
-
292
- CREATE TABLE IF NOT EXISTS cf_ai_chat_stream_metadata (
293
- id TEXT PRIMARY KEY,
294
- request_id TEXT NOT NULL,
295
- status TEXT NOT NULL,
296
- created_at INTEGER NOT NULL,
297
- completed_at INTEGER
298
- );
299
-
300
- CREATE INDEX IF NOT EXISTS idx_stream_chunks_stream_id
301
- ON cf_ai_chat_stream_chunks(stream_id, chunk_index);
302
- ```
303
-
304
- ### Implementation Details
305
-
306
- ```typescript
307
- import { SqlChatAgent } from "@firtoz/chat-agent";
308
-
309
- // Uses Agent's built-in this.sql template tag:
310
- // - this.sql`SELECT * FROM table WHERE id = ${id}`
311
- // - No additional dependencies
312
- // - Tables created automatically in dbInitialize()
161
+ protected getSystemPrompt(): string { /* … */ }
162
+ protected getModel(): string { /* … */ }
163
+ protected getTools(): ToolDefinition[] { /* … */ }
313
164
  ```
314
165
 
315
166
  ## Tools
316
167
 
317
- ### Server-Side Tools
168
+ ### Server-side
318
169
 
319
- Tools with `execute` function run on the server:
170
+ `defineTool` with `execute` runs on the server; results are merged into the conversation.
320
171
 
321
- ```typescript
322
- defineTool({
323
- name: "get_weather",
324
- description: "Get weather for a location",
325
- parameters: {
326
- type: "object",
327
- properties: {
328
- location: { type: "string" }
329
- },
330
- required: ["location"]
331
- },
332
- execute: async (args: { location: string }) => {
333
- // Runs on server, result automatically added to conversation
334
- const weather = await fetchWeather(args.location);
335
- return { temperature: weather.temp, condition: weather.condition };
336
- }
337
- })
338
- ```
172
+ ### Client-side
339
173
 
340
- ### Client-Side Tools
341
-
342
- Tools without `execute` are sent to client for execution:
343
-
344
- ```typescript
345
- defineTool({
346
- name: "get_user_location",
347
- description: "Get user's browser location",
348
- parameters: {
349
- type: "object",
350
- properties: {}
351
- }
352
- // No execute - client handles this via WebSocket
353
- })
354
- ```
174
+ Omit `execute`; tool calls are sent to the client over the WebSocket.
355
175
 
356
- ## Environment Variables
176
+ ## Environment variables
357
177
 
358
178
  ```env
359
- # Required
360
179
  OPENROUTER_API_KEY=sk-or-...
361
-
362
180
  # Optional: Cloudflare AI Gateway
363
- CLOUDFLARE_ACCOUNT_ID=your-account-id
364
- AI_GATEWAY_NAME=your-gateway-name
365
- AI_GATEWAY_TOKEN=your-gateway-token
181
+ CLOUDFLARE_ACCOUNT_ID=…
182
+ AI_GATEWAY_NAME=…
183
+ AI_GATEWAY_TOKEN=…
366
184
  ```
367
185
 
368
- ## API Reference
186
+ ## Types (excerpt)
369
187
 
370
- ### ChatMessage
188
+ `UserMessage`, `AssistantMessage`, `ToolMessage`, `ToolCall`, `ClientMessage`, `ServerMessage`, etc. are exported from this package—import types from `@firtoz/chat-agent` only.
371
189
 
372
- ```typescript
373
- type UserMessage = {
374
- id: string;
375
- role: "user";
376
- content: string;
377
- createdAt: number;
378
- };
190
+ ## Drizzle vs SQL
379
191
 
380
- type AssistantMessage = {
381
- id: string;
382
- role: "assistant";
383
- content: string | null;
384
- toolCalls?: ToolCall[];
385
- createdAt: number;
386
- };
387
-
388
- type ToolMessage = {
389
- id: string;
390
- role: "tool";
391
- toolCallId: string;
392
- content: string;
393
- createdAt: number;
394
- };
395
- ```
396
-
397
- ### ToolDefinition
398
-
399
- ```typescript
400
- type ToolDefinition = {
401
- type: "function";
402
- function: {
403
- name: string;
404
- description?: string;
405
- parameters?: JSONSchema;
406
- strict?: boolean;
407
- };
408
- execute?: (args: any) => unknown | Promise<unknown>;
409
- };
410
- ```
411
-
412
- ## Features
413
-
414
- ### Implemented
415
-
416
- - ✅ Message persistence (Drizzle or SQL)
417
- - ✅ Resumable streaming with chunk buffering
418
- - ✅ Stream restoration on reconnect
419
- - ✅ Request cancellation via AbortController
420
- - ✅ Server-side and client-side tools
421
- - ✅ Tool result handling with auto-continue
422
- - ✅ Cloudflare AI Gateway support
423
- - ✅ DB-agnostic architecture
424
-
425
- ### Comparison: Drizzle vs SQL
426
-
427
- | Feature | DrizzleChatAgent | SqlChatAgent |
428
- |---------|-----------------|--------------|
429
- | Type Safety | ✅ Full type inference | ❌ Template string types only |
430
- | Setup Complexity | ⚠️ Requires migrations | ✅ Auto-creates tables |
431
- | Dependencies | Drizzle ORM + drizzle-kit | None (uses Agent.sql) |
432
- | Query Builder | ✅ Yes | ❌ Raw SQL only |
433
- | Performance | Same | Same |
434
- | Wrangler Config | ⚠️ Requires rules for .sql | ✅ No special config |
435
- | Recommended For | New projects, teams | Quick prototypes, SQL experts |
192
+ | | Drizzle | SQL |
193
+ |---|--------|-----|
194
+ | Package | `@firtoz/chat-agent-drizzle` | `@firtoz/chat-agent-sql` |
195
+ | Type safety | Full schema + query builder | Template SQL |
196
+ | Setup | Migrations + Wrangler `.sql` rules | Auto-creates tables |
197
+ | Best for | New projects | Prototypes / raw SQL |
436
198
 
437
199
  ## License
438
200
 
439
201
  MIT
440
-
441
- ## Contributing
442
-
443
- PRs welcome!