@falai/agent 0.1.4 → 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.
Files changed (97) hide show
  1. package/README.md +117 -1
  2. package/dist/cjs/core/Agent.d.ts +11 -0
  3. package/dist/cjs/core/Agent.d.ts.map +1 -1
  4. package/dist/cjs/core/Agent.js +44 -2
  5. package/dist/cjs/core/Agent.js.map +1 -1
  6. package/dist/cjs/core/Events.d.ts +2 -2
  7. package/dist/cjs/core/Events.d.ts.map +1 -1
  8. package/dist/cjs/core/Events.js +4 -4
  9. package/dist/cjs/core/Events.js.map +1 -1
  10. package/dist/cjs/core/Observation.d.ts.map +1 -1
  11. package/dist/cjs/core/Observation.js +3 -2
  12. package/dist/cjs/core/Observation.js.map +1 -1
  13. package/dist/cjs/core/Route.d.ts.map +1 -1
  14. package/dist/cjs/core/Route.js +3 -4
  15. package/dist/cjs/core/Route.js.map +1 -1
  16. package/dist/cjs/core/State.d.ts +1 -1
  17. package/dist/cjs/core/State.d.ts.map +1 -1
  18. package/dist/cjs/core/State.js +4 -3
  19. package/dist/cjs/core/State.js.map +1 -1
  20. package/dist/cjs/core/Tool.d.ts +1 -0
  21. package/dist/cjs/core/Tool.d.ts.map +1 -1
  22. package/dist/cjs/core/Tool.js +3 -2
  23. package/dist/cjs/core/Tool.js.map +1 -1
  24. package/dist/cjs/index.d.ts +2 -1
  25. package/dist/cjs/index.d.ts.map +1 -1
  26. package/dist/cjs/index.js +7 -1
  27. package/dist/cjs/index.js.map +1 -1
  28. package/dist/cjs/types/agent.d.ts +26 -2
  29. package/dist/cjs/types/agent.d.ts.map +1 -1
  30. package/dist/cjs/types/observation.d.ts +2 -0
  31. package/dist/cjs/types/observation.d.ts.map +1 -1
  32. package/dist/cjs/types/route.d.ts +2 -0
  33. package/dist/cjs/types/route.d.ts.map +1 -1
  34. package/dist/cjs/types/tool.d.ts +6 -2
  35. package/dist/cjs/types/tool.d.ts.map +1 -1
  36. package/dist/cjs/utils/id.d.ts +25 -0
  37. package/dist/cjs/utils/id.d.ts.map +1 -0
  38. package/dist/cjs/utils/id.js +71 -0
  39. package/dist/cjs/utils/id.js.map +1 -0
  40. package/dist/core/Agent.d.ts +11 -0
  41. package/dist/core/Agent.d.ts.map +1 -1
  42. package/dist/core/Agent.js +44 -2
  43. package/dist/core/Agent.js.map +1 -1
  44. package/dist/core/Events.d.ts +2 -2
  45. package/dist/core/Events.d.ts.map +1 -1
  46. package/dist/core/Events.js +4 -4
  47. package/dist/core/Events.js.map +1 -1
  48. package/dist/core/Observation.d.ts.map +1 -1
  49. package/dist/core/Observation.js +3 -2
  50. package/dist/core/Observation.js.map +1 -1
  51. package/dist/core/Route.d.ts.map +1 -1
  52. package/dist/core/Route.js +3 -4
  53. package/dist/core/Route.js.map +1 -1
  54. package/dist/core/State.d.ts +1 -1
  55. package/dist/core/State.d.ts.map +1 -1
  56. package/dist/core/State.js +4 -3
  57. package/dist/core/State.js.map +1 -1
  58. package/dist/core/Tool.d.ts +1 -0
  59. package/dist/core/Tool.d.ts.map +1 -1
  60. package/dist/core/Tool.js +3 -2
  61. package/dist/core/Tool.js.map +1 -1
  62. package/dist/index.d.ts +2 -1
  63. package/dist/index.d.ts.map +1 -1
  64. package/dist/index.js +2 -0
  65. package/dist/index.js.map +1 -1
  66. package/dist/types/agent.d.ts +26 -2
  67. package/dist/types/agent.d.ts.map +1 -1
  68. package/dist/types/observation.d.ts +2 -0
  69. package/dist/types/observation.d.ts.map +1 -1
  70. package/dist/types/route.d.ts +2 -0
  71. package/dist/types/route.d.ts.map +1 -1
  72. package/dist/types/tool.d.ts +6 -2
  73. package/dist/types/tool.d.ts.map +1 -1
  74. package/dist/utils/id.d.ts +25 -0
  75. package/dist/utils/id.d.ts.map +1 -0
  76. package/dist/utils/id.js +65 -0
  77. package/dist/utils/id.js.map +1 -0
  78. package/docs/API_REFERENCE.md +122 -6
  79. package/docs/CONSTRUCTOR_OPTIONS.md +43 -36
  80. package/docs/CONTEXT_MANAGEMENT.md +447 -0
  81. package/docs/GETTING_STARTED.md +2 -0
  82. package/docs/PROVIDERS.md +3 -0
  83. package/examples/declarative-agent.ts +31 -7
  84. package/examples/persistent-onboarding.ts +464 -0
  85. package/package.json +1 -1
  86. package/src/core/Agent.ts +56 -2
  87. package/src/core/Events.ts +6 -4
  88. package/src/core/Observation.ts +3 -3
  89. package/src/core/Route.ts +3 -5
  90. package/src/core/State.ts +5 -4
  91. package/src/core/Tool.ts +4 -3
  92. package/src/index.ts +10 -0
  93. package/src/types/agent.ts +36 -2
  94. package/src/types/observation.ts +2 -0
  95. package/src/types/route.ts +2 -0
  96. package/src/types/tool.ts +6 -2
  97. package/src/utils/id.ts +74 -0
