@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.
- package/README.md +117 -1
- 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/core/Events.d.ts +2 -2
- package/dist/cjs/core/Events.d.ts.map +1 -1
- package/dist/cjs/core/Events.js +4 -4
- package/dist/cjs/core/Events.js.map +1 -1
- package/dist/cjs/core/Observation.d.ts.map +1 -1
- package/dist/cjs/core/Observation.js +3 -2
- package/dist/cjs/core/Observation.js.map +1 -1
- package/dist/cjs/core/Route.d.ts.map +1 -1
- package/dist/cjs/core/Route.js +3 -4
- package/dist/cjs/core/Route.js.map +1 -1
- package/dist/cjs/core/State.d.ts +1 -1
- package/dist/cjs/core/State.d.ts.map +1 -1
- package/dist/cjs/core/State.js +4 -3
- package/dist/cjs/core/State.js.map +1 -1
- package/dist/cjs/core/Tool.d.ts +1 -0
- package/dist/cjs/core/Tool.d.ts.map +1 -1
- package/dist/cjs/core/Tool.js +3 -2
- package/dist/cjs/core/Tool.js.map +1 -1
- package/dist/cjs/index.d.ts +2 -1
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +7 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/types/agent.d.ts +26 -2
- package/dist/cjs/types/agent.d.ts.map +1 -1
- package/dist/cjs/types/observation.d.ts +2 -0
- package/dist/cjs/types/observation.d.ts.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/types/tool.d.ts +6 -2
- package/dist/cjs/types/tool.d.ts.map +1 -1
- package/dist/cjs/utils/id.d.ts +25 -0
- package/dist/cjs/utils/id.d.ts.map +1 -0
- package/dist/cjs/utils/id.js +71 -0
- package/dist/cjs/utils/id.js.map +1 -0
- 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/core/Events.d.ts +2 -2
- package/dist/core/Events.d.ts.map +1 -1
- package/dist/core/Events.js +4 -4
- package/dist/core/Events.js.map +1 -1
- package/dist/core/Observation.d.ts.map +1 -1
- package/dist/core/Observation.js +3 -2
- package/dist/core/Observation.js.map +1 -1
- package/dist/core/Route.d.ts.map +1 -1
- package/dist/core/Route.js +3 -4
- package/dist/core/Route.js.map +1 -1
- package/dist/core/State.d.ts +1 -1
- package/dist/core/State.d.ts.map +1 -1
- package/dist/core/State.js +4 -3
- package/dist/core/State.js.map +1 -1
- package/dist/core/Tool.d.ts +1 -0
- package/dist/core/Tool.d.ts.map +1 -1
- package/dist/core/Tool.js +3 -2
- package/dist/core/Tool.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/types/agent.d.ts +26 -2
- package/dist/types/agent.d.ts.map +1 -1
- package/dist/types/observation.d.ts +2 -0
- package/dist/types/observation.d.ts.map +1 -1
- package/dist/types/route.d.ts +2 -0
- package/dist/types/route.d.ts.map +1 -1
- package/dist/types/tool.d.ts +6 -2
- package/dist/types/tool.d.ts.map +1 -1
- package/dist/utils/id.d.ts +25 -0
- package/dist/utils/id.d.ts.map +1 -0
- package/dist/utils/id.js +65 -0
- package/dist/utils/id.js.map +1 -0
- package/docs/API_REFERENCE.md +122 -6
- package/docs/CONSTRUCTOR_OPTIONS.md +43 -36
- package/docs/CONTEXT_MANAGEMENT.md +447 -0
- package/docs/GETTING_STARTED.md +2 -0
- package/docs/PROVIDERS.md +3 -0
- package/examples/declarative-agent.ts +31 -7
- package/examples/persistent-onboarding.ts +464 -0
- package/package.json +1 -1
- package/src/core/Agent.ts +56 -2
- package/src/core/Events.ts +6 -4
- package/src/core/Observation.ts +3 -3
- package/src/core/Route.ts +3 -5
- package/src/core/State.ts +5 -4
- package/src/core/Tool.ts +4 -3
- package/src/index.ts +10 -0
- package/src/types/agent.ts +36 -2
- package/src/types/observation.ts +2 -0
- package/src/types/route.ts +2 -0
- package/src/types/tool.ts +6 -2
- 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
|
-
{
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
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
|
-
//
|
|
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
|
-
...(
|
|
242
|
+
...(currentContext as Record<string, unknown>),
|
|
189
243
|
...(contextOverride as Record<string, unknown>),
|
|
190
244
|
} as TContext;
|
|
191
245
|
|
package/src/core/Events.ts
CHANGED
|
@@ -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
|
}
|