@falai/agent 0.5.1 → 0.5.4

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.
Files changed (60) hide show
  1. package/README.md +11 -0
  2. package/dist/cjs/core/ResponseEngine.d.ts.map +1 -1
  3. package/dist/cjs/core/ResponseEngine.js +5 -3
  4. package/dist/cjs/core/ResponseEngine.js.map +1 -1
  5. package/dist/cjs/core/State.js +2 -1
  6. package/dist/cjs/core/State.js.map +1 -1
  7. package/dist/cjs/providers/GeminiProvider.d.ts +15 -0
  8. package/dist/cjs/providers/GeminiProvider.d.ts.map +1 -1
  9. package/dist/cjs/providers/GeminiProvider.js +89 -4
  10. package/dist/cjs/providers/GeminiProvider.js.map +1 -1
  11. package/dist/cjs/providers/OpenAIProvider.d.ts +7 -0
  12. package/dist/cjs/providers/OpenAIProvider.d.ts.map +1 -1
  13. package/dist/cjs/providers/OpenAIProvider.js +13 -1
  14. package/dist/cjs/providers/OpenAIProvider.js.map +1 -1
  15. package/dist/cjs/providers/OpenRouterProvider.d.ts +7 -0
  16. package/dist/cjs/providers/OpenRouterProvider.d.ts.map +1 -1
  17. package/dist/cjs/providers/OpenRouterProvider.js +13 -1
  18. package/dist/cjs/providers/OpenRouterProvider.js.map +1 -1
  19. package/dist/cjs/types/route.d.ts +2 -0
  20. package/dist/cjs/types/route.d.ts.map +1 -1
  21. package/dist/core/ResponseEngine.d.ts.map +1 -1
  22. package/dist/core/ResponseEngine.js +5 -3
  23. package/dist/core/ResponseEngine.js.map +1 -1
  24. package/dist/core/State.js +2 -1
  25. package/dist/core/State.js.map +1 -1
  26. package/dist/providers/GeminiProvider.d.ts +15 -0
  27. package/dist/providers/GeminiProvider.d.ts.map +1 -1
  28. package/dist/providers/GeminiProvider.js +90 -5
  29. package/dist/providers/GeminiProvider.js.map +1 -1
  30. package/dist/providers/OpenAIProvider.d.ts +7 -0
  31. package/dist/providers/OpenAIProvider.d.ts.map +1 -1
  32. package/dist/providers/OpenAIProvider.js +13 -1
  33. package/dist/providers/OpenAIProvider.js.map +1 -1
  34. package/dist/providers/OpenRouterProvider.d.ts +7 -0
  35. package/dist/providers/OpenRouterProvider.d.ts.map +1 -1
  36. package/dist/providers/OpenRouterProvider.js +13 -1
  37. package/dist/providers/OpenRouterProvider.js.map +1 -1
  38. package/dist/types/route.d.ts +2 -0
  39. package/dist/types/route.d.ts.map +1 -1
  40. package/docs/API_REFERENCE.md +83 -13
  41. package/docs/ARCHITECTURE.md +48 -1
  42. package/docs/PERSISTENCE.md +18 -6
  43. package/examples/custom-database-persistence.ts +533 -0
  44. package/examples/prisma-persistence.ts +13 -2
  45. package/package.json +1 -1
  46. package/src/core/ResponseEngine.ts +6 -3
  47. package/src/core/State.ts +1 -1
  48. package/src/providers/GeminiProvider.ts +114 -5
  49. package/src/providers/OpenAIProvider.ts +17 -1
  50. package/src/providers/OpenRouterProvider.ts +19 -1
  51. package/src/types/route.ts +2 -0
  52. package/dist/cjs/utils/schema.d.ts +0 -17
  53. package/dist/cjs/utils/schema.d.ts.map +0 -1
  54. package/dist/cjs/utils/schema.js +0 -32
  55. package/dist/cjs/utils/schema.js.map +0 -1
  56. package/dist/utils/schema.d.ts +0 -17
  57. package/dist/utils/schema.d.ts.map +0 -1
  58. package/dist/utils/schema.js +0 -27
  59. package/dist/utils/schema.js.map +0 -1
  60. package/src/utils/schema.ts +0 -32
@@ -121,11 +121,18 @@ const bookingRoute = agent.createRoute<BookingData>({
121
121
  },
122
122
  });