@@ -6,8 +6,9 @@
6
6
  * - Terms (glossary)
7
7
  * - Guidelines (behavior rules)
8
8
  * - Capabilities
9
- * - Routes with nested guidelines
10
- * - Observations with route references
9
+ * - Routes with nested guidelines and custom IDs
10
+ * - Observations with route references and custom IDs
11
+ * - Custom timestamps for events
11
12
  */
12
13
 
13
14
  import {
@@ -29,13 +30,16 @@ interface HealthcareContext {
29
30
  patientName: string;
30
31
  }
31
32
 
32
- // Define tools
33
+ // Define tools with custom IDs (optional - IDs are deterministic by default)
33
34
  const getInsuranceProviders = defineTool<HealthcareContext, [], string[]>(
34
35
  "get_insurance_providers",
35
36
  async () => {
36
37
  return { data: ["MegaCare Insurance", "HealthFirst", "WellnessPlus"] };
37
38
  },
38
- { description: "Retrieves list of accepted insurance providers" }
39
+ {
40
+ id: "healthcare_insurance_providers", // Custom ID for persistence
41
+ description: "Retrieves list of accepted insurance providers",
42
+ }
39
43
  );
40
44
 
41
45
  const getAvailableSlots = defineTool<
@@ -53,7 +57,10 @@ const getAvailableSlots = defineTool<
53
57
  ],
54
58
  };
55
59
  },
56
- { description: "Gets available appointment slots" }
60
+ {
61
+ id: "healthcare_available_slots", // Custom ID
62
+ description: "Gets available appointment slots",
63
+ }
57
64
  );
58
65
 
59
66
  const getLabResults = defineTool<
@@ -70,7 +77,10 @@ const getLabResults = defineTool<
70
77
  },
71
78
  };
72
79
  },
73
- { description: "Retrieves patient lab results" }
80
+ {
81
+ id: "healthcare_lab_results", // Custom ID
82
+ description: "Retrieves patient lab results",
83
+ }
74
84
  );
