@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.
- package/README.md +20 -4
- package/dist/cjs/core/Agent.d.ts +0 -5
- package/dist/cjs/core/Agent.d.ts.map +1 -1
- package/dist/cjs/core/Agent.js +75 -157
- package/dist/cjs/core/Agent.js.map +1 -1
- package/dist/cjs/core/RoutingEngine.d.ts +68 -2
- package/dist/cjs/core/RoutingEngine.d.ts.map +1 -1
- package/dist/cjs/core/RoutingEngine.js +416 -2
- package/dist/cjs/core/RoutingEngine.js.map +1 -1
- package/dist/cjs/core/State.js +2 -1
- package/dist/cjs/core/State.js.map +1 -1
- package/dist/cjs/types/route.d.ts +2 -0
- package/dist/cjs/types/route.d.ts.map +1 -1
- package/dist/cjs/utils/event.d.ts +6 -0
- package/dist/cjs/utils/event.d.ts.map +1 -0
- package/dist/cjs/utils/event.js +20 -0
- package/dist/cjs/utils/event.js.map +1 -0
- package/dist/core/Agent.d.ts +0 -5
- package/dist/core/Agent.d.ts.map +1 -1
- package/dist/core/Agent.js +74 -156
- package/dist/core/Agent.js.map +1 -1
- package/dist/core/RoutingEngine.d.ts +68 -2
- package/dist/core/RoutingEngine.d.ts.map +1 -1
- package/dist/core/RoutingEngine.js +416 -2
- package/dist/core/RoutingEngine.js.map +1 -1
- package/dist/core/State.js +2 -1
- package/dist/core/State.js.map +1 -1
- package/dist/types/route.d.ts +2 -0
- package/dist/types/route.d.ts.map +1 -1
- package/dist/utils/event.d.ts +6 -0
- package/dist/utils/event.d.ts.map +1 -0
- package/dist/utils/event.js +17 -0
- package/dist/utils/event.js.map +1 -0
- package/docs/API_REFERENCE.md +107 -26
- package/docs/ARCHITECTURE.md +83 -12
- package/docs/PERSISTENCE.md +18 -6
- package/examples/business-onboarding.ts +11 -0
- package/examples/custom-database-persistence.ts +533 -0
- package/examples/healthcare-agent.ts +28 -16
- package/examples/persistent-onboarding.ts +16 -10
- package/examples/prisma-persistence.ts +13 -2
- package/examples/travel-agent.ts +94 -52
- package/package.json +1 -1
- package/src/core/Agent.ts +78 -227
- package/src/core/RoutingEngine.ts +663 -2
- package/src/core/State.ts +1 -1
- package/src/types/route.ts +2 -0
- package/src/utils/event.ts +16 -0
package/docs/API_REFERENCE.md
CHANGED
|
@@ -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
|
|
103
|
+
history,
|
|
104
|
+
session: sessionState, // Auto-saves if autoSave: true
|
|
95
105
|
});
|
|
106
|
+
```
|
|
96
107
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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.
|
|
103
|
-
state: response.
|
|
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
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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?",
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -58,7 +58,11 @@ import {
|
|
|
58
58
|
mergeExtracted,
|
|
59
59
|
} from "@falai/agent";
|
|
60
60
|
|
|
61
|
-
|
|
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
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
|
package/docs/PERSISTENCE.md
CHANGED
|
@@ -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
|
|
126
|
-
|
|
127
|
-
|
|
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!'",
|