123
123
 
124
- // Define states with smart data gathering
125
- bookingRoute.initialState.transitionTo({
126
- chatState: "Collect booking details",
127
- gather: ["destination", "date", "passengers"],
128
- });
124
+ // Define states with smart data gathering and custom IDs
125
+ bookingRoute.initialState
126
+ .transitionTo({
127
+ id: "collect_details", // ✅ Custom state ID for easier tracking
128
+ chatState: "Collect booking details",
129
+ gather: ["destination", "date", "passengers"],
130
+ })
131
+ .transitionTo({
132
+ id: "confirm_booking", // ✅ Custom state ID
133
+ chatState: "Confirm all details",
134
+ requiredData: ["destination", "date", "passengers"],
135
+ });
129
136
 
130
137
  // Access persistence methods
131
138
  const persistence = agent.getPersistenceManager();
@@ -137,17 +144,22 @@ const { sessionData, sessionState } =
137
144
  agentName: "My Agent",
138
145
  });
139
146
 
147
+ // Session ID is automatically set in metadata
148
+ console.log("Session ID:", sessionState.metadata?.sessionId);
149
+ // Outputs: sessionData.id (e.g., "cuid_abc123")
150
+
140
151
  // Load history
141
152
  const history = await persistence.loadSessionHistory(sessionData.id);
142
153
 
143
154
  // Generate response with session state
144
155
  const response = await agent.respond({
145
156
  history,
146
- session: sessionState, // Pass session state
157
+ session: sessionState, // Pass session state with ID
147
158
  });
148
159
 
149
160
  // Session state is auto-saved! ✨
150
161
  console.log("Extracted data:", response.session?.extracted);
162
+ console.log("Current state ID:", response.session?.currentState?.id); // Custom or auto-generated ID
151
163
 
152
164
  // Save message
