@falai/agent 0.5.3 → 0.5.5

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 (48) hide show
  1. package/README.md +20 -4
  2. package/dist/cjs/core/Agent.d.ts +0 -5
  3. package/dist/cjs/core/Agent.d.ts.map +1 -1
  4. package/dist/cjs/core/Agent.js +75 -157
  5. package/dist/cjs/core/Agent.js.map +1 -1
  6. package/dist/cjs/core/RoutingEngine.d.ts +68 -2
  7. package/dist/cjs/core/RoutingEngine.d.ts.map +1 -1
  8. package/dist/cjs/core/RoutingEngine.js +416 -2
  9. package/dist/cjs/core/RoutingEngine.js.map +1 -1
  10. package/dist/cjs/core/State.js +2 -1
  11. package/dist/cjs/core/State.js.map +1 -1
  12. package/dist/cjs/types/route.d.ts +2 -0
  13. package/dist/cjs/types/route.d.ts.map +1 -1
  14. package/dist/cjs/utils/event.d.ts +6 -0
  15. package/dist/cjs/utils/event.d.ts.map +1 -0
  16. package/dist/cjs/utils/event.js +20 -0
  17. package/dist/cjs/utils/event.js.map +1 -0
  18. package/dist/core/Agent.d.ts +0 -5
  19. package/dist/core/Agent.d.ts.map +1 -1
  20. package/dist/core/Agent.js +74 -156
  21. package/dist/core/Agent.js.map +1 -1
  22. package/dist/core/RoutingEngine.d.ts +68 -2
  23. package/dist/core/RoutingEngine.d.ts.map +1 -1
  24. package/dist/core/RoutingEngine.js +416 -2
  25. package/dist/core/RoutingEngine.js.map +1 -1
  26. package/dist/core/State.js +2 -1
  27. package/dist/core/State.js.map +1 -1
  28. package/dist/types/route.d.ts +2 -0
  29. package/dist/types/route.d.ts.map +1 -1
  30. package/dist/utils/event.d.ts +6 -0
  31. package/dist/utils/event.d.ts.map +1 -0
  32. package/dist/utils/event.js +17 -0
  33. package/dist/utils/event.js.map +1 -0
  34. package/docs/API_REFERENCE.md +107 -26
  35. package/docs/ARCHITECTURE.md +83 -12
  36. package/docs/PERSISTENCE.md +18 -6
  37. package/examples/business-onboarding.ts +11 -0
  38. package/examples/custom-database-persistence.ts +533 -0
  39. package/examples/healthcare-agent.ts +28 -16
  40. package/examples/persistent-onboarding.ts +16 -10
  41. package/examples/prisma-persistence.ts +13 -2
  42. package/examples/travel-agent.ts +94 -52
  43. package/package.json +1 -1
  44. package/src/core/Agent.ts +78 -227
  45. package/src/core/RoutingEngine.ts +663 -2
  46. package/src/core/State.ts +1 -1
  47. package/src/types/route.ts +2 -0
  48. package/src/utils/event.ts +16 -0
