@firtoz/chat-agent 1.0.0 → 2.0.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,71 @@
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
+ Wire protocol (Zod 4), `defineTool`, and abstract **`ChatAgentBase`** for Cloudflare Durable Objects with OpenRoutera simplified alternative to `@cloudflare/ai-chat`.
4
+
5
+ **Persistence is separate:** install one of:
6
+
7
+ - **`@firtoz/chat-agent-drizzle`** — Drizzle ORM (recommended)
8
+ - **`@firtoz/chat-agent-sql`** — raw `this.sql`
9
+
10
+ ## Upgrading from v1
11
+
12
+ v1 shipped `DrizzleChatAgent`, `SqlChatAgent`, and `ChatAgent` (alias) from this package. v2 keeps only protocol + base here.
13
+
14
+ | v1 | v2 |
15
+ |----|-----|
16
+ | `import { DrizzleChatAgent, defineTool } from "@firtoz/chat-agent"` | `import { defineTool } from "@firtoz/chat-agent"` and `import { DrizzleChatAgent } from "@firtoz/chat-agent-drizzle"` |
17
+ | `import { SqlChatAgent } from "@firtoz/chat-agent"` | `import { SqlChatAgent } from "@firtoz/chat-agent-sql"` |
18
+ | `import { … } from "@firtoz/chat-agent/db/schema"` | `import { … } from "@firtoz/chat-agent-drizzle/db/schema"` |
19
+ | `import { ChatAgent } from "@firtoz/chat-agent"` (Drizzle alias) | `import { DrizzleChatAgent } from "@firtoz/chat-agent-drizzle"` (or a local `type ChatAgent = DrizzleChatAgent`) |
20
+
21
+ Add `bun add @firtoz/chat-agent-drizzle` (and `drizzle-orm`) or `bun add @firtoz/chat-agent-sql` as needed.
4
22
 
5
23
  ## Overview
6
24
 
7
- Three classes for different database preferences:
25
+ - **`ChatAgentBase`** Abstract class with chat + streaming + tools + multi-tab broadcast logic
26
+ - **Concrete agents** — `DrizzleChatAgent` (`@firtoz/chat-agent-drizzle`), `SqlChatAgent` (`@firtoz/chat-agent-sql`)
8
27
 
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
28
+ Shared behavior (all implementations):
12
29
 
13
- All implementations share the same API and features:
14
30
  - OpenRouter API integration (simpler than AI SDK)
15
31
  - Resumable streaming with chunk buffering
16
- - Server-side and client-side tool execution
32
+ - **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)
33
+ - Serialized chat turns with batched `toolResult` + `autoContinue`
34
+ - Server-side and client-side tools
17
35
  - Cloudflare AI Gateway support
18
- - Message persistence in SQLite
36
+ - Optional `maxPersistedMessages` and `sanitizeMessageForPersistence()`
37
+ - `waitUntilStable()`, `resetTurnState()`, `hasPendingInteraction()` (subclasses)
38
+ - Tool approval (`needsApproval` → `toolApprovalRequest` / `toolApprovalResponse`)
39
+ - Regenerate / client sync (`sendMessage` with `trigger`, optional `messages`)
40
+ - **Provider metadata** on tool calls for upstream round-trips
41
+ - Wire schemas use **Zod 4** (`zod/v4`)
42
+
43
+ ### Long-running streams (Durable Object keep-alive)
44
+
45
+ Streaming uses Partyserver’s `experimental_waitUntil`. Enable **`enable_ctx_exports`** in `wrangler.jsonc` (required for `experimental_waitUntil` on `Server` / `Agent`).
19
46
 
20
47
  ## Installation
21
48
 
22
49
  ```bash
23
50
  bun add @firtoz/chat-agent @openrouter/sdk agents
51
+ ```
24
52
 
25
- # For Drizzle implementation:
26
- bun add drizzle-orm
53
+ For a runnable Durable Object agent, add a persistence package:
54
+
55
+ ```bash
56
+ # Drizzle (recommended)
57
+ bun add @firtoz/chat-agent-drizzle drizzle-orm
27
58
  bun add -d drizzle-kit
28
- ```
29
59
 
30
- ## Quick Start
60
+ # Or raw SQL only
61
+ bun add @firtoz/chat-agent-sql
62
+ ```
31
63
 
32
- ### Using DrizzleChatAgent (Recommended)
64
+ ## Quick start (Drizzle)
33
65
 
34
66
  ```typescript
35
- import { DrizzleChatAgent, defineTool } from "@firtoz/chat-agent";
36
- import type { AgentContext } from "agents";
67
+ import { defineTool, type ToolDefinition } from "@firtoz/chat-agent";
68
+ import { DrizzleChatAgent } from "@firtoz/chat-agent-drizzle";
37
69
 
