@falai/agent 0.9.0-alpha-1 → 0.9.0-alpha-2

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 (166) hide show
  1. package/README.md +34 -22
  2. package/dist/cjs/src/core/Agent.d.ts +52 -24
  3. package/dist/cjs/src/core/Agent.d.ts.map +1 -1
  4. package/dist/cjs/src/core/Agent.js +266 -39
  5. package/dist/cjs/src/core/Agent.js.map +1 -1
  6. package/dist/cjs/src/core/PersistenceManager.d.ts.map +1 -1
  7. package/dist/cjs/src/core/PersistenceManager.js +48 -25
  8. package/dist/cjs/src/core/PersistenceManager.js.map +1 -1
  9. package/dist/cjs/src/core/PromptComposer.d.ts +1 -1
  10. package/dist/cjs/src/core/PromptComposer.d.ts.map +1 -1
  11. package/dist/cjs/src/core/PromptComposer.js.map +1 -1
  12. package/dist/cjs/src/core/ResponseEngine.d.ts +13 -12
  13. package/dist/cjs/src/core/ResponseEngine.d.ts.map +1 -1
  14. package/dist/cjs/src/core/ResponseEngine.js +4 -4
  15. package/dist/cjs/src/core/ResponseEngine.js.map +1 -1
  16. package/dist/cjs/src/core/ResponsePipeline.d.ts +66 -38
  17. package/dist/cjs/src/core/ResponsePipeline.d.ts.map +1 -1
  18. package/dist/cjs/src/core/ResponsePipeline.js +71 -3
  19. package/dist/cjs/src/core/ResponsePipeline.js.map +1 -1
  20. package/dist/cjs/src/core/Route.d.ts +24 -5
  21. package/dist/cjs/src/core/Route.d.ts.map +1 -1
  22. package/dist/cjs/src/core/Route.js +45 -1
  23. package/dist/cjs/src/core/Route.js.map +1 -1
  24. package/dist/cjs/src/core/RoutingEngine.d.ts +31 -6
  25. package/dist/cjs/src/core/RoutingEngine.d.ts.map +1 -1
  26. package/dist/cjs/src/core/RoutingEngine.js +113 -9
  27. package/dist/cjs/src/core/RoutingEngine.js.map +1 -1
  28. package/dist/cjs/src/core/SessionManager.d.ts +14 -4
  29. package/dist/cjs/src/core/SessionManager.d.ts.map +1 -1
  30. package/dist/cjs/src/core/SessionManager.js +25 -5
  31. package/dist/cjs/src/core/SessionManager.js.map +1 -1
  32. package/dist/cjs/src/core/Step.d.ts +10 -10
  33. package/dist/cjs/src/core/Step.d.ts.map +1 -1
  34. package/dist/cjs/src/core/Step.js.map +1 -1
  35. package/dist/cjs/src/core/ToolExecutor.d.ts +4 -2
  36. package/dist/cjs/src/core/ToolExecutor.d.ts.map +1 -1
  37. package/dist/cjs/src/core/ToolExecutor.js +13 -3
  38. package/dist/cjs/src/core/ToolExecutor.js.map +1 -1
  39. package/dist/cjs/src/types/agent.d.ts +41 -21
  40. package/dist/cjs/src/types/agent.d.ts.map +1 -1
  41. package/dist/cjs/src/types/agent.js.map +1 -1
  42. package/dist/cjs/src/types/index.d.ts +1 -1
  43. package/dist/cjs/src/types/index.d.ts.map +1 -1
  44. package/dist/cjs/src/types/index.js.map +1 -1
  45. package/dist/cjs/src/types/persistence.d.ts +0 -1
  46. package/dist/cjs/src/types/persistence.d.ts.map +1 -1
  47. package/dist/cjs/src/types/route.d.ts +22 -16
  48. package/dist/cjs/src/types/route.d.ts.map +1 -1
  49. package/dist/cjs/src/types/session.d.ts +6 -11
  50. package/dist/cjs/src/types/session.d.ts.map +1 -1
  51. package/dist/cjs/src/types/tool.d.ts +12 -6
  52. package/dist/cjs/src/types/tool.d.ts.map +1 -1
  53. package/dist/cjs/src/utils/session.d.ts +2 -2
  54. package/dist/cjs/src/utils/session.d.ts.map +1 -1
  55. package/dist/cjs/src/utils/session.js +6 -26
  56. package/dist/cjs/src/utils/session.js.map +1 -1
  57. package/dist/src/core/Agent.d.ts +52 -24
  58. package/dist/src/core/Agent.d.ts.map +1 -1
  59. package/dist/src/core/Agent.js +266 -39
  60. package/dist/src/core/Agent.js.map +1 -1
  61. package/dist/src/core/PersistenceManager.d.ts.map +1 -1
  62. package/dist/src/core/PersistenceManager.js +48 -25
  63. package/dist/src/core/PersistenceManager.js.map +1 -1
  64. package/dist/src/core/PromptComposer.d.ts +1 -1
  65. package/dist/src/core/PromptComposer.d.ts.map +1 -1
  66. package/dist/src/core/PromptComposer.js.map +1 -1
  67. package/dist/src/core/ResponseEngine.d.ts +13 -12
  68. package/dist/src/core/ResponseEngine.d.ts.map +1 -1
  69. package/dist/src/core/ResponseEngine.js +4 -4
  70. package/dist/src/core/ResponseEngine.js.map +1 -1
  71. package/dist/src/core/ResponsePipeline.d.ts +66 -38
  72. package/dist/src/core/ResponsePipeline.d.ts.map +1 -1
  73. package/dist/src/core/ResponsePipeline.js +71 -3
  74. package/dist/src/core/ResponsePipeline.js.map +1 -1
  75. package/dist/src/core/Route.d.ts +24 -5
  76. package/dist/src/core/Route.d.ts.map +1 -1
  77. package/dist/src/core/Route.js +45 -1
  78. package/dist/src/core/Route.js.map +1 -1
  79. package/dist/src/core/RoutingEngine.d.ts +31 -6
  80. package/dist/src/core/RoutingEngine.d.ts.map +1 -1
  81. package/dist/src/core/RoutingEngine.js +113 -9
  82. package/dist/src/core/RoutingEngine.js.map +1 -1
  83. package/dist/src/core/SessionManager.d.ts +14 -4
  84. package/dist/src/core/SessionManager.d.ts.map +1 -1
  85. package/dist/src/core/SessionManager.js +25 -5
  86. package/dist/src/core/SessionManager.js.map +1 -1
  87. package/dist/src/core/Step.d.ts +10 -10
  88. package/dist/src/core/Step.d.ts.map +1 -1
  89. package/dist/src/core/Step.js.map +1 -1
  90. package/dist/src/core/ToolExecutor.d.ts +4 -2
  91. package/dist/src/core/ToolExecutor.d.ts.map +1 -1
  92. package/dist/src/core/ToolExecutor.js +13 -3
  93. package/dist/src/core/ToolExecutor.js.map +1 -1
  94. package/dist/src/types/agent.d.ts +41 -21
  95. package/dist/src/types/agent.d.ts.map +1 -1
  96. package/dist/src/types/agent.js.map +1 -1
  97. package/dist/src/types/index.d.ts +1 -1
  98. package/dist/src/types/index.d.ts.map +1 -1
  99. package/dist/src/types/index.js.map +1 -1
  100. package/dist/src/types/persistence.d.ts +0 -1
  101. package/dist/src/types/persistence.d.ts.map +1 -1
  102. package/dist/src/types/route.d.ts +22 -16
  103. package/dist/src/types/route.d.ts.map +1 -1
  104. package/dist/src/types/session.d.ts +6 -11
  105. package/dist/src/types/session.d.ts.map +1 -1
  106. package/dist/src/types/tool.d.ts +12 -6
  107. package/dist/src/types/tool.d.ts.map +1 -1
  108. package/dist/src/utils/session.d.ts +2 -2
  109. package/dist/src/utils/session.d.ts.map +1 -1
  110. package/dist/src/utils/session.js +6 -26
  111. package/dist/src/utils/session.js.map +1 -1
  112. package/docs/README.md +3 -3
  113. package/docs/api/README.md +35 -4
  114. package/docs/api/overview.md +166 -12
  115. package/docs/core/agent/README.md +162 -17
  116. package/docs/core/agent/context-management.md +39 -15
  117. package/docs/core/agent/session-management.md +49 -16
  118. package/docs/core/ai-integration/prompt-composition.md +38 -14
  119. package/docs/core/ai-integration/response-processing.md +28 -17
  120. package/docs/core/conversation-flows/data-collection.md +103 -25
  121. package/docs/core/conversation-flows/route-dsl.md +45 -22
  122. package/docs/core/conversation-flows/routes.md +74 -18
  123. package/docs/core/conversation-flows/step-transitions.md +3 -3
  124. package/docs/core/conversation-flows/steps.md +39 -15
  125. package/docs/core/routing/intelligent-routing.md +18 -9
  126. package/docs/core/tools/tool-definition.md +8 -8
  127. package/docs/core/tools/tool-execution.md +26 -26
  128. package/docs/core/tools/tool-scoping.md +5 -5
  129. package/docs/guides/getting-started/README.md +54 -32
  130. package/examples/advanced-patterns/knowledge-based-agent.ts +37 -28
  131. package/examples/advanced-patterns/persistent-onboarding.ts +70 -41
  132. package/examples/advanced-patterns/route-lifecycle-hooks.ts +28 -2
  133. package/examples/advanced-patterns/streaming-responses.ts +28 -23
  134. package/examples/ai-providers/anthropic-integration.ts +40 -33
  135. package/examples/ai-providers/openai-integration.ts +25 -25
  136. package/examples/conversation-flows/completion-transitions.ts +36 -32
  137. package/examples/core-concepts/basic-agent.ts +76 -78
  138. package/examples/core-concepts/schema-driven-extraction.ts +20 -16
  139. package/examples/core-concepts/session-management.ts +65 -53
  140. package/examples/integrations/database-integration.ts +49 -34
  141. package/examples/integrations/healthcare-integration.ts +96 -91
  142. package/examples/integrations/search-integration.ts +79 -82
  143. package/examples/integrations/server-session-management.ts +25 -17
  144. package/examples/persistence/database-persistence.ts +61 -45
  145. package/examples/persistence/memory-sessions.ts +52 -63
  146. package/examples/persistence/redis-persistence.ts +81 -95
  147. package/examples/tools/basic-tools.ts +73 -62
  148. package/examples/tools/data-enrichment-tools.ts +52 -44
  149. package/package.json +1 -1
  150. package/src/core/Agent.ts +418 -128
  151. package/src/core/PersistenceManager.ts +51 -27
  152. package/src/core/PromptComposer.ts +1 -1
  153. package/src/core/ResponseEngine.ts +21 -19
  154. package/src/core/ResponsePipeline.ts +174 -59
  155. package/src/core/Route.ts +58 -6
  156. package/src/core/RoutingEngine.ts +174 -27
  157. package/src/core/SessionManager.ts +32 -8
  158. package/src/core/Step.ts +20 -12
  159. package/src/core/ToolExecutor.ts +19 -5
  160. package/src/types/agent.ts +46 -23
  161. package/src/types/index.ts +2 -0
  162. package/src/types/persistence.ts +0 -1
  163. package/src/types/route.ts +22 -16
  164. package/src/types/session.ts +6 -12
  165. package/src/types/tool.ts +15 -9
  166. package/src/utils/session.ts +6 -31