@@ -87,29 +87,99 @@ interface RespondOutput {
87
87
  - Enables "I changed my mind" scenarios with context-aware routing
88
88
  - Automatically merges new extracted data with existing session data
89
89
 
90
- **Example:**
90
+ **Example with Persistence Adapters:**
91
91
 
92
92
  ```typescript
93
+ import { createSession } from "@falai/agent";
94
+
95
+ // Using built-in persistence adapters
96
+ const { sessionData, sessionState } =
97
+ await persistence.createSessionWithState<FlightData>({
98
+ userId: "user_123",
99
+ agentName: "Travel Agent",
100
+ });
101
+
93
102
  const response = await agent.respond({
94
- history: conversationHistory,
103
+ history,
104
+ session: sessionState, // Auto-saves if autoSave: true
95
105
  });
106
+ ```
96
107
 
97
- // Save to database
98
- await db.agentMessages.create({
99
- sessionId: session.id,
108
+ **Example with Custom Database (Manual):**
109
+
110
+ ```typescript
111
+ import { createSession, SessionState } from "@falai/agent";
112
+
113
+ // Load from your custom database
114
+ const dbSession = await yourDb.sessions.findOne({ id: sessionId });
115
+
116
+ // Restore or create session state
117
+ let agentSession: SessionState<YourDataType>;
118
+
119
+ if (dbSession && dbSession.currentRoute && dbSession.collectedData) {
120
+ // Restore existing session from database
121
+ agentSession = {
122
+ currentRoute: {
123
+ id: dbSession.currentRoute,
124
+ title:
125
+ dbSession.collectedData?.currentRouteTitle || dbSession.currentRoute,
126
+ enteredAt: new Date(),
127
+ },
128
+ currentState: dbSession.currentState
129
+ ? {
130
+ id: dbSession.currentState,
131
+ description: dbSession.collectedData?.currentStateDescription,
132
+ enteredAt: new Date(),
133
+ }
134
+ : undefined,
135
+ extracted: dbSession.collectedData?.extracted || {},
136
+ routeHistory: dbSession.collectedData?.routeHistory || [],
137
+ metadata: {
138
+ sessionId: dbSession.id,
139
+ createdAt: dbSession.createdAt,
140
+ lastUpdatedAt: new Date(),
141
+ },
142
+ };
143
+ } else {
144
+ // Create new session
145
+ agentSession = createSession<YourDataType>({
146
+ sessionId: dbSession?.id || "new-session-id",
147
+ });
148
+ }
149
+
150
+ // Use session in conversation
151
+ const response = await agent.respond({
152
+ history,
153
+ session: agentSession,
154
+ });
155
+
156
+ // Manually save to your database
157
+ await yourDb.sessions.update({
158
+ id: dbSession.id,
159
+ currentRoute: response.session?.currentRoute?.id,
160
+ currentState: response.session?.currentState?.id,
161
+ collectedData: {
162
+ extracted: response.session?.extracted,
163
+ routeHistory: response.session?.routeHistory,
164
+ currentRouteTitle: response.session?.currentRoute?.title,
165
+ currentStateDescription: response.session?.currentState?.description,
166
+ metadata: response.session?.metadata,
167
+ },
168
+ lastMessageAt: new Date(),
169
+ });
170
+
171
+ // Save message
172
+ await yourDb.messages.create({
173
+ sessionId: dbSession.id,
100
174
  role: "agent",
101
175
  content: response.message,
102
- route: response.route?.title,
103
- state: response.state?.description,
104
- toolCalls: response.toolCalls || [],
176
+ route: response.session?.currentRoute?.id,
177
+ state: response.session?.currentState?.id,
105
178
  });
106
-
107
- // Check if conversation is complete
108
- if (response.route?.title === END_ROUTE) {
109
- await markSessionComplete(session.id);
110
- }
111
179
  ```
112
180
 
181
+ See also: [Custom Database Integration Example](../examples/custom-database-persistence.ts)
182
+
113
183
  ##### `respondStream(input: RespondInput<TContext>): AsyncGenerator<StreamChunk>`
114
184
 
115
185
  Generates an AI response as a real-time stream for better user experience. Provides the same structured output as `respond()` but delivers it incrementally.
@@ -350,11 +420,16 @@ interface TransitionResult<TExtracted = unknown> {
350
420
  routeId: string; // Route identifier
351
421
  transitionTo: (
352
422
  spec: TransitionSpec<TExtracted>,
353
- condition?: string
423
+ condition?: string // Optional: AI-evaluated text condition for this transition
354
424
  ) => TransitionResult<TExtracted>;
355
425
  }
356
426
  ```
357
427
 
428
+ **Parameters:**
429
+
430
+ - `spec`: The transition specification (see `TransitionSpec` above)
431
+ - `condition` (optional): Human-readable condition text that the AI evaluates when selecting states. Use this to guide the AI's state selection based on conversation context. Examples: "Customer confirmed payment", "All required data collected", "User wants to modify order"
432
+
358
433
  **Returns:** A `TransitionResult` that includes the target state's reference (`id`, `routeId`) and a `transitionTo` method for chaining additional transitions.
359
434
 
360
435
  **Example:**
@@ -381,19 +456,25 @@ const flightRoute = agent.createRoute<FlightData>({
381
456
  },
382
457
  });
383
458
 
384
- // Approach 1: Step-by-step with data extraction
385
- const askDestination = flightRoute.initialState.transitionTo({
386
- chatState: "Ask where they want to fly",
387
- gather: ["destination"],
388
- skipIf: (extracted) => !!extracted.destination, // Skip if already have destination
389
- });
459
+ // Approach 1: Step-by-step with data extraction and text conditions
460
+ const askDestination = flightRoute.initialState.transitionTo(
461
+ {
462
+ chatState: "Ask where they want to fly",
463
+ gather: ["destination"],
464
+ skipIf: (extracted) => !!extracted.destination, // Skip if already have destination
465
+ },
466
+ "Customer hasn't specified destination yet" // AI-evaluated condition
467
+ );
390
468
 
391
- const askDates = askDestination.transitionTo({
392
- chatState: "Ask about travel dates",
393
- gather: ["departureDate"],
394
- skipIf: (extracted) => !!extracted.departureDate,
395
- requiredData: ["destination"], // Must have destination first
396
- });
469
+ const askDates = askDestination.transitionTo(
470
+ {
471
+ chatState: "Ask about travel dates",
472
+ gather: ["departureDate"],
473
+ skipIf: (extracted) => !!extracted.departureDate,
474
+ requiredData: ["destination"], // Must have destination first
475
+ },
476
+ "Destination confirmed, need travel dates"
477
+ );
397
478
 
398
479
  const askPassengers = askDates.transitionTo({
399
480
  chatState: "How many passengers?",
@@ -58,7 +58,11 @@ import {
58
58
  mergeExtracted,
59
59
  } from "@falai/agent";
60
60
 
61
- let session = createSession<FlightData>();
61
+ // Create session with optional metadata including session ID
62
+ let session = createSession<FlightData>(sessionId, {
63
+ userId: "user_456",
64
+ createdAt: new Date(),
65
+ });
62
66
 
63
67
  // Turn 1 - Extract data
64
68
  const response1 = await agent.respond({ history, session });
@@ -67,6 +71,31 @@ session = response1.session!; // Updated with extracted data
67
71
  // Turn 2 - User changes mind (always-on routing)
68
72
  const response2 = await agent.respond({ history, session: response1.session });
69
73
  session = response2.session!; // Route/state updated if user changed direction
74
+
75
+ // Access session metadata
76
+ console.log(session.metadata?.sessionId); // "unique-session-123"
77
+ ```
78
+
79
+ **Session with Persistence:**
80
+
81
+ When using persistence adapters, set the session ID from the database:
82
+
83
+ ```typescript
84
+ // Create database session and in-memory session state
85
+ const { sessionData, sessionState } =
86
+ await persistence.createSessionWithState<FlightData>({
87
+ userId: "user_123",
88
+ agentName: "Travel Agent",
89
+ });
90
+
91
+ // sessionState.metadata.sessionId is automatically set to sessionData.id
92
+ console.log(sessionState.metadata?.sessionId); // "cuid_from_database"
93
+
94
+ // Use it in conversation
95
+ const response = await agent.respond({
96
+ history,
97
+ session: sessionState, // Auto-saves to database!
98
+ });
70
99
  ```
71
100
 
72
101
  **Why?** Session state enables:
@@ -75,33 +104,75 @@ session = response2.session!; // Route/state updated if user changed direction
75
104
  - **Context Awareness** - Router sees current progress and extracted data
76
105
  - **Data Persistence** - Extracted data survives across turns
77
106
  - **State Recovery** - Resume conversations from any point
107
+ - **Session Tracking** - Track conversations via session ID in database
78
108
 
79
- ### 3. 🔧 Code-Based State Logic
109
+ ### 3. 🔧 Code-Based State Logic + AI-Driven Transitions
80
110
 
81
- Use TypeScript functions instead of LLM conditions for deterministic flow:
111
+ Use TypeScript functions for deterministic flow control AND text conditions for AI-driven state selection:
82
112
 
83
113
  ```typescript
84
114
  // State with smart bypassing based on extracted data
85
- const askDestination = route.initialState.transitionTo({
86
- chatState: "Ask where they want to fly",
87
- gather: ["destination"],
88
- skipIf: (extracted) => !!extracted.destination, // Code-based condition!
115
+ const askDestination = route.initialState.transitionTo(
116
+ {
117
+ id: "ask_destination", // Optional: custom state ID
118
+ chatState: "Ask where they want to fly",
119
+ gather: ["destination"],
120
+ skipIf: (extracted) => !!extracted.destination, // Code-based condition!
121
+ },
122
+ "Customer hasn't specified destination yet" // Text condition for AI
123
+ );
124
+
125
+ const askDate = askDestination.transitionTo(
126
+ {
127
+ id: "ask_date", // Optional: custom state ID for easier tracking
128
+ chatState: "Ask about travel dates",
129
+ gather: ["departureDate"],
130
+ skipIf: (extracted) => !!extracted.departureDate,
131
+ requiredData: ["destination"], // Prerequisites
132
+ },
133
+ "Destination confirmed, need travel dates now"
134
+ );
89
135
  });
136
+ ```
90
137
 
91
- const askDate = askDestination.transitionTo({
92
- chatState: "Ask about travel dates",
93
- gather: ["departureDate"],
94
- skipIf: (extracted) => !!extracted.departureDate,
95
- requiredData: ["destination"], // Prerequisites
138
+ **Custom State IDs:**
139
+
140
+ You can optionally provide custom IDs for states to make them easier to track and reference:
141
+
142
+ ```typescript
143
+ const confirmBooking = askDate.transitionTo({
144
+ id: "confirm_booking", // ✅ Custom ID instead of auto-generated
145
+ chatState: "Confirm all booking details",
146
+ requiredData: ["destination", "departureDate", "passengers"],
96
147
  });
97
148
  ```
98
149
 
150
+ If you don't provide an ID, one is automatically generated from the route ID and state description.
151
+
99
152
  **Why?** Code-based logic provides:
100
153
 
101
154
  - **Predictability** - No fuzzy LLM interpretation of conditions
102
155
  - **Performance** - No extra LLM calls for condition checking
103
156
  - **Debugging** - Clear logic flow you can trace
104
157
  - **Type Safety** - Full TypeScript support for data validation
158
+ - **Custom IDs** - Easier tracking and debugging with meaningful state identifiers
159
+
160
+ **How State Transitions Work:**
161
+
162
+ 1. **Code filters first**: `skipIf` and `requiredData` filter out invalid states deterministically
163
+ 2. **AI selects best state**: From valid candidates, AI evaluates text conditions to choose optimal state
164
+ 3. **Combined decision**: Single AI call handles both route selection AND state selection (no extra calls!)
165
+ 4. **Completion detection**: When all states are skipped and `END_ROUTE` is reached, route is marked complete
166
+
167
+ ```typescript
168
+ // The AI sees:
169
+ // "Available states: askDate, confirmBooking
170
+ // - askDate: Destination confirmed, need travel dates now
171
+ // - confirmBooking: All required info collected, ready to book
172
+ // Current extracted: {destination: 'Paris', departureDate: '2025-01-15'}
173
+ //
174
+ // → AI selects 'confirmBooking' based on context"
175
+ ```
105
176
 
106
177
  ### 4. 🛠️ Tools with Data Access
107
178
 
@@ -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({
@@ -586,15 +586,19 @@ async function createBusinessOnboardingAgent(
586
586
  // Beautiful fluent chaining for linear flows
587
587
  feedbackRoute.initialState
588
588
  .transitionTo({
589
+ id: "ask_rating",
589
590
  chatState: "How would you rate your onboarding experience? (1-5 stars)",
590
591
  })
591
592
  .transitionTo({
593
+ id: "ask_liked_most",
592
594
  chatState: "What did you like most about the process?",
593
595
  })
594
596
  .transitionTo({
597
+ id: "ask_improve",
595
598
  chatState: "Is there anything we could improve?",
596
599
  })
597
600
  .transitionTo({
601
+ id: "thank_you",
598
602
  chatState: "Thank you for your feedback! It helps us improve. 🙏",
599
603
  })
600
604
  .transitionTo({ state: END_ROUTE });
@@ -603,38 +607,45 @@ async function createBusinessOnboardingAgent(
603
607
 
604
608
  agent
605
609
  .createGuideline({
610
+ id: "guideline_confused",
606
611
  condition: "User seems confused or doesn't understand something",
607
612
  action:
608
613
  "Be patient and provide practical examples of what you need. E.g., 'José Silva Street, 123, São Paulo - SP' for address",
609
614
  })
610
615
  .createGuideline({
616
+ id: "guideline_incomplete",
611
617
  condition: "User provides incomplete or very vague information",
612
618
  action:
613
619
  "Politely ask for the missing specific details. E.g., 'You mentioned the address, but what's the city and state?'",
614
620
  })
615
621
  .createGuideline({
622
+ id: "guideline_skip",
616
623
  condition:
617
624
  "User wants to skip information saying they don't have it or it doesn't apply",
618
625
  action:
619
626
  "Be smart: if the information is critical for their business type (e.g., address for physical store, website for e-commerce), explain the importance. If not critical, accept it and move forward saying 'no problem, that's fine'",
620
627
  })
621
628
  .createGuideline({
629
+ id: "guideline_physical_online",
622
630
  condition: "User has physical store but said online-only or vice versa",
623
631
  action:
624
632
  "Adjust the flow dynamically: if they have a physical store, prioritize address and hours. If online-only, prioritize website/social media and digital support hours. Don't ask for irrelevant information",
625
633
  })
626
634
  .createGuideline({
635
+ id: "guideline_why",
627
636
  condition: "User asks why they need to provide certain information",
628
637
  action:
629
638
  "Explain practically: 'This information will help your assistant automatically answer customers when they ask about this. E.g., when they ask about payment methods, the assistant will inform automatically'",
630
639
  })
631
640
  .createGuideline({
641
+ id: "guideline_edit",
632
642
  condition:
633
643
  "User wants to edit or correct something they already provided",
634
644
  action:
635
645
  "Accept promptly and update the information: 'Of course! I'll update to...'. Use the appropriate tool to save the correction",
636
646
  })
637
647
  .createGuideline({
648
+ id: "guideline_unrelated",
638
649
  condition: "User asks a question unrelated to onboarding",
639
650
  action:
640
651
  "Answer briefly and redirect: 'I understand, but let's finish the setup first? We're almost there!'",