@falai/agent 0.3.30 → 0.4.1

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 (77) hide show
  1. package/dist/cjs/core/Agent.d.ts +1 -0
  2. package/dist/cjs/core/Agent.d.ts.map +1 -1
  3. package/dist/cjs/core/Agent.js +62 -2
  4. package/dist/cjs/core/Agent.js.map +1 -1
  5. package/dist/cjs/core/ConditionEvaluator.d.ts +72 -0
  6. package/dist/cjs/core/ConditionEvaluator.d.ts.map +1 -0
  7. package/dist/cjs/core/ConditionEvaluator.js +272 -0
  8. package/dist/cjs/core/ConditionEvaluator.js.map +1 -0
  9. package/dist/cjs/core/PreparationEngine.d.ts +116 -0
  10. package/dist/cjs/core/PreparationEngine.d.ts.map +1 -0
  11. package/dist/cjs/core/PreparationEngine.js +353 -0
  12. package/dist/cjs/core/PreparationEngine.js.map +1 -0
  13. package/dist/cjs/core/Route.d.ts +6 -0
  14. package/dist/cjs/core/Route.d.ts.map +1 -1
  15. package/dist/cjs/core/Route.js +9 -0
  16. package/dist/cjs/core/Route.js.map +1 -1
  17. package/dist/cjs/providers/AnthropicProvider.d.ts +3 -3
  18. package/dist/cjs/providers/AnthropicProvider.d.ts.map +1 -1
  19. package/dist/cjs/providers/AnthropicProvider.js.map +1 -1
  20. package/dist/cjs/providers/GeminiProvider.d.ts +3 -3
  21. package/dist/cjs/providers/GeminiProvider.d.ts.map +1 -1
  22. package/dist/cjs/providers/GeminiProvider.js +43 -18
  23. package/dist/cjs/providers/GeminiProvider.js.map +1 -1
  24. package/dist/cjs/providers/OpenAIProvider.d.ts +3 -3
  25. package/dist/cjs/providers/OpenAIProvider.d.ts.map +1 -1
  26. package/dist/cjs/providers/OpenAIProvider.js.map +1 -1
  27. package/dist/cjs/providers/OpenRouterProvider.d.ts +3 -3
  28. package/dist/cjs/providers/OpenRouterProvider.d.ts.map +1 -1
  29. package/dist/cjs/providers/OpenRouterProvider.js.map +1 -1
  30. package/dist/cjs/types/agent.d.ts +3 -0
  31. package/dist/cjs/types/agent.d.ts.map +1 -1
  32. package/dist/cjs/types/ai.d.ts +6 -6
  33. package/dist/cjs/types/ai.d.ts.map +1 -1
  34. package/dist/core/Agent.d.ts +1 -0
  35. package/dist/core/Agent.d.ts.map +1 -1
  36. package/dist/core/Agent.js +62 -2
  37. package/dist/core/Agent.js.map +1 -1
  38. package/dist/core/ConditionEvaluator.d.ts +72 -0
  39. package/dist/core/ConditionEvaluator.d.ts.map +1 -0
  40. package/dist/core/ConditionEvaluator.js +268 -0
  41. package/dist/core/ConditionEvaluator.js.map +1 -0
  42. package/dist/core/PreparationEngine.d.ts +116 -0
  43. package/dist/core/PreparationEngine.d.ts.map +1 -0
  44. package/dist/core/PreparationEngine.js +349 -0
  45. package/dist/core/PreparationEngine.js.map +1 -0
  46. package/dist/core/Route.d.ts +6 -0
  47. package/dist/core/Route.d.ts.map +1 -1
  48. package/dist/core/Route.js +9 -0
  49. package/dist/core/Route.js.map +1 -1
  50. package/dist/providers/AnthropicProvider.d.ts +3 -3
  51. package/dist/providers/AnthropicProvider.d.ts.map +1 -1
  52. package/dist/providers/AnthropicProvider.js.map +1 -1
  53. package/dist/providers/GeminiProvider.d.ts +3 -3
  54. package/dist/providers/GeminiProvider.d.ts.map +1 -1
  55. package/dist/providers/GeminiProvider.js +43 -18
  56. package/dist/providers/GeminiProvider.js.map +1 -1
  57. package/dist/providers/OpenAIProvider.d.ts +3 -3
  58. package/dist/providers/OpenAIProvider.d.ts.map +1 -1
  59. package/dist/providers/OpenAIProvider.js.map +1 -1
  60. package/dist/providers/OpenRouterProvider.d.ts +3 -3
  61. package/dist/providers/OpenRouterProvider.d.ts.map +1 -1
  62. package/dist/providers/OpenRouterProvider.js.map +1 -1
  63. package/dist/types/agent.d.ts +3 -0
  64. package/dist/types/agent.d.ts.map +1 -1
  65. package/dist/types/ai.d.ts +6 -6
  66. package/dist/types/ai.d.ts.map +1 -1
  67. package/package.json +1 -1
  68. package/src/core/Agent.ts +86 -2
  69. package/src/core/ConditionEvaluator.ts +381 -0
  70. package/src/core/PreparationEngine.ts +561 -0
  71. package/src/core/Route.ts +10 -0
  72. package/src/providers/AnthropicProvider.ts +51 -21
  73. package/src/providers/GeminiProvider.ts +86 -40
  74. package/src/providers/OpenAIProvider.ts +48 -21
  75. package/src/providers/OpenRouterProvider.ts +36 -18
  76. package/src/types/agent.ts +3 -0
  77. package/src/types/ai.ts +13 -8
