@firtoz/chat-agent 1.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.
@@ -0,0 +1,472 @@
1
+ import { z } from "zod";
2
+
3
+ // ============================================================================
4
+ // Tool Definitions (for sending to OpenRouter)
5
+ // ============================================================================
6
+
7
+ /**
8
+ * JSON Schema for tool parameters (subset of JSON Schema 7)
9
+ */
10
+ export const JSONSchemaSchema = z.record(z.string(), z.unknown());
11
+
12
+ export type JSONSchema = z.infer<typeof JSONSchemaSchema>;
13
+
14
+ /**
15
+ * Tool definition following OpenAI/OpenRouter format
16
+ * The schema is for wire format (no execute function)
17
+ */
18
+ export const ToolDefinitionSchema = z.object({
19
+ type: z.literal("function"),
20
+ function: z.object({
21
+ name: z.string(),
22
+ description: z.string().optional(),
23
+ parameters: JSONSchemaSchema.optional(),
24
+ strict: z.boolean().optional(),
25
+ }),
26
+ });
27
+
28
+ /**
29
+ * Execute function signature for server-side tools
30
+ * Takes parsed arguments, returns JSON-serializable result
31
+ */
32
+ // biome-ignore lint/suspicious/noExplicitAny: Tool execute functions need flexible typing
33
+ export type ToolExecuteFunction = (args: any) => unknown | Promise<unknown>;
34
+
35
+ /**
36
+ * Tool definition with optional execute function for server-side execution
37
+ * - If `execute` is provided: server runs it automatically and continues
38
+ * - If `execute` is omitted: tool call is sent to client for execution
39
+ */
40
+ export type ToolDefinition = z.infer<typeof ToolDefinitionSchema> & {
41
+ /** Optional server-side execute function. If omitted, tool call goes to client. */
42
+ execute?: ToolExecuteFunction;
43
+ };
44
+
45
+ // ============================================================================
46
+ // Tool Calls (from AI responses)
47
+ // ============================================================================
48
+
49
+ /**
50
+ * A tool call from the AI (complete, after streaming)
51
+ */
52
+ export const ToolCallSchema = z.object({
53
+ id: z.string(),
54
+ type: z.literal("function"),
55
+ function: z.object({
56
+ name: z.string(),
57
+ arguments: z.string(), // JSON string
58
+ }),
59
+ });
60
+
61
+ export type ToolCall = z.infer<typeof ToolCallSchema>;
62
+
63
+ /**
64
+ * Tool call delta during streaming
65
+ */
66
+ export const ToolCallDeltaSchema = z.object({
67
+ index: z.number(),
68
+ id: z.string().optional(),
69
+ type: z.literal("function").optional(),
70
+ function: z
71
+ .object({
72
+ name: z.string().optional(),
73
+ arguments: z.string().optional(),
74
+ })
75
+ .optional(),
76
+ });
77
+
78
+ export type ToolCallDelta = z.infer<typeof ToolCallDeltaSchema>;
79
+
80
+ // ============================================================================
81
+ // Tool Results (from client execution)
82
+ // ============================================================================
83
+
84
+ /**
85
+ * Tool result from client-side execution
86
+ */
87
+ export const ToolResultSchema = z.object({
88
+ toolCallId: z.string(),
89
+ output: z.unknown(), // Can be any JSON-serializable value
90
+ });
91
+
92
+ export type ToolResult = z.infer<typeof ToolResultSchema>;
93
+
94
+ // ============================================================================
95
+ // Chat Message Schema (supports text + tool calls)
96
+ // ============================================================================
97
+
98
+ /**
99
+ * User message
100
+ */
101
+ export const UserMessageSchema = z.object({
102
+ id: z.string(),
103
+ role: z.literal("user"),
104
+ content: z.string(),
105
+ createdAt: z.number(),
106
+ });
107
+
108
+ export type UserMessage = z.infer<typeof UserMessageSchema>;
109
+
110
+ /**
111
+ * Assistant message - can have content, tool calls, or both
112
+ */
113
+ export const AssistantMessageSchema = z.object({
114
+ id: z.string(),
115
+ role: z.literal("assistant"),
116
+ content: z.string().nullable(), // null when only tool calls
117
+ toolCalls: z.array(ToolCallSchema).optional(),
118
+ createdAt: z.number(),
119
+ });
120
+
121
+ export type AssistantMessage = z.infer<typeof AssistantMessageSchema>;
122
+
123
+ /**
124
+ * Tool response message (sent back to AI after tool execution)
125
+ */
126
+ export const ToolMessageSchema = z.object({
127
+ id: z.string(),
128
+ role: z.literal("tool"),
129
+ toolCallId: z.string(),
130
+ content: z.string(), // JSON stringified result
131
+ createdAt: z.number(),
132
+ });
133
+
134
+ export type ToolMessage = z.infer<typeof ToolMessageSchema>;
135
+
136
+ /**
137
+ * Union of all chat message types
138
+ */
139
+ export const ChatMessageSchema = z.discriminatedUnion("role", [
140
+ UserMessageSchema,
141
+ AssistantMessageSchema,
142
+ ToolMessageSchema,
143
+ ]);
144
+
145
+ export type ChatMessage = z.infer<typeof ChatMessageSchema>;
146
+
147
+ // ============================================================================
148
+ // Token Usage Schema
149
+ // ============================================================================
150
+
151
+ export const TokenUsageSchema = z.object({
152
+ prompt_tokens: z.number(),
153
+ completion_tokens: z.number(),
154
+ total_tokens: z.number(),
155
+ });
156
+
157
+ export type TokenUsage = z.infer<typeof TokenUsageSchema>;
158
+
159
+ // ============================================================================
160
+ // Client → Server Messages (Discriminated Union)
161
+ // ============================================================================
162
+
163
+ export const ClientMessageSchema = z.discriminatedUnion("type", [
164
+ z.object({
165
+ type: z.literal("sendMessage"),
166
+ content: z.string(),
167
+ }),
168
+ z.object({
169
+ type: z.literal("clearHistory"),
170
+ }),
171
+ z.object({
172
+ type: z.literal("getHistory"),
173
+ }),
174
+ z.object({
175
+ type: z.literal("resumeStream"),
176
+ streamId: z.string(),
177
+ }),
178
+ z.object({
179
+ type: z.literal("cancelRequest"),
180
+ id: z.string(),
181
+ }),
182
+ // Tool result from client-side tool execution
183
+ z.object({
184
+ type: z.literal("toolResult"),
185
+ toolCallId: z.string(),
186
+ toolName: z.string(),
187
+ output: z.unknown(),
188
+ // If true, server should continue the conversation after tool result
189
+ autoContinue: z.boolean().optional(),
190
+ }),
191
+ // Register client-defined tools at runtime
192
+ z.object({
193
+ type: z.literal("registerTools"),
194
+ tools: z.array(
195
+ z.object({
196
+ name: z.string(),
197
+ description: z.string().optional(),
198
+ parameters: JSONSchemaSchema.optional(),
199
+ }),
200
+ ),
201
+ }),
202
+ ]);
203
+
204
+ export type ClientMessage = z.infer<typeof ClientMessageSchema>;
205
+
206
+ // Individual message types for convenience
207
+ export type SendMessagePayload = Extract<
208
+ ClientMessage,
209
+ { type: "sendMessage" }
210
+ >;
211
+ export type ClearHistoryPayload = Extract<
212
+ ClientMessage,
213
+ { type: "clearHistory" }
214
+ >;
215
+ export type GetHistoryPayload = Extract<ClientMessage, { type: "getHistory" }>;
216
+ export type ResumeStreamPayload = Extract<
217
+ ClientMessage,
218
+ { type: "resumeStream" }
219
+ >;
220
+ export type CancelRequestPayload = Extract<
221
+ ClientMessage,
222
+ { type: "cancelRequest" }
223
+ >;
224
+ export type ToolResultPayload = Extract<ClientMessage, { type: "toolResult" }>;
225
+ export type RegisterToolsPayload = Extract<
226
+ ClientMessage,
227
+ { type: "registerTools" }
228
+ >;
229
+
230
+ // ============================================================================
231
+ // Server → Client Messages (Discriminated Union)
232
+ // ============================================================================
233
+
234
+ export const ServerMessageSchema = z.discriminatedUnion("type", [
235
+ // Full message history
236
+ z.object({
237
+ type: z.literal("history"),
238
+ messages: z.array(ChatMessageSchema),
239
+ }),
240
+ // Stream start
241
+ z.object({
242
+ type: z.literal("messageStart"),
243
+ id: z.string(),
244
+ streamId: z.string(),
245
+ }),
246
+ // Text content chunk
247
+ z.object({
248
+ type: z.literal("messageChunk"),
249
+ id: z.string(),
250
+ chunk: z.string(),
251
+ }),
252
+ // Tool call streaming delta
253
+ z.object({
254
+ type: z.literal("toolCallDelta"),
255
+ id: z.string(),
256
+ delta: ToolCallDeltaSchema,
257
+ }),
258
+ // Tool call complete (full tool call ready for execution)
259
+ z.object({
260
+ type: z.literal("toolCall"),
261
+ id: z.string(), // message id
262
+ toolCall: ToolCallSchema,
263
+ }),
264
+ // Stream end
265
+ z.object({
266
+ type: z.literal("messageEnd"),
267
+ id: z.string(),
268
+ // Final message state (with tool calls if any)
269
+ toolCalls: z.array(ToolCallSchema).optional(),
270
+ createdAt: z.number(),
271
+ usage: TokenUsageSchema.optional(),
272
+ }),
273
+ // Stream resumption
274
+ z.object({
275
+ type: z.literal("streamResume"),
276
+ streamId: z.string(),
277
+ chunks: z.array(z.string()),
278
+ done: z.boolean(),
279
+ }),
280
+ z.object({
281
+ type: z.literal("streamResuming"),
282
+ id: z.string(),
283
+ streamId: z.string(),
284
+ }),
285
+ // Message updated (e.g., tool result applied)
286
+ z.object({
287
+ type: z.literal("messageUpdated"),
288
+ message: ChatMessageSchema,
289
+ }),
290
+ // General error
291
+ z.object({
292
+ type: z.literal("error"),
293
+ message: z.string(),
294
+ }),
295
+ // Tool input error (arguments validation failed)
296
+ z.object({
297
+ type: z.literal("toolError"),
298
+ errorType: z.enum(["input", "output", "not_found"]),
299
+ toolCallId: z.string(),
300
+ toolName: z.string(),
301
+ message: z.string(),
302
+ }),
303
+ ]);
304
+
305
+ export type ServerMessage = z.infer<typeof ServerMessageSchema>;
306
+
307
+ // Individual message types for convenience
308
+ export type HistoryMessage = Extract<ServerMessage, { type: "history" }>;
309
+ export type MessageStartMessage = Extract<
310
+ ServerMessage,
311
+ { type: "messageStart" }
312
+ >;
313
+ export type MessageChunkMessage = Extract<
314
+ ServerMessage,
315
+ { type: "messageChunk" }
316
+ >;
317
+ export type ToolCallDeltaMessage = Extract<
318
+ ServerMessage,
319
+ { type: "toolCallDelta" }
320
+ >;
321
+ export type ToolCallMessage = Extract<ServerMessage, { type: "toolCall" }>;
322
+ export type MessageEndMessage = Extract<ServerMessage, { type: "messageEnd" }>;
323
+ export type StreamResumeMessage = Extract<
324
+ ServerMessage,
325
+ { type: "streamResume" }
326
+ >;
327
+ export type StreamResumingMessage = Extract<
328
+ ServerMessage,
329
+ { type: "streamResuming" }
330
+ >;
331
+ export type MessageUpdatedMessage = Extract<
332
+ ServerMessage,
333
+ { type: "messageUpdated" }
334
+ >;
335
+ export type ErrorMessage = Extract<ServerMessage, { type: "error" }>;
336
+ export type ToolErrorMessage = Extract<ServerMessage, { type: "toolError" }>;
337
+
338
+ // ============================================================================
339
+ // Parsing Helpers
340
+ // ============================================================================
341
+
342
+ /**
343
+ * Parse and validate a client message from JSON string
344
+ * @throws ZodError if validation fails
345
+ */
346
+ export function parseClientMessage(json: string): ClientMessage {
347
+ const data = JSON.parse(json);
348
+ return ClientMessageSchema.parse(data);
349
+ }
350
+
351
+ /**
352
+ * Safely parse a client message, returning null on failure
353
+ */
354
+ export function safeParseClientMessage(json: string): ClientMessage | null {
355
+ try {
356
+ const data = JSON.parse(json);
357
+ const result = ClientMessageSchema.safeParse(data);
358
+ return result.success ? result.data : null;
359
+ } catch {
360
+ return null;
361
+ }
362
+ }
363
+
364
+ /**
365
+ * Parse and validate a server message from JSON string
366
+ * @throws ZodError if validation fails
367
+ */
368
+ export function parseServerMessage(json: string): ServerMessage {
369
+ const data = JSON.parse(json);
370
+ return ServerMessageSchema.parse(data);
371
+ }
372
+
373
+ /**
374
+ * Safely parse a server message, returning null on failure
375
+ */
376
+ export function safeParseServerMessage(json: string): ServerMessage | null {
377
+ try {
378
+ const data = JSON.parse(json);
379
+ const result = ServerMessageSchema.safeParse(data);
380
+ return result.success ? result.data : null;
381
+ } catch {
382
+ return null;
383
+ }
384
+ }
385
+
386
+ // ============================================================================
387
+ // Type Guards
388
+ // ============================================================================
389
+
390
+ export function isClientMessage(data: unknown): data is ClientMessage {
391
+ return ClientMessageSchema.safeParse(data).success;
392
+ }
393
+
394
+ export function isServerMessage(data: unknown): data is ServerMessage {
395
+ return ServerMessageSchema.safeParse(data).success;
396
+ }
397
+
398
+ export function isUserMessage(msg: ChatMessage): msg is UserMessage {
399
+ return msg.role === "user";
400
+ }
401
+
402
+ export function isAssistantMessage(msg: ChatMessage): msg is AssistantMessage {
403
+ return msg.role === "assistant";
404
+ }
405
+
406
+ export function isToolMessage(msg: ChatMessage): msg is ToolMessage {
407
+ return msg.role === "tool";
408
+ }
409
+
410
+ export function hasToolCalls(msg: AssistantMessage): boolean {
411
+ return !!msg.toolCalls && msg.toolCalls.length > 0;
412
+ }
413
+
414
+ // ============================================================================
415
+ // Helper Functions
416
+ // ============================================================================
417
+
418
+ /**
419
+ * Parse tool call arguments from JSON string
420
+ */
421
+ export function parseToolArguments<T = unknown>(toolCall: ToolCall): T {
422
+ return JSON.parse(toolCall.function.arguments) as T;
423
+ }
424
+
425
+ /**
426
+ * Create a tool definition helper
427
+ *
428
+ * @example Server-side tool (executes automatically on server)
429
+ * ```ts
430
+ * defineTool({
431
+ * name: "get_weather",
432
+ * description: "Get current weather",
433
+ * parameters: { type: "object", properties: { location: { type: "string" } } },
434
+ * execute: async (args) => {
435
+ * const weather = await fetchWeather(args.location);
436
+ * return { temp: weather.temp, conditions: weather.desc };
437
+ * }
438
+ * })
439
+ * ```
440
+ *
441
+ * @example Client-side tool (sent to client for execution)
442
+ * ```ts
443
+ * defineTool({
444
+ * name: "get_user_location",
445
+ * description: "Get user's current location",
446
+ * parameters: { type: "object", properties: {} },
447
+ * // No execute function - client handles this
448
+ * })
449
+ * ```
450
+ */
451
+ export function defineTool(config: {
452
+ name: string;
453
+ description?: string;
454
+ parameters?: JSONSchema;
455
+ strict?: boolean;
456
+ /** Server-side execute function. If omitted, tool call goes to client. */
457
+ execute?: ToolExecuteFunction;
458
+ }): ToolDefinition {
459
+ const tool: ToolDefinition = {
460
+ type: "function",
461
+ function: {
462
+ name: config.name,
463
+ description: config.description,
464
+ parameters: config.parameters,
465
+ strict: config.strict,
466
+ },
467
+ };
468
+ if (config.execute) {
469
+ tool.execute = config.execute;
470
+ }
471
+ return tool;
472
+ }
@@ -0,0 +1,21 @@
1
+ import {
2
+ type DrizzleSqliteDODatabase,
3
+ drizzle,
4
+ } from "drizzle-orm/durable-sqlite";
5
+ import * as schema from "./schema";
6
+
7
+ export * from "./schema";
8
+
9
+ /**
10
+ * Creates a Drizzle instance for Durable Object SQLite storage
11
+ * Uses the native drizzle-orm/durable-sqlite driver
12
+ *
13
+ * @see https://orm.drizzle.team/docs/connect-cloudflare-do
14
+ */
15
+ export function createDb(
16
+ storage: DurableObjectStorage,
17
+ ): DrizzleSqliteDODatabase<typeof schema> {
18
+ return drizzle(storage, { schema, logger: false });
19
+ }
20
+
21
+ export type Database = DrizzleSqliteDODatabase<typeof schema>;
@@ -0,0 +1,47 @@
1
+ import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
2
+
3
+ /**
4
+ * Chat messages table
5
+ * Stores the full conversation history for each agent session
6
+ * Messages are stored as JSON to support complex structures (tool calls, etc.)
7
+ */
8
+ export const messagesTable = sqliteTable("messages", {
9
+ id: text("id").primaryKey(),
10
+ role: text("role", { enum: ["user", "assistant", "tool"] }).notNull(),
11
+ // JSON-serialized message data (content, toolCalls, toolCallId, etc.)
12
+ messageJson: text("message_json").notNull(),
13
+ createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull(),
14
+ });
15
+
16
+ /**
17
+ * Stream chunks table for resumable streaming
18
+ * Buffers streamed content so clients can resume mid-stream
19
+ */
20
+ export const streamChunksTable = sqliteTable("stream_chunks", {
21
+ id: text("id").primaryKey(),
22
+ streamId: text("stream_id").notNull(),
23
+ content: text("content").notNull(),
24
+ chunkIndex: integer("chunk_index").notNull(),
25
+ createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull(),
26
+ });
27
+
28
+ /**
29
+ * Stream metadata table for tracking active/completed streams
30
+ */
31
+ export const streamMetadataTable = sqliteTable("stream_metadata", {
32
+ id: text("id").primaryKey(),
33
+ messageId: text("message_id").notNull(), // The assistant message being streamed
34
+ status: text("status", {
35
+ enum: ["streaming", "completed", "error"],
36
+ }).notNull(),
37
+ createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull(),
38
+ completedAt: integer("completed_at", { mode: "timestamp_ms" }),
39
+ });
40
+
41
+ // Type exports for use in the agent
42
+ export type Message = typeof messagesTable.$inferSelect;
43
+ export type NewMessage = typeof messagesTable.$inferInsert;
44
+ export type StreamChunk = typeof streamChunksTable.$inferSelect;
45
+ export type NewStreamChunk = typeof streamChunksTable.$inferInsert;
46
+ export type StreamMeta = typeof streamMetadataTable.$inferSelect;
47
+ export type NewStreamMeta = typeof streamMetadataTable.$inferInsert;
package/src/index.ts ADDED
@@ -0,0 +1,99 @@
1
+ /**
2
+ * @firtoz/chat-agent - ChatAgent for Cloudflare Durable Objects with OpenRouter
3
+ *
4
+ * A simplified alternative to @cloudflare/ai-chat's AIChatAgent that uses OpenRouter
5
+ * directly instead of Vercel AI SDK.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * import { ChatAgent, defineTool, type ToolDefinition } from "@firtoz/chat-agent";
10
+ *
11
+ * class MyAgent extends ChatAgent {
12
+ * protected override getSystemPrompt(): string {
13
+ * return "You are a helpful assistant.";
14
+ * }
15
+ *
16
+ * protected override getModel(): string {
17
+ * return "anthropic/claude-sonnet-4.5";
18
+ * }
19
+ *
20
+ * protected override getTools(): ToolDefinition[] {
21
+ * return [
22
+ * defineTool({
23
+ * name: "get_time",
24
+ * description: "Get current time",
25
+ * parameters: { type: "object", properties: {} },
26
+ * execute: async () => ({ time: new Date().toISOString() })
27
+ * })
28
+ * ];
29
+ * }
30
+ * }
31
+ * ```
32
+ */
33
+
34
+ // Export the abstract base class
35
+ export { ChatAgentBase } from "./chat-agent-base";
36
+
37
+ // Export concrete implementations
38
+ export { DrizzleChatAgent } from "./chat-agent-drizzle";
39
+ export { SqlChatAgent } from "./chat-agent-sql";
40
+
41
+ // Alias DrizzleChatAgent as ChatAgent for convenience (Drizzle is recommended)
42
+ export { DrizzleChatAgent as ChatAgent } from "./chat-agent-drizzle";
43
+
44
+ // Re-export all types and utilities from chat-messages
45
+ export type {
46
+ // Message types
47
+ AssistantMessage,
48
+ ChatMessage,
49
+ UserMessage,
50
+ ToolMessage,
51
+ // Tool types
52
+ ToolCall,
53
+ ToolCallDelta,
54
+ ToolDefinition,
55
+ ToolResult,
56
+ JSONSchema,
57
+ // Client/Server message types
58
+ ClientMessage,
59
+ ServerMessage,
60
+ SendMessagePayload,
61
+ ClearHistoryPayload,
62
+ GetHistoryPayload,
63
+ ResumeStreamPayload,
64
+ CancelRequestPayload,
65
+ ToolResultPayload,
66
+ RegisterToolsPayload,
67
+ HistoryMessage,
68
+ MessageStartMessage,
69
+ MessageChunkMessage,
70
+ ToolCallDeltaMessage,
71
+ ToolCallMessage,
72
+ MessageEndMessage,
73
+ StreamResumeMessage,
74
+ StreamResumingMessage,
75
+ MessageUpdatedMessage,
76
+ ErrorMessage,
77
+ ToolErrorMessage,
78
+ // Usage types
79
+ TokenUsage,
80
+ } from "./chat-messages";
81
+
82
+ export {
83
+ // Tool definition helper
84
+ defineTool,
85
+ // Parsing helpers
86
+ parseClientMessage,
87
+ safeParseClientMessage,
88
+ parseServerMessage,
89
+ safeParseServerMessage,
90
+ // Type guards
91
+ isClientMessage,
92
+ isServerMessage,
93
+ isUserMessage,
94
+ isAssistantMessage,
95
+ isToolMessage,
96
+ hasToolCalls,
97
+ // Helper functions
98
+ parseToolArguments,
99
+ } from "./chat-messages";