153
165
  await persistence.saveMessage({
@@ -0,0 +1,533 @@
1
+ /**
2
+ * Example: Custom Database Integration (Manual Session State Management)
3
+ *
4
+ * This example shows how to manually manage session state when using your own
5
+ * database structure instead of the built-in persistence adapters.
6
+ *
7
+ * Use this approach if you:
8
+ * - Have an existing database schema you want to integrate with
9
+ * - Need custom data structures beyond what adapters provide
10
+ * - Want full control over database operations
11
+ */
12
+
13
+ import {
14
+ Agent,
15
+ GeminiProvider,
16
+ createMessageEvent,
17
+ EventSource,
18
+ createSession,
19
+ SessionState,
20
+ MessageEventData,
21
+ Event,
22
+ } from "../src/index";
23
+
24
+ /**
25
+ * Example: Your existing database structure
26
+ * This could be any ORM (Prisma, TypeORM, Drizzle) or query builder
27
+ */
28
+ interface CustomDatabaseSession {
29
+ id: string;
30
+ userId: string;
31
+ currentRoute?: string;
32
+ currentState?: string;
33
+ collectedData?: {
34
+ extracted?: Record<string, unknown>;
35
+ routeHistory?: unknown[];
36
+ currentRouteTitle?: string;
37
+ currentStateDescription?: string;
38
+ metadata?: Record<string, unknown>;
39
+ };
40
+ createdAt: Date;
41
+ updatedAt: Date;
42
+ }
43
+
44
+ interface CustomDatabaseMessage {
45
+ id: string;
46
+ sessionId: string;
47
+ userId: string;
48
+ role: "user" | "agent" | "system";
49
+ content: string;
50
+ route?: string;
51
+ state?: string;
52
+ createdAt: Date;
53
+ }
54
+
55
+ // Mock database - replace with your actual database
56
+ class CustomDatabase {
57
+ private sessions: Map<string, CustomDatabaseSession> = new Map();
58
+ private messages: Map<string, CustomDatabaseMessage[]> = new Map();
59
+
60
+ async findSession(id: string): Promise<CustomDatabaseSession | null> {
61
+ return this.sessions.get(id) || null;
62
+ }
63
+
64
+ async createSession(
65
+ data: Omit<CustomDatabaseSession, "id" | "createdAt" | "updatedAt">
66
+ ): Promise<CustomDatabaseSession> {
67
+ const session: CustomDatabaseSession = {
68
+ id: `session_${Date.now()}`,
69
+ ...data,
70
+ createdAt: new Date(),
71
+ updatedAt: new Date(),
72
+ };
73
+ this.sessions.set(session.id, session);
74
+ return session;
75
+ }
76
+
77
+ async updateSession(
78
+ id: string,
79
+ data: Partial<CustomDatabaseSession>
80
+ ): Promise<CustomDatabaseSession | null> {
81
+ const session = this.sessions.get(id);
82
+ if (!session) return null;
83
+
84
+ const updated = {
85
+ ...session,
86
+ ...data,
87
+ updatedAt: new Date(),
88
+ };
89
+ this.sessions.set(id, updated);
90
+ return updated;
91
+ }
92
+
93
+ async createMessage(
94
+ data: Omit<CustomDatabaseMessage, "id" | "createdAt">
95
+ ): Promise<CustomDatabaseMessage> {
96
+ const message: CustomDatabaseMessage = {
97
+ id: `msg_${Date.now()}`,
98
+ ...data,
99
+ createdAt: new Date(),
100
+ };
101
+
102
+ const messages = this.messages.get(data.sessionId) || [];
103
+ messages.push(message);
104
+ this.messages.set(data.sessionId, messages);
105
+
106
+ return message;
107
+ }
108
+
109
+ async getSessionMessages(
110
+ sessionId: string
111
+ ): Promise<CustomDatabaseMessage[]> {
112
+ return this.messages.get(sessionId) || [];
113
+ }
114
+ }
115
+
116
+ // Example data type for a customer onboarding route
117
+ interface OnboardingData {
118
+ fullName: string;
119
+ email: string;
120
+ companyName: string;
121
+ phoneNumber?: string;
122
+ industry?: string;
123
+ }
124
+
125
+ async function example() {
126
+ const db = new CustomDatabase();
127
+ const userId = "user_123";
128
+
129
+ // Create agent
130
+ const agent = new Agent({
131
+ name: "Onboarding Assistant",
132
+ description: "Help new customers get started",
133
+ goal: "Collect customer information efficiently",
134
+ ai: new GeminiProvider({
135
+ apiKey: process.env.GEMINI_API_KEY!,
136
+ model: "models/gemini-2.0-flash-exp",
137
+ }),
138
+ // NOTE: No persistence adapter - we handle it manually!
139
+ });
140
+
141
+ // Create onboarding route
142
+ const onboardingRoute = agent.createRoute<OnboardingData>({
143
+ title: "Customer Onboarding",
144
+ description: "Collect customer information",
145
+ conditions: [
146
+ "User is a new customer",
147
+ "User needs to set up their account",
148
+ ],
149
+ gatherSchema: {
150
+ type: "object",
151
+ properties: {
152
+ fullName: { type: "string" },
153
+ email: { type: "string" },
154
+ companyName: { type: "string" },
155
+ phoneNumber: { type: "string" },
156
+ industry: { type: "string" },
157
+ },
158
+ required: ["fullName", "email", "companyName"],
159
+ },
160
+ });
161
+
162
+ // Define states with custom IDs
163
+ onboardingRoute.initialState
164
+ .transitionTo({
165
+ id: "ask_name",
166
+ chatState: "Ask for full name",
167
+ gather: ["fullName"],
168
+ skipIf: (data) => !!data.fullName,
169
+ })
170
+ .transitionTo({
171
+ id: "ask_email",
172
+ chatState: "Ask for email address",
173
+ gather: ["email"],
174
+ skipIf: (data) => !!data.email,
175
+ })
176
+ .transitionTo({
177
+ id: "ask_company",
178
+ chatState: "Ask for company name",
179
+ gather: ["companyName"],
180
+ skipIf: (data) => !!data.companyName,
181
+ })
182
+ .transitionTo({
183
+ id: "ask_phone",
184
+ chatState: "Ask for phone number (optional)",
185
+ gather: ["phoneNumber"],
186
+ })
187
+ .transitionTo({
188
+ id: "ask_industry",
189
+ chatState: "Ask for industry",
190
+ gather: ["industry"],
191
+ })
192
+ .transitionTo({
193
+ id: "confirm_details",
194
+ chatState: "Confirm all details",
195
+ requiredData: ["fullName", "email", "companyName"],
196
+ });
197
+
198
+ /**
199
+ * Create or load session from your custom database
200
+ */
201
+ let dbSession = await db.findSession("existing_session_id");
202
+
203
+ if (!dbSession) {
204
+ // Create new session in your database
205
+ dbSession = await db.createSession({
206
+ userId,
207
+ collectedData: {},
208
+ });
209
+ console.log("✨ Created new database session:", dbSession.id);
210
+ }
211
+
212
+ /**
213
+ * Convert database session to agent SessionState
214
+ */
215
+ let agentSession: SessionState<OnboardingData>;
216
+
217
+ if (dbSession.currentRoute && dbSession.collectedData) {
218
+ // Restore existing session from database
219
+ console.log("📥 Restoring session from database...");
220
+
221
+ agentSession = {
222
+ currentRoute: {
223
+ id: dbSession.currentRoute,
224
+ title:
225
+ dbSession.collectedData?.currentRouteTitle || dbSession.currentRoute,
226
+ enteredAt: new Date(),
227
+ },
228
+ currentState: dbSession.currentState
229
+ ? {
230
+ id: dbSession.currentState,
231
+ description: dbSession.collectedData?.currentStateDescription,
232
+ enteredAt: new Date(),
233
+ }
234
+ : undefined,
235
+ extracted:
236
+ (dbSession.collectedData?.extracted as Partial<OnboardingData>) || {},
237
+ routeHistory:
238
+ (dbSession.collectedData
239
+ ?.routeHistory as SessionState<OnboardingData>["routeHistory"]) || [],
240
+ metadata: {
241
+ sessionId: dbSession.id,
242
+ userId,
243
+ createdAt: dbSession.createdAt,
244
+ lastUpdatedAt: dbSession.updatedAt,
245
+ ...(dbSession.collectedData?.metadata as Record<string, unknown>),
246
+ },
247
+ };
248
+
249
+ console.log("✅ Session restored:", {
250
+ sessionId: agentSession.metadata?.sessionId,
251
+ currentRoute: agentSession.currentRoute?.title,
252
+ currentState: agentSession.currentState?.id,
253
+ extracted: agentSession.extracted,
254
+ });
255
+ } else {
256
+ // Create new session state
257
+ console.log("🆕 Creating new session state...");
258
+
259
+ agentSession = createSession<OnboardingData>(dbSession.id, {
260
+ sessionId: dbSession.id,
261
+ userId,
262
+ createdAt: dbSession.createdAt,
263
+ });
264
+ }
265
+
266
+ /**
267
+ * Simulate conversation
268
+ */
269
+ const history: Event<MessageEventData>[] = [];
270
+
271
+ // Turn 1: User provides name and email
272
+ console.log("\n--- Turn 1 ---");
273
+ const userMessage1 = createMessageEvent(
274
+ EventSource.CUSTOMER,
275
+ "User",
276
+ "Hi! I'm John Smith and my email is john@acme.com"
277
+ );
278
+ history.push(userMessage1);
279
+
280
+ // Save user message to database
281
+ await db.createMessage({
282
+ sessionId: dbSession.id,
283
+ userId,
284
+ role: "user",
285
+ content: userMessage1.data.message,
286
+ });
287
+
288
+ const response1 = await agent.respond({
289
+ history,
290
+ session: agentSession,
291
+ });
292
+
293
+ console.log("🤖 Agent:", response1.message);
294
+ console.log("📊 Extracted so far:", response1.session?.extracted);
295
+
296
+ // Save agent message to database
297
+ await db.createMessage({
298
+ sessionId: dbSession.id,
299
+ userId,
300
+ role: "agent",
301
+ content: response1.message,
302
+ route: response1.session?.currentRoute?.id,
303
+ state: response1.session?.currentState?.id,
304
+ });
305
+
306
+ // Manually save session state back to database
307
+ await db.updateSession(dbSession.id, {
308
+ currentRoute: response1.session?.currentRoute?.id,
309
+ currentState: response1.session?.currentState?.id,
310
+ collectedData: {
311
+ extracted: response1.session?.extracted,
312
+ routeHistory: response1.session?.routeHistory,
313
+ currentRouteTitle: response1.session?.currentRoute?.title,
314
+ currentStateDescription: response1.session?.currentState?.description,
315
+ metadata: response1.session?.metadata,
316
+ },
317
+ });
318
+
319
+ console.log("💾 Session saved to database");
320
+
321
+ // Update session for next turn
322
+ agentSession = response1.session!;
323
+
324
+ // Turn 2: User provides company
325
+ console.log("\n--- Turn 2 ---");
326
+ history.push(
327
+ createMessageEvent(EventSource.AI_AGENT, "Agent", response1.message)
328
+ );
329
+
330
+ const userMessage2 = createMessageEvent(
331
+ EventSource.CUSTOMER,
332
+ "User",
333
+ "I work for Acme Corporation"
334
+ );
335
+ history.push(userMessage2);
336
+
337
+ await db.createMessage({
338
+ sessionId: dbSession.id,
339
+ userId,
340
+ role: "user",
341
+ content: userMessage2.data.message,
342
+ });
343
+
344
+ const response2 = await agent.respond({
345
+ history,
346
+ session: agentSession,
347
+ });
348
+
349
+ console.log("🤖 Agent:", response2.message);
350
+ console.log("📊 Extracted so far:", response2.session?.extracted);
351
+
352
+ await db.createMessage({
353
+ sessionId: dbSession.id,
354
+ userId,
355
+ role: "agent",
356
+ content: response2.message,
357
+ route: response2.session?.currentRoute?.id,
358
+ state: response2.session?.currentState?.id,
359
+ });
360
+
361
+ // Save session state
362
+ await db.updateSession(dbSession.id, {
363
+ currentRoute: response2.session?.currentRoute?.id,
364
+ currentState: response2.session?.currentState?.id,
365
+ collectedData: {
366
+ extracted: response2.session?.extracted,
367
+ routeHistory: response2.session?.routeHistory,
368
+ currentRouteTitle: response2.session?.currentRoute?.title,
369
+ currentStateDescription: response2.session?.currentState?.description,
370
+ metadata: response2.session?.metadata,
371
+ },
372
+ });
373
+
374
+ console.log("💾 Session saved to database");
375
+
376
+ /**
377
+ * Demonstrate session recovery
378
+ */
379
+ console.log("\n--- Session Recovery ---");
380
+ console.log("Simulating app restart...\n");
381
+
382
+ // Load session from database again
383
+ const reloadedDbSession = await db.findSession(dbSession.id);
384
+ if (!reloadedDbSession) throw new Error("Session not found");
385
+
386
+ // Reconstruct session state
387
+ const recoveredSession: SessionState<OnboardingData> = {
388
+ currentRoute: reloadedDbSession.currentRoute
389
+ ? {
390
+ id: reloadedDbSession.currentRoute,
391
+ title:
392
+ reloadedDbSession.collectedData?.currentRouteTitle ||
393
+ reloadedDbSession.currentRoute,
394
+ enteredAt: new Date(),
395
+ }
396
+ : undefined,
397
+ currentState: reloadedDbSession.currentState
398
+ ? {
399
+ id: reloadedDbSession.currentState,
400
+ description: reloadedDbSession.collectedData?.currentStateDescription,
401
+ enteredAt: new Date(),
402
+ }
403
+ : undefined,
404
+ extracted:
405
+ (reloadedDbSession.collectedData?.extracted as Partial<OnboardingData>) ||
406
+ {},
407
+ routeHistory:
408
+ (reloadedDbSession.collectedData
409
+ ?.routeHistory as SessionState<OnboardingData>["routeHistory"]) || [],
410
+ metadata: {
411
+ sessionId: reloadedDbSession.id,
412
+ userId,
413
+ createdAt: reloadedDbSession.createdAt,
414
+ lastUpdatedAt: reloadedDbSession.updatedAt,
415
+ },
416
+ };
417
+
418
+ console.log("✅ Session recovered from database:", {
419
+ sessionId: recoveredSession.metadata?.sessionId,
420
+ currentRoute: recoveredSession.currentRoute?.title,
421
+ currentState: recoveredSession.currentState?.id,
422
+ extracted: recoveredSession.extracted,
423
+ });
424
+
425
+ // Load message history
426
+ const messages = await db.getSessionMessages(dbSession.id);
427
+ console.log(`📜 Loaded ${messages.length} messages from history`);
428
+
429
+ console.log("\n✅ Example complete!");
430
+ }
431
+
432
+ /**
433
+ * Advanced Example: With validation hooks
434
+ */
435
+ async function advancedExample() {
436
+ const db = new CustomDatabase();
437
+ const userId = "user_456";
438
+
439
+ const agent = new Agent({
440
+ name: "Smart Onboarding",
441
+ ai: new GeminiProvider({
442
+ apiKey: process.env.GEMINI_API_KEY!,
443
+ model: "models/gemini-2.0-flash-exp",
444
+ }),
445
+ hooks: {
446
+ // Validate and enrich extracted data
447
+ onExtractedUpdate: async (extracted, previous) => {
448
+ console.log("🔄 Data extracted, validating...");
449
+
450
+ // Normalize email
451
+ if (extracted.email) {
452
+ extracted.email = extracted.email.toLowerCase().trim();
453
+ }
454
+
455
+ // Normalize phone
456
+ if (extracted.phoneNumber) {
457
+ extracted.phoneNumber = extracted.phoneNumber.replace(/\D/g, "");
458
+ }
459
+
460
+ return extracted;
461
+ },
462
+ },
463
+ });
464
+
465
+ const route = agent.createRoute<OnboardingData>({
466
+ title: "Onboarding",
467
+ gatherSchema: {
468
+ type: "object",
469
+ properties: {
470
+ fullName: { type: "string" },
471
+ email: { type: "string" },
472
+ companyName: { type: "string" },
473
+ phoneNumber: { type: "string" },
474
+ },
475
+ required: ["fullName", "email", "companyName"],
476
+ },
477
+ });
478
+
479
+ route.initialState.transitionTo({
480
+ id: "collect_all",
481
+ chatState: "Collect all information",
482
+ gather: ["fullName", "email", "companyName", "phoneNumber"],
483
+ });
484
+
485
+ // Create database session
486
+ const dbSession = await db.createSession({
487
+ userId,
488
+ collectedData: {},
489
+ });
490
+
491
+ // Create agent session
492
+ let agentSession = createSession<OnboardingData>(dbSession.id, {
493
+ userId,
494
+ });
495
+
496
+ // Simulate conversation
497
+ const response = await agent.respond({
498
+ history: [
499
+ createMessageEvent(
500
+ EventSource.CUSTOMER,
501
+ "User",
502
+ "I'm Alice Johnson, alice@EXAMPLE.COM, working for TechCorp. Phone: (555) 123-4567"
503
+ ),
504
+ ],
505
+ session: agentSession,
506
+ });
507
+
508
+ console.log("🤖 Agent:", response.message);
509
+ console.log("📊 Normalized data:", response.session?.extracted);
510
+ // Shows: { email: "alice@example.com", phoneNumber: "5551234567", ... }
511
+
512
+ // Save to database
513
+ await db.updateSession(dbSession.id, {
514
+ currentRoute: response.session?.currentRoute?.id,
515
+ currentState: response.session?.currentState?.id,
516
+ collectedData: {
517
+ extracted: response.session?.extracted,
518
+ routeHistory: response.session?.routeHistory,
519
+ currentRouteTitle: response.session?.currentRoute?.title,
520
+ currentStateDescription: response.session?.currentState?.description,
521
+ metadata: response.session?.metadata,
522
+ },
523
+ });
524
+
525
+ console.log("✅ Validated data saved to custom database!");
526
+ }
527
+
528
+ // Run the example
529
+ if (require.main === module) {
530
+ example().catch(console.error);
531
+ }
532
+
533
+ export { example, advancedExample };
@@ -128,14 +128,16 @@ async function example() {
128
128
  },
129
129
  });