@@ -0,0 +1,561 @@
1
+ /**
2
+ * PreparationEngine - Handles the preparation iteration loop
3
+ *
4
+ * This engine implements the core Parlant/Emcie architecture:
5
+ * 1. Before generating a message, run preparation iterations
6
+ * 2. Each iteration:
7
+ * - Match guidelines against current context
8
+ * - Walk the state machine and execute tool transitions
9
+ * - Execute tools when conditions are met
10
+ * - Update context from tool results
11
+ * - Check if prepared to respond
12
+ * 3. After preparation, the AI generates the final message
13
+ *
14
+ * The AI NEVER sees tools - tools execute automatically based on:
15
+ * - State machine transitions ({ toolState: tool })
16
+ * - Guideline matching with associated tools
17
+ */
18
+
19
+ import type { Event, StateRef, AiProvider } from "../types/index";
20
+ import type { Guideline, GuidelineMatch } from "../types/agent";
21
+ import type { ToolRef } from "../types/tool";
22
+ import type { Route } from "./Route";
23
+ import { State } from "./State";
24
+ import { ConditionEvaluator } from "./ConditionEvaluator";
25
+
26
+ /**
27
+ * Tool execution result
28
+ */
29
+ export interface ToolExecutionResult {
30
+ toolName: string;
31
+ arguments: Record<string, unknown>;
32
+ result: unknown;
33
+ success: boolean;
34
+ error?: string;
35
+ }
36
+
37
+ /**
38
+ * Preparation iteration state
39
+ */
40
+ export interface IterationState {
41
+ iterationNumber: number;
42
+ matchedGuidelines: GuidelineMatch[];
43
+ executedTools: ToolExecutionResult[];
44
+ contextUpdates: Record<string, unknown>;
45
+ preparedToRespond: boolean;
46
+ }
47
+
48
+ /**
49
+ * Preparation context
50
+ */
51
+ export interface PreparationContext<TContext = unknown> {
52
+ history: Event[];
53
+ currentState?: StateRef;
54
+ context: TContext;
55
+ routes: Route<TContext>[];
56
+ guidelines: Guideline[];
57
+ maxIterations: number;
58
+ }
59
+
60
+ /**
61
+ * Preparation result
62
+ */
63
+ export interface PreparationResult<TContext = unknown> {
64
+ iterations: IterationState[];
65
+ finalContext: TContext;
66
+ toolExecutions: ToolExecutionResult[];
67
+ preparedToRespond: boolean;
68
+ }
69
+
70
+ /**
71
+ * PreparationEngine - Executes the preparation iteration loop
72
+ */
73
+ export interface PreparationEngineOptions {
74
+ maxParallelLlmCalls?: number;
75
+ maxParallelTools?: number;
76
+ }
77
+
78
+ export class PreparationEngine<TContext = unknown> {
79
+ private readonly conditionEvaluator?: ConditionEvaluator<TContext>;
80
+ private readonly maxParallelLlmCalls: number;
81
+ private readonly maxParallelTools: number;
82
+
83
+ /**
84
+ * Run a list of async jobs with a concurrency limit, preserving order of results.
85
+ */
86
+ private async processWithConcurrency<T>(
87
+ tasks: Array<() => Promise<T>>,
88
+ runner: (task: () => Promise<T>) => Promise<T>,
89
+ concurrency: number
90
+ ): Promise<T[]> {
91
+ const results: T[] = new Array<T>(tasks.length);
92
+ let nextIndex = 0;
93
+ const workers: Promise<void>[] = [];
94
+
95
+ const runNext = async (): Promise<void> => {
96
+ const current = nextIndex++;
97
+ if (current >= tasks.length) {
98
+ return;
99
+ }
100
+ try {
101
+ results[current] = await runner(tasks[current]);
102
+ } finally {
103
+ await runNext();
104
+ }
105
+ };
106
+
107
+ const workerCount = Math.min(concurrency, tasks.length);
108
+ for (let i = 0; i < workerCount; i++) {
109
+ workers.push(runNext());
110
+ }
111
+ await Promise.all(workers);
112
+ return results;
113
+ }
114
+
115
+ constructor(ai?: AiProvider, options?: PreparationEngineOptions) {
116
+ if (ai) {
117
+ this.conditionEvaluator = new ConditionEvaluator<TContext>(ai);
118
+ }
119
+ this.maxParallelLlmCalls = options?.maxParallelLlmCalls ?? 4;
120
+ this.maxParallelTools = options?.maxParallelTools ?? 8;
121
+ }
122
+
123
+ /**
124
+ * Run preparation iterations before message generation
125
+ *
126
+ * This is the core engine that executes tools automatically
127
+ * based on state machine transitions and guideline matching.
128
+ */
129
+ async prepare(
130
+ preparationContext: PreparationContext<TContext>
131
+ ): Promise<PreparationResult<TContext>> {
132
+ const iterations: IterationState[] = [];
133
+ let currentContext = { ...preparationContext.context };
134
+ const allToolExecutions: ToolExecutionResult[] = [];
135
+ let preparedToRespond = false;
136
+
137
+ // Run preparation iterations
138
+ for (
139
+ let i = 0;
140
+ i < preparationContext.maxIterations && !preparedToRespond;
141
+ i++
142
+ ) {
143
+ const iteration = await this.runIteration({
144
+ iterationNumber: i + 1,
145
+ history: preparationContext.history,
146
+ currentState: preparationContext.currentState,
147
+ context: currentContext,
148
+ routes: preparationContext.routes,
149
+ guidelines: preparationContext.guidelines,
150
+ });
151
+
152
+ iterations.push(iteration);
153
+ allToolExecutions.push(...iteration.executedTools);
154
+
155
+ // Update context from tool results
156
+ if (Object.keys(iteration.contextUpdates).length > 0) {
157
+ currentContext = {
158
+ ...currentContext,
159
+ ...iteration.contextUpdates,
160
+ } as TContext;
161
+ }
162
+
163
+ // Check if we're prepared to respond
164
+ // We're prepared if:
165
+ // 1. No tools were executed in this iteration (nothing left to do)
166
+ // 2. OR we've reached max iterations
167
+ // 3. OR iteration explicitly set preparedToRespond to true
168
+ if (
169
+ iteration.executedTools.length === 0 ||
170
+ i === preparationContext.maxIterations - 1 ||
171
+ iteration.preparedToRespond
172
+ ) {
173
+ preparedToRespond = true;
174
+ }
175
+ }
176
+
177
+ return {
178
+ iterations,
179
+ finalContext: currentContext,
180
+ toolExecutions: allToolExecutions,
181
+ preparedToRespond,
182
+ };
183
+ }
184
+
185
+ /**
186
+ * Run a single preparation iteration
187
+ */
188
+ private async runIteration(params: {
189
+ iterationNumber: number;
190
+ history: Event[];
191
+ currentState?: StateRef;
192
+ context: TContext;
193
+ routes: Route<TContext>[];
194
+ guidelines: Guideline[];
195
+ }): Promise<IterationState> {
196
+ const executedTools: ToolExecutionResult[] = [];
197
+ const contextUpdates: Record<string, unknown> = {};
198
+ const matchedGuidelines: GuidelineMatch[] = [];
199
+
200
+ // Step 1: Match guidelines against current context
201
+ const matchedResults = await this.matchGuidelines(
202
+ params.guidelines,
203
+ params.context,
204
+ params.history
205
+ );
206
+ matchedGuidelines.push(...matchedResults);
207
+
208
+ // Step 2: Execute tools from matched guidelines
209
+ // Guidelines with associated tools should execute those tools
210
+ const guidelineToolCalls: Array<() => Promise<ToolExecutionResult | null>> =
211
+ [];
212
+ for (const match of matchedGuidelines) {
213
+ if (match.guideline.tools && match.guideline.tools.length > 0) {
214
+ for (const tool of match.guideline.tools) {
215
+ const t = tool as ToolRef<TContext, unknown[], unknown>;
216
+ guidelineToolCalls.push(() =>
217
+ this.executeTool(t, params.context, params.history)
218
+ );
219
+ }
220
+ }
221
+ }
222
+ if (guidelineToolCalls.length > 0) {
223
+ const results = await this.processWithConcurrency(
224
+ guidelineToolCalls,
225
+ (fn) => fn(),
226
+ this.maxParallelTools
227
+ );
228
+ for (const toolResult of results) {
229
+ if (toolResult) {
230
+ executedTools.push(toolResult);
231
+ if (toolResult.result && typeof toolResult.result === "object") {
232
+ const result = toolResult.result as Record<string, unknown>;
233
+ if (result.contextUpdate) {
234
+ Object.assign(contextUpdates, result.contextUpdate);
235
+ }
236
+ }
237
+ }
238
+ }
239
+ }
240
+
241
+ // Step 3: Walk state machine and execute tool transitions
242
+ // Find current route and state, then execute any toolState transitions
243
+ if (params.currentState?.id) {
244
+ const currentRoute = params.routes.find(
245
+ (r) => r.id === params.currentState!.routeId
246
+ );
247
+
248
+ if (currentRoute) {
249
+ const toolResults = await this.executeStateToolTransitions(
250
+ currentRoute,
251
+ params.currentState,
252
+ params.context,
253
+ params.history
254
+ );
255
+ executedTools.push(...toolResults);
256
+
257
+ // Update context from state tool results
258
+ for (const toolResult of toolResults) {
259
+ if (toolResult.result && typeof toolResult.result === "object") {
260
+ const result = toolResult.result as Record<string, unknown>;
261
+ if (result.contextUpdate) {
262
+ Object.assign(contextUpdates, result.contextUpdate);
263
+ }
264
+ }
265
+ }
266
+ }
267
+ }
268
+
269
+ return {
270
+ iterationNumber: params.iterationNumber,
271
+ matchedGuidelines,
272
+ executedTools,
273
+ contextUpdates,
274
+ preparedToRespond: false, // Will be determined by caller
275
+ };
276
+ }
277
+
278
+ /**
279
+ * Match guidelines against current context
280
+ *
281
+ * Evaluates guideline conditions against the current context and history
282
+ * using AI to determine relevance and priority
283
+ */
284
+ private async matchGuidelines(
285
+ guidelines: Guideline[],
286
+ context: TContext,
287
+ history: Event[]
288
+ ): Promise<GuidelineMatch[]> {
289
+ const alwaysMatches = guidelines.filter(
290
+ (g) => g.enabled !== false && !g.condition
291
+ );
292
+ const conditional = guidelines.filter(
293
+ (g) => g.enabled !== false && g.condition
294
+ );
295
+
296
+ const matches: GuidelineMatch[] = alwaysMatches.map((guideline) => ({
297
+ guideline,
298
+ rationale: "Guideline has no condition - always active",
299
+ }));
300
+
301
+ if (!this.conditionEvaluator) {
302
+ // No AI available, match all enabled guidelines with conditions
303
+ for (const guideline of conditional) {
304
+ matches.push({
305
+ guideline,
306
+ rationale: "AI not available - defaulting to match",
307
+ });
308
+ }
309
+ return matches;
310
+ }
311
+
312
+ // Evaluate conditional guidelines with limited parallelism
313
+ const workers = conditional.map((guideline) => async () => {
314
+ const evaluation =
315
+ await this.conditionEvaluator!.evaluateGuidelineCondition(
316
+ guideline,
317
+ context,
318
+ history
319
+ );
320
+ if (evaluation.matches) {
321
+ return {
322
+ guideline,
323
+ rationale: evaluation.rationale || "Condition evaluated as true",
324
+ } as GuidelineMatch;
325
+ }
326
+ return null;
327
+ });
328
+
329
+ const evaluated = await this.processWithConcurrency(
330
+ workers,
331
+ (w) => w(),
332
+ this.maxParallelLlmCalls
333
+ );
334
+ for (const r of evaluated) {
335
+ if (r) {
336
+ matches.push(r);
337
+ }
338
+ }
339
+
340
+ return matches;
341
+ }
342
+
343
+ /**
344
+ * Execute a single tool
345
+ */
346
+ private async executeTool(
347
+ tool: ToolRef<TContext, unknown[], unknown>,
348
+ context: TContext,
349
+ history: Event[]
350
+ ): Promise<ToolExecutionResult | null> {
351
+ try {
352
+ // Extract arguments from context and history
353
+ let args: unknown[];
354
+
355
+ if (this.conditionEvaluator && tool.parameters) {
356
+ // Use AI-powered extraction if available
357
+ const extraction = await this.conditionEvaluator.extractToolArguments(
358
+ tool,
359
+ context,
360
+ history
361
+ );
362
+ args = extraction.arguments;
363
+ } else {
364
+ // Fallback to simple extraction
365
+ args = this.conditionEvaluator
366
+ ? this.conditionEvaluator.simpleArgumentExtraction(tool, context)
367
+ : [];
368
+ }
369
+
370
+ // Execute the tool handler
371
+ const result = await tool.handler(
372
+ {
373
+ context,
374
+ history,
375
+ updateContext: async (_updates: Partial<TContext>) => {
376
+ // Context updates are handled separately in the preparation loop
377
+ // This is a no-op placeholder
378
+ },
379
+ },
380
+ ...args
381
+ );
382
+
383
+ return {
384
+ toolName: tool.name,
385
+ arguments: { args }, // Wrap array in object for logging
386
+ result,
387
+ success: true,
388
+ };
389
+ } catch (error) {
390
+ console.error(
391
+ `[PreparationEngine] Tool execution failed: ${tool.name}`,
392
+ error
393
+ );
394
+ return {
395
+ toolName: tool.name,
396
+ arguments: {},
397
+ result: null,
398
+ success: false,
399
+ error: error instanceof Error ? error.message : String(error),
400
+ };
401
+ }
402
+ }
403
+
404
+ /**
405
+ * Execute tool transitions in the state machine
406
+ *
407
+ * Walks through state transitions and executes tools when
408
+ * reaching a { toolState: tool } transition
409
+ */
410
+ private async executeStateToolTransitions(
411
+ route: Route<TContext>,
412
+ currentStateRef: StateRef,
413
+ context: TContext,
414
+ history: Event[]
415
+ ): Promise<ToolExecutionResult[]> {
416
+ const executedTools: ToolExecutionResult[] = [];
417
+
418
+ // Get the current state from the route or use initial state
419
+ let stateToWalk: State<TContext>;
420
+
421
+ try {
422
+ const foundState = route.getState(currentStateRef.id);
423
+ if (foundState) {
424
+ stateToWalk = foundState;
425
+ } else {
426
+ stateToWalk = route.initialState;
427
+ }
428
+ } catch {
429
+ // If any error occurs, fallback to initial state
430
+ stateToWalk = route.initialState;
431
+ }
432
+
433
+ // Walk the state machine starting from the determined state
434
+ const toolResults = await this.walkStateChain(
435
+ stateToWalk,
436
+ context,
437
+ history,
438
+ new Set() // Track visited states to prevent loops
439
+ );
440
+
441
+ executedTools.push(...toolResults);
442
+
443
+ return executedTools;
444
+ }
445
+
446
+ /**
447
+ * Walk through a chain of states, executing tools along the way
448
+ */
449
+ private async walkStateChain(
450
+ state: State<TContext>,
451
+ context: TContext,
452
+ history: Event[],
453
+ visited: Set<string>
454
+ ): Promise<ToolExecutionResult[]> {
455
+ const executedTools: ToolExecutionResult[] = [];
456
+
457
+ // Prevent infinite loops
458
+ if (visited.has(state.id)) {
459
+ return executedTools;
460
+ }
461
+ visited.add(state.id);
462
+
463
+ // Get all transitions from this state
464
+ const transitions = state.getTransitions();
465
+
466
+ // Process each transition
467
+ for (const transition of transitions) {
468
+ // Check if transition has a condition
469
+ if (transition.hasCondition()) {
470
+ // Evaluate condition
471
+ const shouldFollow = await this.evaluateTransitionCondition(
472
+ transition,
473
+ context,
474
+ history
475
+ );
476
+
477
+ if (!shouldFollow) {
478
+ continue; // Skip this transition
479
+ }
480
+ }
481
+
482
+ // Check if this is a toolState transition
483
+ if (transition.spec.toolState) {
484
+ // Execute the tool
485
+ const toolResult = await this.executeTool(
486
+ transition.spec.toolState,
487
+ context,
488
+ history
489
+ );
490
+
491
+ if (toolResult) {
492
+ executedTools.push(toolResult);
493
+
494
+ // Update context with tool results for next transitions
495
+ if (toolResult.result && typeof toolResult.result === "object") {
496
+ const result = toolResult.result as Record<string, unknown>;
497
+ if (result.contextUpdate) {
498
+ context = {
499
+ ...context,
500
+ ...(result.contextUpdate as Partial<TContext>),
501
+ };
502
+ }
503
+ }
504
+ }
505
+ }
506
+
507
+ // Get the target state and recursively walk it
508
+ const targetState = transition.getTarget();
509
+ if (targetState && !visited.has(targetState.id)) {
510
+ const childResults = await this.walkStateChain(
511
+ targetState,
512
+ context,
513
+ history,
514
+ visited
515
+ );
516
+ executedTools.push(...childResults);
517
+ }
518
+ }
519
+
520
+ return executedTools;
521
+ }
522
+
523
+ /**
524
+ * Evaluate a transition condition
525
+ */
526
+ private async evaluateTransitionCondition(
527
+ transition: { condition?: string },
528
+ context: TContext,
529
+ history: Event[]
530
+ ): Promise<boolean> {
531
+ if (!transition.condition) {
532
+ return true; // No condition = always follow
533
+ }
534
+
535
+ // If no AI evaluator available, default to true
536
+ if (!this.conditionEvaluator) {
537
+ return true;
538
+ }
539
+
540
+ try {
541
+ const evaluation =
542
+ await this.conditionEvaluator.evaluateTransitionCondition(
543
+ transition.condition,
544
+ context,
545
+ history
546
+ );
547
+
548
+ return evaluation.shouldFollow;
549
+ } catch (error) {
550
+ console.error(
551
+ `[PreparationEngine] Failed to evaluate transition condition`,
552
+ error
553
+ );
554
+ // On error, default to false (don't follow)
555
+ return false;
556
+ }
557
+ }
558
+ }
559
+
560
+ // Helper methods (outside class scope are not necessary; keep inside class)
561
+ export type _Internal = unknown;
package/src/core/Route.ts CHANGED
@@ -132,6 +132,16 @@ export class Route<TContext = unknown> {
132
132
  return states;
133
133
  }
134
134
 
135
+ /**
136
+ * Get a specific state by ID
137
+ * @param stateId - The state ID to find
138
+ * @returns The state if found, undefined otherwise
139
+ */
140
+ getState(stateId: string): State<TContext> | undefined {
141
+ const states = this.getAllStates();
142
+ return states.find((state) => state.id === stateId);
143
+ }
144
+
135
145
  /**
136
146
  * Get a description of the route structure for debugging
137
147
  */