@falai/agent 0.1.5 → 0.2.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,447 @@
1
+ # Context Management & Persistence
2
+
3
+ ## Overview
4
+
5
+ The `@falai/agent` framework provides flexible patterns for managing context in both **single-turn** and **multi-turn persistent** conversations. This guide explains how to handle stateful conversations that persist across requests.
6
+
7
+ ---
8
+
9
+ ## 🤔 The Context Lifecycle Problem
10
+
11
+ ### Single-Turn Conversations (Simple)
12
+
13
+ ```typescript
14
+ // ✅ Works great for single-turn conversations
15
+ const agent = new Agent({
16
+ name: "Bot",
17
+ ai: provider,
18
+ context: { userId: "123", preferences: {} },
19
+ });
20
+
21
+ const response = await agent.respond({ history });
22
+ ```
23
+
24
+ **Works because:** Agent is used once and discarded.
25
+
26
+ ### Multi-Turn Conversations (Complex)
27
+
28
+ ```typescript
29
+ // ❌ PROBLEM: Context updates don't persist
30
+ const agent = new Agent({
31
+ name: "Bot",
32
+ ai: provider,
33
+ context: { userId: "123", preferences: {} },
34
+ });
35
+
36
+ // Turn 1
37
+ await agent.respond({ history: [message1] });
38
+ // Tool modifies context in memory...
39
+
40
+ // Turn 2 (new request, agent recreated)
41
+ const agent2 = new Agent({ context: { userId: "123", preferences: {} } });
42
+ await agent2.respond({ history: [message2] });
43
+ // ❌ Lost the context changes from Turn 1!
44
+ ```
45
+
46
+ **Problem:** When agents are recreated (e.g., across HTTP requests), context updates are lost.
47
+
48
+ ---
49
+
50
+ ## 💡 Solution: Context Lifecycle Hooks
51
+
52
+ The framework provides **lifecycle hooks** to integrate with your persistence layer (database, cache, etc.).
53
+
54
+ ### Pattern 1: `beforeRespond` + `onContextUpdate` (Recommended)
55
+
56
+ ```typescript
57
+ import { Agent, type ContextLifecycleHooks } from "@falai/agent";
58
+
59
+ // Define hooks
60
+ const hooks: ContextLifecycleHooks<MyContext> = {
61
+ // Load fresh context before each response
62
+ beforeRespond: async (currentContext) => {
63
+ return await database.loadContext(sessionId);
64
+ },
65
+
66
+ // Persist context after updates
67
+ onContextUpdate: async (newContext, previousContext) => {
68
+ await database.saveContext(sessionId, newContext);
69
+ },
70
+ };
71
+
72
+ // Create agent with hooks
73
+ const agent = new Agent({
74
+ name: "Bot",
75
+ ai: provider,
76
+ context: initialContext,
77
+ hooks, // 🔑 Enable persistence
78
+ });
79
+ ```
80
+
81
+ **How it works:**
82
+
83
+ 1. `beforeRespond` fetches fresh context from your database before `respond()` is called
84
+ 2. Tools can update context using `toolContext.updateContext()` or returning `{ contextUpdate }`
85
+ 3. `onContextUpdate` automatically persists changes to your database
86
+ 4. Next request creates a new agent, `beforeRespond` loads the updated context
87
+
88
+ ---
89
+
90
+ ## 🔧 Tool Context Updates
91
+
92
+ Tools can update context in **two ways**:
93
+
94
+ ### Option A: Return `contextUpdate`
95
+
96
+ ```typescript
97
+ const saveName = defineTool<MyContext, [name: string], boolean>(
98
+ "save_name",
99
+ async (toolContext, name) => {
100
+ return {
101
+ data: true,
102
+ contextUpdate: {
103
+ userName: name,
104
+ updatedAt: new Date(),
105
+ },
106
+ };
107
+ }
108
+ );
109
+ ```
110
+
111
+ **Pros:**
112
+
113
+ - Declarative and functional
114
+ - Easy to test
115
+ - Context update is part of the result
116
+
117
+ ### Option B: Call `updateContext()` directly
118
+
119
+ ```typescript
120
+ const saveName = defineTool<MyContext, [name: string], boolean>(
121
+ "save_name",
122
+ async (toolContext, name) => {
123
+ await toolContext.updateContext({
124
+ userName: name,
125
+ updatedAt: new Date(),
126
+ });
127
+
128
+ return { data: true };
129
+ }
130
+ );
131
+ ```
132
+
133
+ **Pros:**
134
+
135
+ - More imperative and direct
136
+ - Can update context at any point in the tool
137
+ - Useful for complex logic
138
+
139
+ **Both approaches trigger the `onContextUpdate` hook automatically.**
140
+
141
+ ---
142
+
143
+ ## 🌐 Context Provider Pattern
144
+
145
+ For scenarios where context is **always loaded from an external source**, use the `contextProvider` pattern:
146
+
147
+ ```typescript
148
+ const agent = new Agent({
149
+ name: "Bot",
150
+ ai: provider,
151
+
152
+ // Instead of static context:
153
+ contextProvider: async () => {
154
+ // Fetch context fresh on every respond() call
155
+ return await database.loadContext(sessionId);
156
+ },
157
+
158
+ hooks: {
159
+ // Still persist updates
160
+ onContextUpdate: async (newContext) => {
161
+ await database.saveContext(sessionId, newContext);
162
+ },
163
+ },
164
+ });
165
+ ```
166
+
167
+ **When to use:**
168
+
169
+ - Context is always loaded from a database/cache
170
+ - You never have a static starting context
171
+ - You want guaranteed fresh data on every request
172
+
173
+ **Difference from `beforeRespond`:**
174
+
175
+ - `contextProvider`: **Replaces** the context entirely
176
+ - `beforeRespond`: **Updates** the existing context (can access previous state)
177
+
178
+ ---
179
+
180
+ ## 🎯 Complete Example: Multi-Turn Onboarding
181
+
182
+ ```typescript
183
+ import { Agent, defineTool } from "@falai/agent";
184
+
185
+ interface OnboardingContext {
186
+ sessionId: string;
187
+ userId: string;
188
+ businessName?: string;
189
+ industry?: string;
190
+ completedSteps: string[];
191
+ }
192
+
193
+ // Database simulation
194
+ const db = {
195
+ async loadSession(sessionId: string) {
196
+ return await database.findSession(sessionId);
197
+ },
198
+ async saveSession(sessionId: string, data: Partial<OnboardingContext>) {
199
+ await database.updateSession(sessionId, data);
200
+ },
201
+ };
202
+
203
+ // Factory function for creating agents
204
+ async function createOnboardingAgent(sessionId: string) {
205
+ const session = await db.loadSession(sessionId);
206
+
207
+ const agent = new Agent<OnboardingContext>({
208
+ name: "OnboardingBot",
209
+ ai: provider,
210
+ context: {
211
+ sessionId,
212
+ userId: session.userId,
213
+ businessName: session.businessName,
214
+ industry: session.industry,
215
+ completedSteps: session.completedSteps || [],
216
+ },
217
+ hooks: {
218
+ // Load fresh context before responding
219
+ beforeRespond: async (current) => {
220
+ const fresh = await db.loadSession(sessionId);
221
+ return { ...current, ...fresh };
222
+ },
223
+
224
+ // Persist context after updates
225
+ onContextUpdate: async (newContext) => {
226
+ await db.saveSession(sessionId, {
227
+ businessName: newContext.businessName,
228
+ industry: newContext.industry,
229
+ completedSteps: newContext.completedSteps,
230
+ });
231
+ },
232
+ },
233
+ });
234
+
235
+ // Define tools with context updates
236
+ const saveBusinessName = defineTool(
237
+ "save_business_name",
238
+ async (ctx, name: string) => {
239
+ return {
240
+ data: true,
241
+ contextUpdate: {
242
+ businessName: name,
243
+ completedSteps: [...ctx.context.completedSteps, "business_name"],
244
+ },
245
+ };
246
+ }
247
+ );
248
+
249
+ const saveIndustry = defineTool(
250
+ "save_industry",
251
+ async (ctx, industry: string) => {
252
+ // Alternative: use updateContext directly
253
+ await ctx.updateContext({
254
+ industry,
255
+ completedSteps: [...ctx.context.completedSteps, "industry"],
256
+ });
257
+
258
+ return { data: true };
259
+ }
260
+ );
261
+
262
+ // Build conversation routes...
263
+ const route = agent.createRoute({ title: "Onboarding" });
264
+ // ...
265
+
266
+ return agent;
267
+ }
268
+
269
+ // Usage across multiple turns
270
+ async function handleUserMessage(sessionId: string, message: string) {
271
+ // Recreate agent for each turn (context is loaded fresh)
272
+ const agent = await createOnboardingAgent(sessionId);
273
+
274
+ const response = await agent.respond({
275
+ history: [createMessageEvent(EventSource.CUSTOMER, "User", message)],
276
+ });
277
+
278
+ return response.message;
279
+ }
280
+ ```
281
+
282
+ ---
283
+
284
+ ## 📋 Best Practices
285
+
286
+ ### ✅ DO
287
+
288
+ - **Recreate agents** for each request in multi-turn conversations
289
+ - **Use lifecycle hooks** to integrate with your database
290
+ - **Store context** in your database/cache (Redis, PostgreSQL, etc.)
291
+ - **Load fresh context** via `beforeRespond` or `contextProvider`
292
+ - **Test persistence** by simulating multiple turns
293
+ - **Handle errors** in hooks gracefully (fallback to current context)
294
+
295
+ ### ❌ DON'T
296
+
297
+ - **Cache agent instances** across requests (context gets stale)
298
+ - **Mutate context directly** without using `updateContext()` or `contextUpdate`
299
+ - **Rely on in-memory state** for multi-turn conversations
300
+ - **Forget to handle** `onContextUpdate` failures (could lose data)
301
+ - **Mix** `context` and `contextProvider` (will throw an error)
302
+
303
+ ---
304
+
305
+ ## 🔍 Debugging Context Issues
306
+
307
+ ### Problem: Context updates aren't persisting
308
+
309
+ **Check:**
310
+
311
+ 1. Are you using `onContextUpdate` hook?
312
+ 2. Is your database save actually working?
313
+ 3. Are you recreating the agent with fresh context each turn?
314
+
315
+ ```typescript
316
+ // Debug your hooks
317
+ hooks: {
318
+ beforeRespond: async (current) => {
319
+ console.log("📥 Loading context:", current);
320
+ const fresh = await db.load(sessionId);
321
+ console.log("✅ Loaded fresh:", fresh);
322
+ return fresh;
323
+ },
324
+
325
+ onContextUpdate: async (newContext) => {
326
+ console.log("💾 Saving context:", newContext);
327
+ await db.save(sessionId, newContext);
328
+ console.log("✅ Saved successfully");
329
+ },
330
+ },
331
+ ```
332
+
333
+ ### Problem: Context is null/undefined
334
+
335
+ **Check:**
336
+
337
+ 1. Did you provide either `context` or `contextProvider`?
338
+ 2. Is your `contextProvider` returning valid data?
339
+ 3. Is `beforeRespond` returning valid context?
340
+
341
+ ```typescript
342
+ // Validate your contextProvider
343
+ contextProvider: async () => {
344
+ const ctx = await db.load(sessionId);
345
+ if (!ctx) {
346
+ console.error("❌ Context not found!");
347
+ throw new Error("Session not found");
348
+ }
349
+ return ctx;
350
+ },
351
+ ```
352
+
353
+ ### Problem: Tools not updating context
354
+
355
+ **Check:**
356
+
357
+ 1. Are you returning `{ contextUpdate }` or calling `toolContext.updateContext()`?
358
+ 2. Is `onContextUpdate` being called? (Add console.log)
359
+ 3. Are your tools being executed at all?
360
+
361
+ ---
362
+
363
+ ## 🚀 Advanced Patterns
364
+
365
+ ### Pattern: Context Versioning
366
+
367
+ ```typescript
368
+ interface VersionedContext extends MyContext {
369
+ version: number;
370
+ }
371
+
372
+ hooks: {
373
+ onContextUpdate: async (newContext) => {
374
+ await db.save(sessionId, {
375
+ ...newContext,
376
+ version: newContext.version + 1,
377
+ });
378
+ },
379
+
380
+ beforeRespond: async (current) => {
381
+ const fresh = await db.load(sessionId);
382
+ if (fresh.version > current.version) {
383
+ console.log("⚠️ Context changed by another request!");
384
+ }
385
+ return fresh;
386
+ },
387
+ },
388
+ ```
389
+
390
+ ### Pattern: Selective Persistence
391
+
392
+ ```typescript
393
+ // Only persist specific fields
394
+ hooks: {
395
+ onContextUpdate: async (newContext) => {
396
+ // Don't persist temporary/computed fields
397
+ const { tempData, ...persistable } = newContext;
398
+ await db.save(sessionId, persistable);
399
+ },
400
+ },
401
+ ```
402
+
403
+ ### Pattern: Multi-User Context
404
+
405
+ ```typescript
406
+ interface MultiUserContext {
407
+ conversationId: string;
408
+ participants: Map<string, UserData>;
409
+ }
410
+
411
+ hooks: {
412
+ beforeRespond: async (current) => {
413
+ // Load all participant data
414
+ const conversation = await db.loadConversation(conversationId);
415
+ return {
416
+ conversationId,
417
+ participants: new Map(conversation.participants),
418
+ };
419
+ },
420
+ },
421
+ ```
422
+
423
+ ---
424
+
425
+ ## 📚 Related Resources
426
+
427
+ - [Complete Example: Persistent Onboarding](../examples/persistent-onboarding.ts)
428
+ - [API Reference: AgentOptions](./API_REFERENCE.md#agentoptions)
429
+ - [API Reference: ContextLifecycleHooks](./API_REFERENCE.md#contextlifecyclehooks)
430
+ - [Getting Started](./GETTING_STARTED.md)
431
+
432
+ ---
433
+
434
+ ## 🆘 Need Help?
435
+
436
+ If you're still having issues:
437
+
438
+ 1. Check the [examples](../examples/) for working implementations
439
+ 2. Review the [API Reference](./API_REFERENCE.md) for detailed type information
440
+ 3. Open an issue on GitHub with your use case
441
+
442
+ **Remember:** The key to persistent conversations is:
443
+
444
+ 1. **Recreate agents** each turn
445
+ 2. **Load fresh context** via hooks/provider
446
+ 3. **Persist updates** via `onContextUpdate`
447
+ 4. **Never cache** agent instances across requests