38
70
  interface Env {
39
71
  OPENROUTER_API_KEY: string;
@@ -48,14 +80,14 @@ class MyAgent extends DrizzleChatAgent<Env> {
48
80
  return "anthropic/claude-sonnet-4.5";
49
81
  }
50
82
 
51
- protected override getTools() {
83
+ protected override getTools(): ToolDefinition[] {
52
84
  return [
53
85
  defineTool({
54
86
  name: "get_time",
55
87
  description: "Get current time",
56
88
  parameters: { type: "object", properties: {} },
57
- execute: async () => ({ time: new Date().toISOString() })
58
- })
89
+ execute: async () => ({ time: new Date().toISOString() }),
90
+ }),
59
91
  ];
60
92
  }
61
93
  }
@@ -63,381 +95,99 @@ class MyAgent extends DrizzleChatAgent<Env> {
63
95
  export { MyAgent };
64
96
  ```
65
97
 
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
98
+ See **`@firtoz/chat-agent-drizzle`** for Wrangler rules, migrations, and `drizzle.config.ts`.
84
99
 
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:
100
+ ## Quick start (SQL)
107
101
 
108
102
  ```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
- ```
118
-
119
- Or if you're in the chat-agent package itself:
120
-
121
- ```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
- ```
103
+ import { defineTool } from "@firtoz/chat-agent";
104
+ import { SqlChatAgent } from "@firtoz/chat-agent-sql";
131
105
 
132
- #### 3. Add Script to package.json
133
-
134
- ```json
135
- {
136
- "scripts": {
137
- "db:generate": "bunx drizzle-kit generate"
138
- }
106
+ class MyAgent extends SqlChatAgent<Env> {
107
+ // Same overrides as Drizzle; tables are created in dbInitialize()
139
108
  }
140
109
  ```
141
110
 
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
111
+ ## ChatAgentBase (abstract)
152
112
 
153
- The migrations are automatically run when `DrizzleChatAgent` initializes.
113
+ Subclasses must implement the database interface (see Drizzle/SQL packages for examples).
154
114
 
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.
162
-
163
- ### Abstract Methods
164
-
165
- Subclasses must implement these database operations:
115
+ ### Abstract methods
166
116
 
167
117
  ```typescript
168
- // Initialization
169
118
  protected abstract dbInitialize(): void;
170
-
171
- // Messages
172
119
  protected abstract dbLoadMessages(): ChatMessage[];
173
120
  protected abstract dbSaveMessage(msg: ChatMessage): void;
174
121
  protected abstract dbClearAll(): void;
175
-
176
- // Stream metadata
177
- protected abstract dbFindActiveStream(): {
178
- id: string;
179
- messageId: string;
180
- createdAt: Date
122
+ protected abstract dbFindActiveStream(): {
123
+ id: string;
124
+ messageId: string;
125
+ createdAt: Date;
181
126
  } | null;
182
127
  protected abstract dbDeleteStreamWithChunks(streamId: string): void;
183
128
  protected abstract dbInsertStreamMetadata(streamId: string, messageId: string): void;
184
- protected abstract dbUpdateStreamStatus(streamId: string, status: 'completed' | 'error'): void;
129
+ protected abstract dbUpdateStreamStatus(
130
+ streamId: string,
131
+ status: "completed" | "error",
132
+ ): void;
185
133
  protected abstract dbDeleteOldCompletedStreams(cutoffMs: number): void;
186
-
187
- // Stream chunks
188
134
  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;
135
+ protected abstract dbInsertChunks(
136
+ chunks: Array<{
137
+ id: string;
138
+ streamId: string;
139
+ content: string;
140
+ chunkIndex: number;
141
+ }>,
142
+ ): void;
195
143
  protected abstract dbGetChunks(streamId: string): string[];
196
144
  protected abstract dbDeleteChunks(streamId: string): void;
145
+ protected abstract dbIsStreamKnown(streamId: string): boolean;
146
+ protected abstract dbReplaceAllMessages(messages: ChatMessage[]): void;
147
+ protected abstract dbTrimMessagesToMax(maxMessages: number): void;
197
148
  ```
198
149
 
199
- ### Override Methods
200
-
201
- Customize your agent's behavior:
150
+ ### Common overrides
202
151
 
203
152
  ```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()
153
+ protected getSystemPrompt(): string { /* … */ }
154
+ protected getModel(): string { /* … */ }
155
+ protected getTools(): ToolDefinition[] { /* … */ }
313
156
  ```
314
157
 
315
158
  ## Tools
316
159
 
317
- ### Server-Side Tools
160
+ ### Server-side
318
161
 
319
- Tools with `execute` function run on the server:
162
+ `defineTool` with `execute` runs on the server; results are merged into the conversation.
320
163
 
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
- ```
164
+ ### Client-side
339
165
 
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
- ```
166
+ Omit `execute`; tool calls are sent to the client over the WebSocket.
355
167
 
356
- ## Environment Variables
168
+ ## Environment variables
357
169
 
358
170
  ```env
359
- # Required
360
171
  OPENROUTER_API_KEY=sk-or-...
361
-
362
172
  # 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
173
+ CLOUDFLARE_ACCOUNT_ID=…
174
+ AI_GATEWAY_NAME=…
175
+ AI_GATEWAY_TOKEN=…
366
176
  ```
367
177
 
368
- ## API Reference
178
+ ## Types (excerpt)
369
179
 
370
- ### ChatMessage
180
+ `UserMessage`, `AssistantMessage`, `ToolMessage`, `ToolCall`, `ClientMessage`, `ServerMessage`, etc. are exported from this package—import types from `@firtoz/chat-agent` only.
371
181
 
372
- ```typescript
373
- type UserMessage = {
374
- id: string;
375
- role: "user";
376
- content: string;
377
- createdAt: number;
378
- };
182
+ ## Drizzle vs SQL
379
183
 
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 |
184
+ | | Drizzle | SQL |
185
+ |---|--------|-----|
186
+ | Package | `@firtoz/chat-agent-drizzle` | `@firtoz/chat-agent-sql` |
187
+ | Type safety | Full schema + query builder | Template SQL |
188
+ | Setup | Migrations + Wrangler `.sql` rules | Auto-creates tables |
189
+ | Best for | New projects | Prototypes / raw SQL |
436
190
 
437
191
  ## License
438
192
 
439
193
  MIT
440
-
441
- ## Contributing
442
-
443
- PRs welcome!
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@firtoz/chat-agent",
3
- "version": "1.0.0",
4
- "description": "ChatAgent for Cloudflare Durable Objects with OpenRouter - a simplified alternative to @cloudflare/ai-chat",
3
+ "version": "2.0.0",
4
+ "description": "Wire protocol, tools, and ChatAgentBase for Cloudflare Durable Objects with OpenRouter (use @firtoz/chat-agent-drizzle or @firtoz/chat-agent-sql for persistence)",
5
5
  "main": "./src/index.ts",
6
6
  "module": "./src/index.ts",
7
7
  "types": "./src/index.ts",
@@ -10,11 +10,6 @@
10
10
  "types": "./src/index.ts",
11
11
  "import": "./src/index.ts",
12
12
  "require": "./src/index.ts"
13
- },
14
- "./db/schema": {
15
- "types": "./src/db/schema.ts",
16
- "import": "./src/db/schema.ts",
17
- "require": "./src/db/schema.ts"
18
13
  }
19
14
  },
20
15
  "files": [
@@ -26,7 +21,8 @@
26
21
  "typecheck": "tsc --noEmit -p ./tsconfig.json",
27
22
  "lint": "biome check --write src",
28
23
  "lint:ci": "biome ci src",
29
- "db:generate": "drizzle-kit generate",
24
+ "test": "bun test",
25
+ "test:watch": "bun test --watch",
30
26
  "format": "biome format src --write"
31
27
  },
32
28
  "keywords": [
@@ -37,7 +33,6 @@
37
33
  "chat",
38
34
  "ai",
39
35
  "openrouter",
40
- "drizzle-orm",
41
36
  "agents"
42
37
  ],
43
38
  "author": "Firtina Ozbalikchi <firtoz@github.com>",
@@ -52,13 +47,9 @@
52
47
  "url": "https://github.com/firtoz/fullstack-toolkit/issues"
53
48
  },
54
49
  "peerDependencies": {
55
- "@cloudflare/workers-types": "^4.20260118.0",
56
- "@openrouter/sdk": "^0.3.12",
57
- "agents": "^0.3.4",
58
- "drizzle-orm": "^0.45.1"
59
- },
60
- "optionalDependencies": {
61
- "zod": "^4.3.5"
50
+ "@cloudflare/workers-types": "^4.20260317.1",
51
+ "@openrouter/sdk": "^0.9.11",
52
+ "agents": "^0.8.1"
62
53
  },
63
54
  "engines": {
64
55
  "node": ">=18.0.0"
@@ -67,8 +58,11 @@
67
58
  "access": "public"
68
59
  },
69
60
  "devDependencies": {
70
- "bun-types": "^1.3.6",
71
- "drizzle-kit": "^0.31.8",
72
- "typescript": "^5.9.3"
61
+ "bun-types": "^1.3.11",
62
+ "typescript": "^6.0.2"
63
+ },
64
+ "dependencies": {
65
+ "@firtoz/maybe-error": "^1.5.2",
66
+ "zod": "^4.3.6"
73
67
  }
74
68
  }