@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.
- package/README.md +61 -0
- package/dist/cjs/core/Agent.d.ts +11 -0
- package/dist/cjs/core/Agent.d.ts.map +1 -1
- package/dist/cjs/core/Agent.js +44 -2
- package/dist/cjs/core/Agent.js.map +1 -1
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/types/agent.d.ts +24 -0
- package/dist/cjs/types/agent.d.ts.map +1 -1
- package/dist/cjs/types/tool.d.ts +6 -2
- package/dist/cjs/types/tool.d.ts.map +1 -1
- package/dist/core/Agent.d.ts +11 -0
- package/dist/core/Agent.d.ts.map +1 -1
- package/dist/core/Agent.js +44 -2
- package/dist/core/Agent.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/types/agent.d.ts +24 -0
- package/dist/types/agent.d.ts.map +1 -1
- package/dist/types/tool.d.ts +6 -2
- package/dist/types/tool.d.ts.map +1 -1
- package/docs/CONTEXT_MANAGEMENT.md +447 -0
- package/examples/persistent-onboarding.ts +464 -0
- package/package.json +1 -1
- package/src/core/Agent.ts +56 -2
- package/src/index.ts +2 -0
- package/src/types/agent.ts +32 -0
- package/src/types/tool.ts +6 -2
|
@@ -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
|