130
130
 
131
- // State flow with smart data gathering
131
+ // State flow with smart data gathering and custom IDs
132
132
  const askDestination = flightRoute.initialState.transitionTo({
133
+ id: "ask_destination", // Custom state ID for easier tracking
133
134
  chatState: "Ask where they want to fly",
134
135
  gather: ["destination"],
135
136
  skipIf: (extracted) => !!extracted.destination,
136
137
  });
137
138
 
138
139
  const askDates = askDestination.transitionTo({
140
+ id: "ask_dates", // Custom state ID
139
141
  chatState: "Ask about travel dates",
140
142
  gather: ["departureDate", "returnDate"],
141
143
  skipIf: (extracted) => !!extracted.departureDate,
@@ -143,6 +145,7 @@ async function example() {
143
145
  });
144
146
 
145
147
  const askPassengers = askDates.transitionTo({
148
+ id: "ask_passengers", // Custom state ID
146
149
  chatState: "Ask how many passengers",
147
150
  gather: ["passengers"],
148
151
  skipIf: (extracted) => !!extracted.passengers,
@@ -150,6 +153,7 @@ async function example() {
150
153
  });
151
154
 
152
155
  const askCabinClass = askPassengers.transitionTo({
156
+ id: "ask_cabin_class", // Custom state ID
153
157
  chatState: "Ask about cabin class preference",
154
158
  gather: ["cabinClass"],
155
159
  skipIf: (extracted) => !!extracted.cabinClass,
@@ -157,6 +161,7 @@ async function example() {
157
161
  });
158
162
 
159
163
  const confirmBooking = askCabinClass.transitionTo({
164
+ id: "confirm_booking", // Custom state ID
160
165
  chatState: "Present options and confirm booking details",
161
166
  requiredData: ["destination", "departureDate", "passengers", "cabinClass"],
162
167
  });
@@ -186,6 +191,10 @@ async function example() {
186
191
  const dbSessionId = sessionResult.sessionData.id;
187
192
 
188
193
  console.log("✨ Created new session:", dbSessionId);
194
+ console.log("📊 Session metadata:", {
195
+ sessionId: session.metadata?.sessionId, // Same as dbSessionId
196
+ createdAt: session.metadata?.createdAt,
197
+ });
189
198
  console.log("📊 Initial session state:", {
190
199
  currentRoute: session.currentRoute,
191
200
  extracted: session.extracted,
@@ -216,8 +225,10 @@ async function example() {
216
225
 
217
226
  console.log("🤖 Agent:", response1.message);
218
227
  console.log("📊 Session state after turn 1:", {
228
+ sessionId: response1.session?.metadata?.sessionId,
219
229
  currentRoute: response1.session?.currentRoute?.title,
220
- currentState: response1.session?.currentState?.id,
230
+ currentStateId: response1.session?.currentState?.id, // Custom ID like "ask_destination"
231
+ currentStateDescription: response1.session?.currentState?.description,
221
232
  extracted: response1.session?.extracted,
222
233
  });
223
234
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@falai/agent",
3
- "version": "0.5.1",
3
+ "version": "0.5.4",
4
4
  "description": "Standalone, strongly-typed AI Agent framework with route DSL and AI provider strategy",
5
5
  "type": "module",
6
6
  "main": "./dist/cjs/index.js",
@@ -19,13 +19,16 @@ export class ResponseEngine<TContext = unknown> {
19
19
  type: "object",
20
20
  properties: {
21
21
  message: { type: "string", description: "Final user-facing message" },
22
- data: route.responseOutputSchema || { type: "object" },
23
- contextUpdate: { type: "object" },
24
22
  },
25
23
  required: ["message"],
26
24
  additionalProperties: false,
27
25
  };
28
26
 
27
+ // Add data field only if route has responseOutputSchema
28
+ if (route.responseOutputSchema) {
29
+ base.properties!.data = route.responseOutputSchema;
30
+ }
31
+
29
32
  // Add gather fields from current state
30
33
  if (currentState?.gatherFields && route.gatherSchema?.properties) {
31
34
  for (const field of currentState.gatherFields) {
@@ -75,7 +78,7 @@ export class ResponseEngine<TContext = unknown> {
75
78
  pc.addInteractionHistory(history);
76
79
  pc.addLastMessage(lastMessage);
77
80
  pc.addInstruction(
78
- "Return ONLY JSON matching the schema (message + optional data/contextUpdate)."
81
+ "Return ONLY JSON matching the schema. The 'message' field is required."
79
82
  );
80
83
  return pc.build();
81
84
  }
package/src/core/State.ts CHANGED
@@ -83,7 +83,7 @@ export class State<TContext = unknown, TExtracted = unknown> {
83
83
  const targetState = new State<TContext, TExtracted>(
84
84
  this.routeId,
85
85
  spec.chatState,
86
- undefined,
86
+ spec.id, // Use custom ID if provided
87
87
  spec.gather,
88
88
  spec.skipIf,
89
89
  spec.requiredData