package/src/core/Route.ts CHANGED
@@ -39,8 +39,9 @@ export class Route<TContext = unknown, TData = unknown> {
39
39
  "step" | "condition" | "skipIf"
40
40
  >;
41
41
  public readonly responseOutputSchema?: StructuredSchema;
42
- public readonly schema?: StructuredSchema;
43
42
  public readonly initialData?: Partial<TData>;
43
+ public readonly requiredFields?: (keyof TData)[];
44
+ public readonly optionalFields?: (keyof TData)[];
44
45
  public readonly onComplete?:
45
46
  | string
46
47
  | RouteTransitionConfig<TContext, TData>
@@ -49,7 +50,7 @@ export class Route<TContext = unknown, TData = unknown> {
49
50
  public routingExtrasSchema?: StructuredSchema;
50
51
  public guidelines: Guideline<TContext>[] = [];
51
52
  public terms: Term<TContext>[] = [];
52
- public tools: Tool<TContext, unknown[], unknown, TData>[] = [];
53
+ public tools: Tool<TContext, TData, unknown[], unknown>[] = [];
53
54
  public knowledgeBase: Record<string, unknown> = {};
54
55
 
55
56
  constructor(options: RouteOptions<TContext, TData>) {
@@ -87,8 +88,9 @@ export class Route<TContext = unknown, TData = unknown> {
87
88
  };
88
89
  this.routingExtrasSchema = options.routingExtrasSchema;
89
90
  this.responseOutputSchema = options.responseOutputSchema;
90
- this.schema = options.schema;
91
91
  this.initialData = options.initialData;
92
+ this.requiredFields = options.requiredFields;
93
+ this.optionalFields = options.optionalFields;
92
94
  this.onComplete = options.onComplete;
93
95
  this.hooks = options.hooks;
94
96
 
@@ -166,7 +168,7 @@ export class Route<TContext = unknown, TData = unknown> {
166
168
  /**
167
169
  * Register a tool for this route
168
170
  */
169
- createTool(tool: Tool<TContext, unknown[], unknown, TData>): this {
171
+ createTool(tool: Tool<TContext, TData, unknown[], unknown>): this {
170
172
  this.tools.push(tool);
171
173
  return this;
172
174
  }
@@ -174,7 +176,7 @@ export class Route<TContext = unknown, TData = unknown> {
174
176
  /**
175
177
  * Register multiple tools for this route
176
178
  */
177
- registerTools(tools: Tool<TContext, unknown[], unknown, TData>[]): this {
179
+ registerTools(tools: Tool<TContext, TData, unknown[], unknown>[]): this {
178
180
  tools.forEach((tool) => this.createTool(tool));
179
181
  return this;
180
182
  }
@@ -196,7 +198,7 @@ export class Route<TContext = unknown, TData = unknown> {
196
198
  /**
197
199
  * Get all tools for this route
198
200
  */
199
- getTools(): Tool<TContext, unknown[], unknown, TData>[] {
201
+ getTools(): Tool<TContext, TData, unknown[], unknown>[] {
200
202
  return [...this.tools];
201
203
  }
202
204
 
@@ -353,6 +355,56 @@ export class Route<TContext = unknown, TData = unknown> {
353
355
  }
354
356
  }
355
357
 
358
+ /**
359
+ * Check if this route is complete based on the provided data
360
+ * @param data - Currently collected agent-level data
361
+ * @returns true if all required fields are present, false otherwise
362
+ */
363
+ isComplete(data: Partial<TData>): boolean {
364
+ if (!this.requiredFields || this.requiredFields.length === 0) {
365
+ return true; // No required fields means route is always complete
366
+ }
367
+
368
+ return this.requiredFields.every(field => {
369
+ const value = data[field];
370
+ return value !== undefined && value !== null && value !== '';
371
+ });
372
+ }
373
+
374
+ /**
375
+ * Get the list of missing required fields for this route
376
+ * @param data - Currently collected agent-level data
377
+ * @returns Array of missing required field keys
378
+ */
379
+ getMissingRequiredFields(data: Partial<TData>): (keyof TData)[] {
380
+ if (!this.requiredFields || this.requiredFields.length === 0) {
381
+ return [];
382
+ }
383
+
384
+ return this.requiredFields.filter(field => {
385
+ const value = data[field];
386
+ return value === undefined || value === null || value === '';
387
+ });
388
+ }
389
+
390
+ /**
391
+ * Get the completion progress for this route as a percentage
392
+ * @param data - Currently collected agent-level data
393
+ * @returns Completion progress as a number between 0 and 1
394
+ */
395
+ getCompletionProgress(data: Partial<TData>): number {
396
+ if (!this.requiredFields || this.requiredFields.length === 0) {
397
+ return 1; // No required fields means 100% complete
398
+ }
399
+
400
+ const completedFields = this.requiredFields.filter(field => {
401
+ const value = data[field];
402
+ return value !== undefined && value !== null && value !== '';
403
+ });
404
+
405
+ return completedFields.length / this.requiredFields.length;
406
+ }
407
+
356
408
  /**
357
409
  * Evaluate the onComplete handler and return transition config
358
410
  * @param session - Current session step
@@ -52,7 +52,7 @@ export interface BuildStepSelectionPromptParams<
52
52
  data: Partial<TData>;
53
53
  history: Event[];
54
54
  lastMessage: string;
55
- agentOptions?: AgentOptions<TContext>;
55
+ agentOptions?: AgentOptions<TContext, TData>;
56
56
  context?: TContext;
57
57
  session?: SessionState<TData>;
58
58
  }
@@ -61,14 +61,14 @@ export interface BuildRoutingPromptParams<TContext = unknown, TData = unknown> {
61
61
  history: Event[];
62
62
  routes: Route<TContext, TData>[];
63
63
  lastMessage: string;
64
- agentOptions?: AgentOptions<TContext>;
64
+ agentOptions?: AgentOptions<TContext, TData>;
65
65
  session?: SessionState<TData>;
66
66
  activeRouteSteps?: Step<TContext, TData>[];
67
67
  context?: TContext;
68
68
  }
69
69
 
70
70
  export class RoutingEngine<TContext = unknown, TData = unknown> {
71
- constructor(private readonly options?: RoutingEngineOptions) {}
71
+ constructor(private readonly options?: RoutingEngineOptions) { }
72
72
 
73
73
  /**
74
74
  * Optimized decision for single-route scenarios
@@ -79,16 +79,17 @@ export class RoutingEngine<TContext = unknown, TData = unknown> {
79
79
  route: Route<TContext, TData>;
80
80
  session: SessionState<TData>;
81
81
  history: Event[];
82
- agentOptions?: AgentOptions<TContext>;
82
+ agentOptions?: AgentOptions<TContext, TData>;
83
83
  provider: AiProvider;
84
84
  context: TContext;
85
85
  signal?: AbortSignal;
86
86
  }): Promise<{
87
- selectedRoute?: Route<TContext>;
88
- selectedStep?: Step<TContext>;
87
+ selectedRoute?: Route<TContext, TData>;
88
+ selectedStep?: Step<TContext, TData>;
89
89
  responseDirectives?: string[];
90
90
  session: SessionState<TData>;
91
91
  isRouteComplete?: boolean;
92
+ completedRoutes?: Route<TContext, TData>[];
92
93
  }> {
93
94
  const { route, session, history, agentOptions, provider, context, signal } =
94
95
  params;
@@ -96,6 +97,9 @@ export class RoutingEngine<TContext = unknown, TData = unknown> {
96
97
  let updatedSession = session;
97
98
  const selectedRoute = route;
98
99
 
100
+ // Check if this single route is complete
101
+ const completedRoutes = route.isComplete(session.data || {}) ? [route] : [];
102
+
99
103
  // Enter route if not already in it
100
104
  if (!session.currentRoute || session.currentRoute.id !== route.id) {
101
105
  updatedSession = enterRoute(session, route.id, route.title);
@@ -139,6 +143,7 @@ export class RoutingEngine<TContext = unknown, TData = unknown> {
139
143
  selectedStep: undefined,
140
144
  session: updatedSession,
141
145
  isRouteComplete: true,
146
+ completedRoutes,
142
147
  };
143
148
  } else {
144
149
  logger.debug(
@@ -149,6 +154,7 @@ export class RoutingEngine<TContext = unknown, TData = unknown> {
149
154
  selectedStep: candidates[0].step,
150
155
  session: updatedSession,
151
156
  isRouteComplete: false,
157
+ completedRoutes,
152
158
  };
153
159
  }
154
160
  }
@@ -210,6 +216,7 @@ export class RoutingEngine<TContext = unknown, TData = unknown> {
210
216
  selectedStep: selectedStep?.step || candidates[0].step,
211
217
  responseDirectives: stepResult.structured?.responseDirectives,
212
218
  session: updatedSession,
219
+ completedRoutes,
213
220
  };
214
221
  }
215
222
 
@@ -217,7 +224,7 @@ export class RoutingEngine<TContext = unknown, TData = unknown> {
217
224
  * Recursively traverse step chain to find first non-skipped step or END_ROUTE
218
225
  * @private
219
226
  */
220
- private findFirstValidStepRecursive<TData = unknown>(
227
+ private findFirstValidStepRecursive(
221
228
  currentStep: Step<TContext, TData>,
222
229
  data: Partial<TData>,
223
230
  visited: Set<string>
@@ -277,7 +284,7 @@ export class RoutingEngine<TContext = unknown, TData = unknown> {
277
284
  * Identify valid next candidate steps based on current step and collected data
278
285
  * Returns step with isRouteComplete flag if route is complete (all steps skipped + has END_ROUTE transition)
279
286
  */
280
- getCandidateSteps<TData = unknown>(
287
+ getCandidateSteps(
281
288
  route: Route<TContext, TData>,
282
289
  currentStep: Step<TContext, TData> | undefined,
283
290
  data: Partial<TData>
@@ -397,21 +404,23 @@ export class RoutingEngine<TContext = unknown, TData = unknown> {
397
404
  * and updates the session (including initialData merge when entering a new route).
398
405
  *
399
406
  * OPTIMIZATION: If there's only 1 route, skips route scoring and only does step selection.
407
+ * CROSS-ROUTE COMPLETION: Evaluates all routes for completion based on collected data.
400
408
  */
401
409
  async decideRouteAndStep(params: {
402
410
  routes: Route<TContext, TData>[];
403
411
  session: SessionState<TData>;
404
412
  history: Event[];
405
- agentOptions?: AgentOptions<TContext>;
413
+ agentOptions?: AgentOptions<TContext, TData>;
406
414
  provider: AiProvider;
407
415
  context: TContext;
408
416
  signal?: AbortSignal;
409
417
  }): Promise<{
410
- selectedRoute?: Route<TContext>;
411
- selectedStep?: Step<TContext>;
418
+ selectedRoute?: Route<TContext, TData>;
419
+ selectedStep?: Step<TContext, TData>;
412
420
  responseDirectives?: string[];
413
421
  session: SessionState<TData>;
414
422
  isRouteComplete?: boolean;
423
+ completedRoutes?: Route<TContext, TData>[];
415
424
  }> {
416
425
  const {
417
426
  routes,
@@ -427,9 +436,19 @@ export class RoutingEngine<TContext = unknown, TData = unknown> {
427
436
  return { session };
428
437
  }
429
438
 
439
+ // CROSS-ROUTE COMPLETION EVALUATION: Check all routes for completion
440
+ const completedRoutes = this.evaluateRouteCompletions(routes, session.data || {});
441
+
442
+ // Log completed routes
443
+ if (completedRoutes.length > 0) {
444
+ logger.debug(
445
+ `[RoutingEngine] Found ${completedRoutes.length} completed routes: ${completedRoutes.map(r => r.title).join(', ')}`
446
+ );
447
+ }
448
+
430
449
  // OPTIMIZATION: Single route - skip route scoring, only do step selection
431
450
  if (routes.length === 1) {
432
- return this.decideSingleRouteStep({
451
+ const result = await this.decideSingleRouteStep({
433
452
  route: routes[0],
434
453
  session,
435
454
  history,
@@ -438,6 +457,10 @@ export class RoutingEngine<TContext = unknown, TData = unknown> {
438
457
  context,
439
458
  signal,
440
459
  });
460
+ return {
461
+ ...result,
462
+ completedRoutes,
463
+ };
441
464
  }
442
465
 
443
466
  const lastUserMessage = getLastMessageFromHistory(history);
@@ -505,18 +528,29 @@ export class RoutingEngine<TContext = unknown, TData = unknown> {
505
528
  },
506
529
  });
507
530
 
508
- let selectedRoute: Route<TContext> | undefined;
509
- let selectedStep: Step<TContext> | undefined;
531
+ let selectedRoute: Route<TContext, TData> | undefined;
532
+ let selectedStep: Step<TContext, TData> | undefined;
510
533
  let responseDirectives: string[] | undefined;
511
534
  let updatedSession = session;
512
535
 
513
536
  if (routingResult.structured?.routes) {
514
- const decision = this.decideRouteFromScores({
515
- context: routingResult.structured.context,
516
- routes: routingResult.structured.routes,
517
- responseDirectives: routingResult.structured.responseDirectives,
518
- });
519
- selectedRoute = routes.find((r) => r.id === decision.routeId);
537
+ // Use cross-route completion evaluation to select optimal route
538
+ const optimalRoute = this.selectOptimalRoute(
539
+ routes,
540
+ updatedSession.data || {},
541
+ routingResult.structured.routes
542
+ );
543
+
544
+ // Fall back to traditional scoring if no optimal route found
545
+ selectedRoute = optimalRoute || (() => {
546
+ const decision = this.decideRouteFromScores({
547
+ context: routingResult.structured.context,
548
+ routes: routingResult.structured.routes,
549
+ responseDirectives: routingResult.structured.responseDirectives,
550
+ });
551
+ return routes.find((r) => r.id === decision.routeId);
552
+ })();
553
+
520
554
  responseDirectives = routingResult.structured.responseDirectives;
521
555
 
522
556
  if (
@@ -569,14 +603,95 @@ export class RoutingEngine<TContext = unknown, TData = unknown> {
569
603
  responseDirectives,
570
604
  session: updatedSession,
571
605
  isRouteComplete,
606
+ completedRoutes,
572
607
  };
573
608
  }
574
609
 
610
+ /**
611
+ * Evaluate all routes for completion based on collected data
612
+ * @param routes - All available routes
613
+ * @param data - Currently collected agent-level data
614
+ * @returns Array of routes that are complete
615
+ */
616
+ evaluateRouteCompletions(routes: Route<TContext, TData>[], data: Partial<TData>): Route<TContext, TData>[] {
617
+ return routes.filter(route => route.isComplete(data));
618
+ }
619
+
620
+ /**
621
+ * Get completion status for all routes
622
+ * @param routes - All available routes
623
+ * @param data - Currently collected agent-level data
624
+ * @returns Map of route ID to completion progress (0-1)
625
+ */
626
+ getRouteCompletionStatus(routes: Route<TContext, TData>[], data: Partial<TData>): Map<string, number> {
627
+ const completionStatus = new Map<string, number>();
628
+
629
+ for (const route of routes) {
630
+ const progress = route.getCompletionProgress(data);
631
+ completionStatus.set(route.id, progress);
632
+ }
633
+
634
+ return completionStatus;
635
+ }
636
+
637
+ /**
638
+ * Find the best route to continue based on completion status and user intent
639
+ * Prioritizes routes that are partially complete but not finished
640
+ * @param routes - All available routes
641
+ * @param data - Currently collected agent-level data
642
+ * @param routeScores - AI-generated route scores from routing decision
643
+ * @returns Route that should be prioritized for continuation
644
+ */
645
+ selectOptimalRoute(
646
+ routes: Route<TContext, TData>[],
647
+ data: Partial<TData>,
648
+ routeScores: Record<string, number>
649
+ ): Route<TContext, TData> | undefined {
650
+ const completionStatus = this.getRouteCompletionStatus(routes, data);
651
+
652
+ // Create weighted scores combining AI intent scores with completion progress
653
+ const weightedScores: Array<{ route: Route<TContext, TData>; score: number }> = [];
654
+
655
+ for (const route of routes) {
656
+ const aiScore = routeScores[route.id] || 0;
657
+ const completionProgress = completionStatus.get(route.id) || 0;
658
+
659
+ // Skip fully completed routes unless they have very high AI scores
660
+ if (completionProgress >= 1.0 && aiScore < 80) {
661
+ continue;
662
+ }
663
+
664
+ // Boost partially complete routes that match user intent
665
+ let weightedScore = aiScore;
666
+ if (completionProgress > 0 && completionProgress < 1.0) {
667
+ // Boost score for partially complete routes
668
+ weightedScore += (completionProgress * 20); // Up to 20 point boost
669
+ }
670
+
671
+ weightedScores.push({ route, score: weightedScore });
672
+ }
673
+
674
+ // Sort by weighted score and return the best option
675
+ weightedScores.sort((a, b) => b.score - a.score);
676
+
677
+ if (weightedScores.length > 0) {
678
+ logger.debug(
679
+ `[RoutingEngine] Selected optimal route: ${weightedScores[0].route.title} ` +
680
+ `(AI: ${routeScores[weightedScores[0].route.id]}, ` +
681
+ `Completion: ${(completionStatus.get(weightedScores[0].route.id) || 0) * 100}%, ` +
682
+ `Weighted: ${weightedScores[0].score})`
683
+ );
684
+ return weightedScores[0].route;
685
+ }
686
+
687
+ return undefined;
688
+ }
689
+
575
690
  /**
576
691
  * Build prompt for step selection within a single route
577
692
  * @private
578
693
  */
579
- private async buildStepSelectionPrompt<TData>(
694
+ private async buildStepSelectionPrompt(
580
695
  params: BuildStepSelectionPromptParams<TContext, TData>
581
696
  ): Promise<string> {
582
697
  const {
@@ -591,7 +706,7 @@ export class RoutingEngine<TContext = unknown, TData = unknown> {
591
706
  session,
592
707
  } = params;
593
708
  const templateContext = { context, session, history };
594
- const pc = new PromptComposer(templateContext);
709
+ const pc = new PromptComposer<TContext, TData>(templateContext);
595
710
 
596
711
  // Add agent metadata
597
712
  if (agentOptions) {
@@ -606,8 +721,7 @@ export class RoutingEngine<TContext = unknown, TData = unknown> {
606
721
  // Add current step context
607
722
  if (currentStep) {
608
723
  await pc.addInstruction(
609
- `Current Step: ${currentStep.id}\nDescription: ${
610
- currentStep.description || "N/A"
724
+ `Current Step: ${currentStep.id}\nDescription: ${currentStep.description || "N/A"
611
725
  }`
612
726
  );
613
727
  } else {
@@ -802,7 +916,7 @@ export class RoutingEngine<TContext = unknown, TData = unknown> {
802
916
  context,
803
917
  } = params;
804
918
  const templateContext = { context, session, history };
805
- const pc = new PromptComposer(templateContext);
919
+ const pc = new PromptComposer<TContext, TData>(templateContext);
806
920
  if (agentOptions) {
807
921
  await pc.addAgentMeta(agentOptions);
808
922
  }
@@ -830,6 +944,41 @@ export class RoutingEngine<TContext = unknown, TData = unknown> {
830
944
  );
831
945
  await pc.addInstruction(sessionInfo.join("\n"));
832
946
 
947
+ // Add cross-route completion status
948
+ const completionStatus = this.getRouteCompletionStatus(routes, session.data || {});
949
+ const completedRoutes = this.evaluateRouteCompletions(routes, session.data || {});
950
+
951
+ if (completionStatus.size > 0) {
952
+ const statusInfo = [
953
+ "",
954
+ "Route completion status based on collected data:",
955
+ ];
956
+
957
+ for (const route of routes) {
958
+ const progress = completionStatus.get(route.id) || 0;
959
+ const isComplete = completedRoutes.includes(route);
960
+ const progressPercent = Math.round(progress * 100);
961
+
962
+ statusInfo.push(
963
+ `- ${route.title}: ${progressPercent}% complete${isComplete ? ' ✓ COMPLETE' : ''}`
964
+ );
965
+
966
+ if (!isComplete && route.requiredFields) {
967
+ const missingFields = route.getMissingRequiredFields(session.data || {});
968
+ if (missingFields.length > 0) {
969
+ statusInfo.push(` Missing: ${missingFields.join(', ')}`);
970
+ }
971
+ }
972
+ }
973
+
974
+ statusInfo.push(
975
+ "",
976
+ "Consider route completion status when scoring. Partially complete routes may be good candidates for continuation."
977
+ );
978
+
979
+ await pc.addInstruction(statusInfo.join("\n"));
980
+ }
981
+
833
982
  // Add available steps for the active route
834
983
  if (activeRouteSteps && activeRouteSteps.length > 0) {
835
984
  const stepInfo = [
@@ -869,8 +1018,6 @@ export class RoutingEngine<TContext = unknown, TData = unknown> {
869
1018
 
870
1019
  await pc.addInteractionHistory(history);
871
1020
  await pc.addLastMessage(lastMessage);
872
- // Cast to unknown to satisfy generic constraints in composer
873
- // This is safe because PromptComposer only reads route metadata (id, title, description)
874
1021
  await pc.addRoutingOverview(routes);
875
1022
  await pc.addInstruction(
876
1023
  [
@@ -75,8 +75,7 @@ export class SessionManager<TData = unknown> {
75
75
 
76
76
  const session: SessionState<TData> = {
77
77
  id,
78
- data: {} as Partial<TData>,
79
- dataByRoute: {},
78
+ data: {} as Partial<TData>, // Agent-level data structure
80
79
  routeHistory: [],
81
80
  history: [], // Session manages its own history
82
81
  metadata: {
@@ -186,25 +185,50 @@ export class SessionManager<TData = unknown> {
186
185
  }
187
186
 
188
187
  /**
189
- * Get collected data from the current session
188
+ * Get agent-level collected data from the current session
190
189
  */
191
- getData<T = TData>(): Partial<T> | undefined {
192
- return this.currentSession?.data as unknown as Partial<T> | undefined;
190
+ getData(): Partial<TData> {
191
+ return this.currentSession?.data || ({} as Partial<TData>);
193
192
  }
194
193
 
195
194
  /**
196
- * Set/merge data into the current session
195
+ * Set/merge agent-level data into the current session
196
+ * This updates the single source of truth for all collected data
197
197
  */
198
- async setData<T = TData>(data: Partial<T>): Promise<void> {
198
+ async setData(data: Partial<TData>): Promise<void> {
199
199
  // Ensure session exists
200
200
  await this.getOrCreate();
201
201
 
202
202
  if (this.currentSession && data) {
203
203
  this.currentSession.data = {
204
204
  ...this.currentSession.data,
205
- ...(data as unknown as Partial<TData>),
205
+ ...data,
206
206
  };
207
207
  this.currentSession.metadata!.lastUpdatedAt = new Date();
208
+
209
+ // Auto-save to persistence
210
+ await this.save();
211
+ }
212
+ }
213
+
214
+ /**
215
+ * Update specific fields in the agent-level data
216
+ * Provides a more explicit method for data updates
217
+ */
218
+ async updateData(updates: Partial<TData>): Promise<void> {
219
+ await this.setData(updates);
220
+ }
221
+
222
+ /**
223
+ * Clear all collected data while preserving session structure
224
+ */
225
+ async clearData(): Promise<void> {
226
+ if (this.currentSession) {
227
+ this.currentSession.data = {} as Partial<TData>;
228
+ this.currentSession.metadata!.lastUpdatedAt = new Date();
229
+
230
+ // Auto-save to persistence
231
+ await this.save();
208
232
  }
209
233
  }
210
234
 
package/src/core/Step.ts CHANGED
@@ -24,27 +24,31 @@ export class Step<TContext = unknown, TData = unknown> {
24
24
  private nextSteps: Step<TContext, TData>[] = [];
25
25
  private guidelines: Guideline<TContext>[] = [];
26
26
  public readonly routeId: string;
27
- public collect?: string[];
27
+ public collect?: (keyof TData)[];
28
28
  public description?: string;
29
29
  public when?: Template<TContext, TData>;
30
30
  public skipIf?: (data: Partial<TData>) => boolean;
31
- public requires?: string[];
31
+ public requires?: (keyof TData)[];
32
32
  public prompt?: Template<TContext, TData>;
33
33
  public prepare?:
34
34
  | string
35
- | Tool<TContext, unknown[], unknown, TData>
35
+ | Tool<TContext, TData, unknown[], unknown>
36
36
  | ((context: TContext, data?: Partial<TData>) => void | Promise<void>);
37
37
  public finalize?:
38
38
  | string
39
- | Tool<TContext, unknown[], unknown, TData>
39
+ | Tool<TContext, TData, unknown[], unknown>
40
40
  | ((context: TContext, data?: Partial<TData>) => void | Promise<void>);
41
- public tools?: (string | Tool<TContext, unknown[], unknown, TData>)[];
41
+ public tools?: (string | Tool<TContext, TData, unknown[], unknown>)[];
42
42
 
43
- constructor(routeId: string, options: StepOptions<TContext, TData> = {}) {
43
+ constructor(
44
+ routeId: string,
45
+ options: StepOptions<TContext, TData> = {}
46
+ ) {
44
47
  // Use provided ID or generate a deterministic one
45
48
  this.id = options.id || generateStepId(routeId, options.description);
46
49
  this.routeId = routeId;
47
50
  this.description = options.description;
51
+
48
52
  this.collect = options.collect;
49
53
  this.skipIf = options.skipIf;
50
54
  this.requires = options.requires;
@@ -61,32 +65,36 @@ export class Step<TContext = unknown, TData = unknown> {
61
65
  */
62
66
  configure(config: {
63
67
  description?: string;
64
- collect?: string[];
68
+ collect?: (keyof TData)[];
65
69
  skipIf?: (data: Partial<TData>) => boolean;
66
- requires?: string[];
70
+ requires?: (keyof TData)[];
67
71
  prompt?: Template<TContext, TData>;
68
72
  prepare?:
69
73
  | string
70
- | Tool<TContext, unknown[], unknown, TData>
74
+ | Tool<TContext, TData, unknown[], unknown>
71
75
  | ((context: TContext, data?: Partial<TData>) => void | Promise<void>);
72
76
  finalize?:
73
77
  | string
74
- | Tool<TContext, unknown[], unknown, TData>
78
+ | Tool<TContext, TData, unknown[], unknown>
75
79
  | ((context: TContext, data?: Partial<TData>) => void | Promise<void>);
76
- tools?: (string | Tool<TContext, unknown[], unknown, TData>)[];
80
+ tools?: (string | Tool<TContext, TData, unknown[], unknown>)[];
77
81
  }): this {
78
82
  if (config.description !== undefined) {
79
83
  this.description = config.description;
80
84
  }
85
+
81
86
  if (config.collect !== undefined) {
82
87
  this.collect = config.collect;
83
88
  }
89
+
84
90
  if (config.skipIf !== undefined) {
85
91
  this.skipIf = config.skipIf;
86
92
  }
93
+
87
94
  if (config.requires !== undefined) {
88
95
  this.requires = config.requires;
89
96
  }
97
+
90
98
  if (config.prompt !== undefined) {
91
99
  this.prompt = config.prompt;
92
100
  }
@@ -214,7 +222,7 @@ export class Step<TContext = unknown, TData = unknown> {
214
222
  */
215
223
  hasRequires(data: Partial<TData>): boolean {
216
224
  if (!this.requires || this.requires.length === 0) return true;
217
- return this.requires.every((key) => data[key as keyof TData] !== undefined);
225
+ return this.requires.every((key) => data[key] !== undefined);
218
226
  }
219
227
 
220
228
  /**