75
85
 
76
86
  // Declarative configuration
@@ -126,6 +136,7 @@ const capabilities: Capability[] = [
126
136
 
127
137
  const routes: RouteOptions[] = [
128
138
  {
139
+ id: "route_schedule_appointment", // Custom ID ensures consistency across restarts
129
140
  title: "Schedule Appointment",
130
141
  description: "Helps the patient schedule an appointment",
131
142
  conditions: ["The patient wants to schedule an appointment"],
@@ -139,6 +150,7 @@ const routes: RouteOptions[] = [
139
150
  ],
140
151
  },
141
152
  {
153
+ id: "route_check_lab_results", // Custom ID
142
154
  title: "Check Lab Results",
143
155
  description: "Retrieves and explains patient lab results",
144
156
  conditions: ["The patient wants to see their lab results"],
@@ -155,6 +167,7 @@ const routes: RouteOptions[] = [
155
167
 
156
168
  const observations: ObservationOptions[] = [
157
169
  {
170
+ id: "obs_visit_followup", // Custom ID for tracking
158
171
  description:
159
172
  "The patient asks to follow up on their visit, but it's not clear in which way",
160
173
  routeRefs: ["Schedule Appointment", "Check Lab Results"], // Reference by title
@@ -172,6 +185,7 @@ const agent = new Agent<HealthcareContext>({
172
185
  },
173
186
  ai: new GeminiProvider({
174
187
  apiKey: process.env.GEMINI_API_KEY || "demo-key",
188
+ model: "models/gemini-2.5-flash",
175
189
  }),
176
190
  // Declarative initialization
177
191
  terms,
@@ -196,19 +210,29 @@ agent
196
210
 
197
211
  // Example usage
198
212
  async function main() {
213
+ // Create events with custom timestamps (useful for historical data)
199
214
  const history = [
200
215
  createMessageEvent(
201
216
  EventSource.CUSTOMER,
202
217
  "Alice",
203
- "Hi, I need to follow up on my recent visit"
218
+ "Hi, I need to follow up on my recent visit",
219
+ "2025-10-13T14:30:00Z" // Optional custom timestamp
204
220
  ),
205
221
  ];
206
222
 
207
223
  const response = await agent.respond({ history });
208
224
  console.log("Agent:", response.message);
225
+ console.log("Route chosen:", response.route?.title);
226
+ console.log("Route ID:", response.route?.id); // Custom ID is preserved
209
227
 
210
228
  // The agent will use the observation to disambiguate
211
229
  // and ask which type of follow-up the patient needs
230
+
231
+ // Note: Custom IDs ensure consistency across server restarts
232
+ // This is crucial for:
233
+ // - Storing conversation state in databases
234
+ // - Tracking metrics and analytics
235
+ // - Referencing routes in external systems
212
236
  }
213
237
 
214
238
  // Uncomment to run:
@@ -0,0 +1,464 @@
1
+ /**
2
+ * Persistent multi-turn onboarding agent example
3
+ * Demonstrates context lifecycle management for stateful conversations
4
+ */
5
+
6
+ import {
7
+ Agent,
8
+ defineTool,
9
+ GeminiProvider,
10
+ END_ROUTE,
11
+ EventSource,
12
+ createMessageEvent,
13
+ type ContextLifecycleHooks,
14
+ } from "../src/index";
15
+
16
+ // ============================================================================
17
+ // DATABASE SIMULATION
18
+ // ============================================================================
19
+
20
+ interface SessionData {
21
+ sessionId: string;
22
+ userId: string;
23
+ collectedData: {
24
+ businessName?: string;
25
+ businessDescription?: string;
26
+ industry?: string;
27
+ contactEmail?: string;
28
+ };
29
+ completedSteps: string[];
30
+ lastUpdated: Date;
31
+ }
32
+
33
+ // Simple in-memory database simulation
34
+ const database = new Map<string, SessionData>();
35
+
36
+ const db = {
37
+ sessions: {
38
+ async findById(sessionId: string): Promise<SessionData | undefined> {
39
+ return database.get(sessionId);
40
+ },
41
+
42
+ async update(
43
+ sessionId: string,
44
+ updates: Partial<SessionData>
45
+ ): Promise<void> {
46
+ const existing = database.get(sessionId);
47
+ if (existing) {
48
+ database.set(sessionId, {
49
+ ...existing,
50
+ ...updates,
51
+ lastUpdated: new Date(),
52
+ });
53
+ }
54
+ },
55
+
56
+ async create(sessionData: SessionData): Promise<void> {
57
+ database.set(sessionData.sessionId, sessionData);
58
+ },
59
+ },
60
+ };
61
+
62
+ // ============================================================================
63
+ // CONTEXT TYPE
64
+ // ============================================================================
65
+
66
+ interface OnboardingContext {
67
+ sessionId: string;
68
+ userId: string;
69
+ userName?: string;
70
+ collectedData: {
71
+ businessName?: string;
72
+ businessDescription?: string;
73
+ industry?: string;
74
+ contactEmail?: string;
75
+ };
76
+ completedSteps: string[];
77
+ }
78
+
79
+ // ============================================================================
80
+ // AGENT FACTORY WITH LIFECYCLE HOOKS
81
+ // ============================================================================
82
+
83
+ /**
84
+ * Creates an onboarding agent with persistent context management
85
+ *
86
+ * PATTERN 1: Factory + Lifecycle Hooks
87
+ * - Load fresh context from database before each response
88
+ * - Persist context updates automatically after changes
89
+ */
90
+ async function createPersistentOnboardingAgent(sessionId: string) {
91
+ // Load session from database
92
+ const session = await db.sessions.findById(sessionId);
93
+
94
+ if (!session) {
95
+ throw new Error(`Session ${sessionId} not found`);
96
+ }
97
+
98
+ // Define lifecycle hooks for automatic persistence
99
+ const hooks: ContextLifecycleHooks<OnboardingContext> = {
100
+ // Called before respond() - load fresh context from database
101
+ beforeRespond: async (currentContext) => {
102
+ console.log("🔄 Loading fresh context from database...");
103
+ const freshSession = await db.sessions.findById(sessionId);
104
+
105
+ if (!freshSession) {
106
+ return currentContext; // Fallback to current
107
+ }
108
+
109
+ return {
110
+ sessionId: freshSession.sessionId,
111
+ userId: freshSession.userId,
112
+ collectedData: freshSession.collectedData,
113
+ completedSteps: freshSession.completedSteps,
114
+ };
115
+ },
116
+
117
+ // Called after context updates - persist to database
118
+ onContextUpdate: async (newContext) => {
119
+ console.log("💾 Persisting context update to database...");
120
+ await db.sessions.update(sessionId, {
121
+ collectedData: newContext.collectedData,
122
+ completedSteps: newContext.completedSteps,
123
+ });
124
+ },
125
+ };
126
+
127
+ const provider = new GeminiProvider({
128
+ apiKey: process.env.GEMINI_API_KEY || "test-key",
129
+ model: "models/gemini-2.5-flash",
130
+ });
131
+
132
+ const agent = new Agent<OnboardingContext>({
133
+ name: "OnboardingBot",
134
+ description: "A friendly assistant that helps businesses get started",
135
+ goal: "Collect business information efficiently while being conversational",
136
+ ai: provider,
137
+ context: {
138
+ sessionId: session.sessionId,
139
+ userId: session.userId,
140
+ collectedData: session.collectedData,
141
+ completedSteps: session.completedSteps,
142
+ },
143
+ hooks, // Enable lifecycle hooks for persistence
144
+ });
145
+
146
+ // ============================================================================
147
+ // TOOLS (with context updates)
148
+ // ============================================================================
149
+
150
+ // OPTION 1: Using contextUpdate in return value
151
+ const saveBusinessInfo = defineTool<
152
+ OnboardingContext,
153
+ [name: string, description: string],
154
+ boolean
155
+ >(
156
+ "save_business_info",
157
+ async (toolContext, name, description) => {
158
+ console.log(`📝 Saving business info: ${name}`);
159
+
160
+ return {
161
+ data: true,
162
+ // Context update is automatically persisted via onContextUpdate hook
163
+ contextUpdate: {
164
+ collectedData: {
165
+ ...toolContext.context.collectedData,
166
+ businessName: name,
167
+ businessDescription: description,
168
+ },
169
+ completedSteps: [
170
+ ...toolContext.context.completedSteps,
171
+ "business_info",
172
+ ],
173
+ },
174
+ };
175
+ },
176
+ {
177
+ description: "Save business name and description",
178
+ }
179
+ );
180
+
181
+ // OPTION 2: Using updateContext method directly
182
+ const saveIndustry = defineTool<
183
+ OnboardingContext,
184
+ [industry: string],
185
+ boolean
186
+ >(
187
+ "save_industry",
188
+ async (toolContext, industry) => {
189
+ console.log(`🏭 Saving industry: ${industry}`);
190
+
191
+ // Direct context update (triggers onContextUpdate hook)
192
+ await toolContext.updateContext({
193
+ collectedData: {
194
+ ...toolContext.context.collectedData,
195
+ industry,
196
+ },
197
+ completedSteps: [...toolContext.context.completedSteps, "industry"],
198
+ });
199
+
200
+ return { data: true };
201
+ },
202
+ {
203
+ description: "Save business industry",
204
+ }
205
+ );
206
+
207
+ const saveContactEmail = defineTool<
208
+ OnboardingContext,
209
+ [email: string],
210
+ boolean
211
+ >(
212
+ "save_contact_email",
213
+ async (toolContext, email) => {
214
+ console.log(`📧 Saving contact email: ${email}`);
215
+
216
+ await toolContext.updateContext({
217
+ collectedData: {
218
+ ...toolContext.context.collectedData,
219
+ contactEmail: email,
220
+ },
221
+ completedSteps: [...toolContext.context.completedSteps, "contact"],
222
+ });
223
+
224
+ return { data: true };
225
+ },
226
+ {
227
+ description: "Save contact email",
228
+ }
229
+ );
230
+
231
+ // ============================================================================
232
+ // ONBOARDING ROUTE
233
+ // ============================================================================
234
+
235
+ const onboardingRoute = agent.createRoute({
236
+ title: "Business Onboarding",
237
+ description: "Guide user through business information collection",
238
+ conditions: ["User is onboarding their business"],
239
+ });
240
+
241
+ // Step 1: Collect business info
242
+ const askBusinessInfo = onboardingRoute.initialState.transitionTo({
243
+ chatState: "Ask for business name and a brief description",
244
+ });
245
+
246
+ const saveBusinessStep = askBusinessInfo.target.transitionTo({
247
+ toolState: saveBusinessInfo,
248
+ });
249
+
250
+ // Step 2: Collect industry
251
+ const askIndustry = saveBusinessStep.target.transitionTo({
252
+ chatState: "Ask what industry the business operates in",
253
+ });
254
+
255
+ const saveIndustryStep = askIndustry.target.transitionTo({
256
+ toolState: saveIndustry,
257
+ });
258
+
259
+ // Step 3: Collect contact
260
+ const askContact = saveIndustryStep.target.transitionTo({
261
+ chatState: "Ask for their contact email",
262
+ });
263
+
264
+ const saveContactStep = askContact.target.transitionTo({
265
+ toolState: saveContactEmail,
266
+ });
267
+
268
+ // Step 4: Confirmation
269
+ const confirm = saveContactStep.target.transitionTo({
270
+ chatState: "Summarize all collected information and ask for confirmation",
271
+ });
272
+
273
+ confirm.target.transitionTo({ state: END_ROUTE });
274
+
275
+ // Guidelines
276
+ onboardingRoute.createGuideline({
277
+ condition: "User provides invalid email format",
278
+ action: "Politely ask for a valid email address",
279
+ tags: ["validation"],
280
+ });
281
+
282
+ onboardingRoute.createGuideline({
283
+ condition: "User wants to skip a step",
284
+ action: "Explain why the information is important but allow them to skip",
285
+ tags: ["flexibility"],
286
+ });
287
+
288
+ agent.createGuideline({
289
+ condition: "User asks to start over",
290
+ action:
291
+ "Confirm they want to clear their progress, then restart the onboarding",
292
+ tags: ["reset"],
293
+ });
294
+
295
+ return agent;
296
+ }
297
+
298
+ // ============================================================================
299
+ // ALTERNATIVE PATTERN: CONTEXT PROVIDER
300
+ // ============================================================================
301
+
302
+ /**
303
+ * Creates an onboarding agent using the contextProvider pattern
304
+ *
305
+ * PATTERN 2: Context Provider (Always Fresh)
306
+ * - Context is fetched fresh on every respond() call
307
+ * - No need for beforeRespond hook
308
+ * - Still use onContextUpdate for persistence
309
+ */
310
+ async function createOnboardingAgentWithProvider(sessionId: string) {
311
+ const provider = new GeminiProvider({
312
+ apiKey: process.env.GEMINI_API_KEY || "test-key",
313
+ model: "models/gemini-2.5-flash",
314
+ });
315
+
316
+ const agent = new Agent<OnboardingContext>({
317
+ name: "OnboardingBot",
318
+ description: "A friendly assistant that helps businesses get started",
319
+ ai: provider,
320
+
321
+ // Context is always fetched fresh from database
322
+ contextProvider: async () => {
323
+ const session = await db.sessions.findById(sessionId);
324
+ if (!session) {
325
+ throw new Error(`Session ${sessionId} not found`);
326
+ }
327
+
328
+ return {
329
+ sessionId: session.sessionId,
330
+ userId: session.userId,
331
+ collectedData: session.collectedData,
332
+ completedSteps: session.completedSteps,
333
+ };
334
+ },
335
+
336
+ // Still persist updates
337
+ hooks: {
338
+ onContextUpdate: async (newContext) => {
339
+ await db.sessions.update(sessionId, {
340
+ collectedData: newContext.collectedData,
341
+ completedSteps: newContext.completedSteps,
342
+ });
343
+ },
344
+ },
345
+ });
346
+
347
+ // ... rest of agent setup (same as above)
348
+
349
+ return agent;
350
+ }
351
+
352
+ // ============================================================================
353
+ // USAGE EXAMPLE
354
+ // ============================================================================
355
+
356
+ async function main() {
357
+ const sessionId = "session_123";
358
+ const userId = "user_456";
359
+
360
+ // Initialize session in database
361
+ await db.sessions.create({
362
+ sessionId,
363
+ userId,
364
+ collectedData: {},
365
+ completedSteps: [],
366
+ lastUpdated: new Date(),
367
+ });
368
+
369
+ console.log("=== MULTI-TURN CONVERSATION SIMULATION ===\n");
370
+
371
+ // Turn 1: Start onboarding
372
+ console.log("📱 Turn 1: User starts onboarding");
373
+ const agent1 = await createPersistentOnboardingAgent(sessionId);
374
+ const response1 = await agent1.respond({
375
+ history: [
376
+ createMessageEvent(
377
+ EventSource.CUSTOMER,
378
+ "Alice",
379
+ "Hi, I want to onboard my business"
380
+ ),
381
+ ],
382
+ });
383
+ console.log("🤖 Bot:", response1.message);
384
+ console.log("📊 Context after turn 1:", agent1["context"]);
385
+ console.log();
386
+
387
+ // Turn 2: User provides business info
388
+ // NOTE: We create a NEW agent instance - context is loaded from database
389
+ console.log("📱 Turn 2: User provides business info");
390
+ const agent2 = await createPersistentOnboardingAgent(sessionId);
391
+ const response2 = await agent2.respond({
392
+ history: [
393
+ createMessageEvent(
394
+ EventSource.CUSTOMER,
395
+ "Alice",
396
+ "My business is called 'TechFlow' and we build AI-powered workflow automation tools"
397
+ ),
398
+ ],
399
+ });
400
+ console.log("🤖 Bot:", response2.message);
401
+ console.log("📊 Context after turn 2:", agent2["context"]);
402
+ console.log();
403
+
404
+ // Turn 3: User provides industry
405
+ console.log("📱 Turn 3: User provides industry");
406
+ const agent3 = await createPersistentOnboardingAgent(sessionId);
407
+ const response3 = await agent3.respond({
408
+ history: [
409
+ createMessageEvent(
410
+ EventSource.CUSTOMER,
411
+ "Alice",
412
+ "We're in the SaaS industry"
413
+ ),
414
+ ],
415
+ });
416
+ console.log("🤖 Bot:", response3.message);
417
+ console.log("📊 Context after turn 3:", agent3["context"]);
418
+ console.log();
419
+
420
+ // Verify persistence
421
+ console.log("=== PERSISTENCE VERIFICATION ===");
422
+ const finalSession = await db.sessions.findById(sessionId);
423
+ console.log(
424
+ "💾 Final persisted session:",
425
+ JSON.stringify(finalSession, null, 2)
426
+ );
427
+ }
428
+
429
+ // ============================================================================
430
+ // KEY PATTERNS DEMONSTRATED
431
+ // ============================================================================
432
+
433
+ /*
434
+ * ✅ PATTERN 1: Lifecycle Hooks (Recommended for most cases)
435
+ * - beforeRespond: Load fresh context before each response
436
+ * - onContextUpdate: Persist context after updates
437
+ * - Works with both static context and updates
438
+ *
439
+ * ✅ PATTERN 2: Context Provider (For always-fresh context)
440
+ * - contextProvider: Function that returns fresh context
441
+ * - onContextUpdate: Still needed for persistence
442
+ * - Best when context is always loaded from external source
443
+ *
444
+ * ✅ PATTERN 3: Tool Context Updates (Two options)
445
+ * - Option A: Return { data, contextUpdate } in tool result
446
+ * - Option B: Call toolContext.updateContext() directly
447
+ * - Both trigger onContextUpdate hook automatically
448
+ *
449
+ * ✅ PATTERN 4: Explicit Updates
450
+ * - agent.updateContext() for manual updates
451
+ * - Triggers onContextUpdate hook
452
+ * - Useful outside of tool execution
453
+ *
454
+ * ❌ ANTI-PATTERN: Caching Agents
455
+ * - DON'T cache agent instances across requests
456
+ * - DO recreate agents with fresh context
457
+ * - Context gets stale if agent is cached
458
+ */
459
+
460
+ if (import.meta.url === `file://${process.argv[1]}`) {
461
+ main().catch(console.error);
462
+ }
463
+
464
+ export { createPersistentOnboardingAgent, createOnboardingAgentWithProvider };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@falai/agent",
3
- "version": "0.1.4",
3
+ "version": "0.2.0",
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",
package/src/core/Agent.ts CHANGED
@@ -27,6 +27,7 @@ export class Agent<TContext = unknown> {
27
27
  private routes: Route<TContext>[] = [];
28
28
  private observations: Observation[] = [];
29
29
  private domainRegistry = new DomainRegistry();
30
+ private context: TContext | undefined;
30
31
 
31
32
  /**
32
33
  * Dynamic domain property - populated via addDomain
@@ -39,6 +40,16 @@ export class Agent<TContext = unknown> {
39
40
  this.options.maxEngineIterations = 1;
40
41
  }
41
42
 
43
+ // Validate context configuration
44
+ if (options.context !== undefined && options.contextProvider) {
45
+ throw new Error(
46
+ "Cannot provide both 'context' and 'contextProvider'. Choose one."
47
+ );
48
+ }
49
+
50
+ // Initialize context if provided
51
+ this.context = options.context;
52
+
42
53
  // Initialize from options
43
54
  if (options.terms) {
44
55
  this.terms = [...options.terms];
@@ -167,6 +178,39 @@ export class Agent<TContext = unknown> {
167
178
  this.domain[name] = domainObject;
168
179
  }
169
180
 
181
+ /**
182
+ * Update the agent's context
183
+ * Triggers the onContextUpdate lifecycle hook if configured
184
+ */
185
+ async updateContext(updates: Partial<TContext>): Promise<void> {
186
+ const previousContext = this.context;
187
+
188
+ // Merge updates with current context
189
+ this.context = {
190
+ ...(this.context as Record<string, unknown>),
191
+ ...(updates as Record<string, unknown>),
192
+ } as TContext;
193
+
194
+ // Trigger lifecycle hook if configured
195
+ if (this.options.hooks?.onContextUpdate && previousContext !== undefined) {
196
+ await this.options.hooks.onContextUpdate(this.context, previousContext);
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Get current context (fetches from provider if configured)
202
+ * @internal
203
+ */
204
+ private async getContext(): Promise<TContext | undefined> {
205
+ // If context provider is configured, use it to fetch fresh context
206
+ if (this.options.contextProvider) {
207
+ return await this.options.contextProvider();
208
+ }
209
+
210
+ // Otherwise return the stored context
211
+ return this.context;
212
+ }
213
+
170
214
  /**
171
215
  * Generate a response based on history and context
172
216
  */
@@ -183,9 +227,19 @@ export class Agent<TContext = unknown> {
183
227
  }> {
184
228
  const { history, contextOverride, signal } = params;
185
229
 
186
- // Merge context
230
+ // Get current context (may fetch from provider)
231
+ let currentContext = await this.getContext();
232
+
233
+ // Call beforeRespond hook if configured
234
+ if (this.options.hooks?.beforeRespond && currentContext !== undefined) {
235
+ currentContext = await this.options.hooks.beforeRespond(currentContext);
236
+ // Update stored context with the result from beforeRespond
237
+ this.context = currentContext;
238
+ }
239
+
240
+ // Merge context with override
187
241
  const effectiveContext = {
188
- ...(this.options.context as Record<string, unknown>),
242
+ ...(currentContext as Record<string, unknown>),
189
243
  ...(contextOverride as Record<string, unknown>),
190
244
  } as TContext;
191
245
 
@@ -70,7 +70,8 @@ export function adaptEvent(e: Event | EmittedEvent): string {
70
70
  export function createMessageEvent(
71
71
  source: EventSource,
72
72
  participantName: string,
73
- message: string
73
+ message: string,
74
+ timestamp?: string
74
75
  ): Event<MessageEventData> {
75
76
  return {
76
77
  kind: EventKind.MESSAGE,
@@ -79,7 +80,7 @@ export function createMessageEvent(
79
80
  participant: { display_name: participantName },
80
81
  message,
81
82
  },
82
- timestamp: new Date().toISOString(),
83
+ timestamp: timestamp || new Date().toISOString(),
83
84
  };
84
85
  }
85
86
 
@@ -88,7 +89,8 @@ export function createMessageEvent(
88
89
  */
89
90
  export function createToolEvent(
90
91
  source: EventSource,
91
- toolCalls: ToolCall[]
92
+ toolCalls: ToolCall[],
93
+ timestamp?: string
92
94
  ): Event<ToolEventData> {
93
95
  return {
94
96
  kind: EventKind.TOOL,
@@ -96,6 +98,6 @@ export function createToolEvent(
96
98
  data: {
97
99
  tool_calls: toolCalls,
98
100
  },
99
- timestamp: new Date().toISOString(),
101
+ timestamp: timestamp || new Date().toISOString(),
100
102
  };
101
103
  }