@falai/agent 1.1.1 → 1.1.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 (50) hide show
  1. package/dist/cjs/core/Agent.d.ts +202 -67
  2. package/dist/cjs/core/Agent.d.ts.map +1 -1
  3. package/dist/cjs/core/Agent.js +366 -158
  4. package/dist/cjs/core/Agent.js.map +1 -1
  5. package/dist/cjs/core/BatchExecutor.d.ts.map +1 -1
  6. package/dist/cjs/core/BatchExecutor.js +4 -2
  7. package/dist/cjs/core/BatchExecutor.js.map +1 -1
  8. package/dist/cjs/core/BatchPromptBuilder.d.ts.map +1 -1
  9. package/dist/cjs/core/BatchPromptBuilder.js +5 -2
  10. package/dist/cjs/core/BatchPromptBuilder.js.map +1 -1
  11. package/dist/cjs/core/ResponseEngine.d.ts.map +1 -1
  12. package/dist/cjs/core/ResponseEngine.js +6 -3
  13. package/dist/cjs/core/ResponseEngine.js.map +1 -1
  14. package/dist/cjs/core/ResponseModal.d.ts.map +1 -1
  15. package/dist/cjs/core/ResponseModal.js +18 -17
  16. package/dist/cjs/core/ResponseModal.js.map +1 -1
  17. package/dist/cjs/core/RoutingEngine.d.ts.map +1 -1
  18. package/dist/cjs/core/RoutingEngine.js +8 -73
  19. package/dist/cjs/core/RoutingEngine.js.map +1 -1
  20. package/dist/core/Agent.d.ts +202 -67
  21. package/dist/core/Agent.d.ts.map +1 -1
  22. package/dist/core/Agent.js +366 -158
  23. package/dist/core/Agent.js.map +1 -1
  24. package/dist/core/BatchExecutor.d.ts.map +1 -1
  25. package/dist/core/BatchExecutor.js +4 -2
  26. package/dist/core/BatchExecutor.js.map +1 -1
  27. package/dist/core/BatchPromptBuilder.d.ts.map +1 -1
  28. package/dist/core/BatchPromptBuilder.js +5 -2
  29. package/dist/core/BatchPromptBuilder.js.map +1 -1
  30. package/dist/core/ResponseEngine.d.ts.map +1 -1
  31. package/dist/core/ResponseEngine.js +6 -3
  32. package/dist/core/ResponseEngine.js.map +1 -1
  33. package/dist/core/ResponseModal.d.ts.map +1 -1
  34. package/dist/core/ResponseModal.js +18 -17
  35. package/dist/core/ResponseModal.js.map +1 -1
  36. package/dist/core/RoutingEngine.d.ts.map +1 -1
  37. package/dist/core/RoutingEngine.js +8 -73
  38. package/dist/core/RoutingEngine.js.map +1 -1
  39. package/docs/api/README.md +2 -2
  40. package/docs/api/overview.md +1 -1
  41. package/docs/architecture/data-extraction-flow.md +17 -19
  42. package/docs/core/conversation-flows/data-collection.md +2 -2
  43. package/docs/core/error-handling.md +3 -4
  44. package/package.json +2 -2
  45. package/src/core/Agent.ts +427 -195
  46. package/src/core/BatchExecutor.ts +5 -2
  47. package/src/core/BatchPromptBuilder.ts +41 -38
  48. package/src/core/ResponseEngine.ts +56 -53
  49. package/src/core/ResponseModal.ts +79 -81
  50. package/src/core/RoutingEngine.ts +67 -149
@@ -358,7 +358,7 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
358
358
  if (this.agent && 'tool' in this.agent && this.agent.tool) {
359
359
  return this.agent.tool;
360
360
  }
361
-
361
+
362
362
  // Log warning if ToolManager is not available
363
363
  logger.warn(`[ResponseModal] ToolManager not available on agent - tool execution will use fallback methods`);
364
364
  return undefined;
@@ -514,7 +514,7 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
514
514
  });
515
515
 
516
516
  let updatedSession = routingResult.session;
517
- let isRouteComplete = routingResult.isRouteComplete;
517
+ const isRouteComplete = routingResult.isRouteComplete;
518
518
 
519
519
  // PRE-EXTRACTION: If entering a route that collects data, extract data from user message first
520
520
  // This allows us to skip steps whose data is already provided
@@ -544,15 +544,6 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
544
544
  updatedSession = mergeCollected(updatedSession, extractedData);
545
545
  // Also update agent's collected data
546
546
  await this.agent.updateCollectedData(extractedData);
547
-
548
- // Re-check route completion after pre-extraction
549
- const allRequiredFieldsCollected = routingResult.selectedRoute.isComplete(updatedSession.data || {});
550
- if (allRequiredFieldsCollected) {
551
- logger.debug(
552
- `[ResponseModal] Route ${routingResult.selectedRoute.title} completed after pre-extraction`
553
- );
554
- isRouteComplete = true;
555
- }
556
547
  }
557
548
  }
558
549
  }
@@ -565,7 +556,7 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
565
556
 
566
557
  if (routingResult.selectedRoute && !isRouteComplete) {
567
558
  // Determine current step position for batch determination
568
- const currentStep = routingResult.selectedStep ||
559
+ const currentStep = routingResult.selectedStep ||
569
560
  (updatedSession.currentStep ? routingResult.selectedRoute.getStep(updatedSession.currentStep.id) : undefined);
570
561
 
571
562
  logger.debug(`[ResponseModal] Determining batch starting from step: ${currentStep?.id || 'initial'}`);
@@ -642,7 +633,7 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
642
633
  session: SessionState<TData>;
643
634
  signal?: AbortSignal;
644
635
  }): Promise<Partial<TData>> {
645
- const { route, history, context, signal } = params;
636
+ const { route, history, signal } = params;
646
637
 
647
638
  // Build a schema for data extraction based on route's fields
648
639
  const extractionSchema = this.agent.getSchema();
@@ -683,7 +674,10 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
683
674
  const result = await agentOptions.provider.generateMessage<TContext, Partial<TData>>({
684
675
  prompt: extractionPrompt.join('\n'),
685
676
  history,
686
- context,
677
+ context: {} as TContext, // Passed as empty object so AI doesn't "extract" from context
678
+ // NOTE: context is intentionally NOT passed here.
679
+ // Passing context caused the AI to "extract" data from the lead's context
680
+ // (e.g., name, sector, city) instead of from what the user actually said.
687
681
  signal,
688
682
  parameters: {
689
683
  jsonSchema: extractionSchema,
@@ -705,13 +699,13 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
705
699
  private async generateUnifiedResponse(
706
700
  responseContext: ResponseContext<TContext, TData>
707
701
  ): Promise<AgentResponse<TData>> {
708
- const {
709
- effectiveContext,
710
- session: initialSession,
711
- history,
712
- selectedRoute,
713
- selectedStep,
714
- responseDirectives,
702
+ const {
703
+ effectiveContext,
704
+ session: initialSession,
705
+ history,
706
+ selectedRoute,
707
+ selectedStep,
708
+ responseDirectives,
715
709
  isRouteComplete,
716
710
  batchSteps,
717
711
  batchStoppedReason,
@@ -729,7 +723,7 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
729
723
  let stoppedReason: StoppedReason | undefined;
730
724
 
731
725
 
732
-
726
+
733
727
  if (selectedRoute && !isRouteComplete) {
734
728
  // Check if we have batch steps to execute
735
729
  if (batchSteps && batchSteps.length > 0) {
@@ -770,7 +764,7 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
770
764
  message = result.message;
771
765
  toolCalls = result.toolCalls;
772
766
  session = result.session;
773
-
767
+
774
768
  // Track executed step for single-step execution
775
769
  if (selectedStep) {
776
770
  executedSteps = [{
@@ -815,7 +809,7 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
815
809
  context: effectiveContext,
816
810
  session,
817
811
  });
818
-
812
+
819
813
  // For fallback responses, set empty executedSteps and no stoppedReason
820
814
  // since there's no route/step execution happening
821
815
  executedSteps = [];
@@ -897,12 +891,12 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
897
891
  logger.error(`[ResponseModal] Prepare hook failed:`, prepareResult.error);
898
892
  throw new ResponseGenerationError(
899
893
  `Prepare hook failed: ${prepareResult.error?.message}`,
900
- {
901
- phase: 'prepare_hooks',
902
- context: {
894
+ {
895
+ phase: 'prepare_hooks',
896
+ context: {
903
897
  stepId: prepareResult.error?.stepId,
904
898
  executedSteps: prepareResult.executedSteps,
905
- }
899
+ }
906
900
  }
907
901
  );
908
902
  }
@@ -1029,36 +1023,36 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
1029
1023
  * @private
1030
1024
  */
1031
1025
  private buildBatchResponseSchema(collectFields: string[]): Record<string, unknown> {
1032
- const properties: Record<string, unknown> = {
1033
- message: {
1034
- type: "string",
1035
- description: "Your response to the user",
1036
- },
1037
- };
1026
+ const properties: Record<string, unknown> = {
1027
+ message: {
1028
+ type: "string",
1029
+ description: "Natural, conversational response directed at the user. Must NOT contain field names, raw data, or internal information.",
1030
+ },
1031
+ };
1038
1032
 
1039
- const agentSchema = this.agent.getSchema();
1033
+ const agentSchema = this.agent.getSchema();
1040
1034
 
1041
- // Add collect fields to schema, using agent schema definitions when available
1042
- for (const field of collectFields) {
1043
- if (agentSchema?.properties && agentSchema.properties[field]) {
1044
- properties[field] = agentSchema.properties[field];
1045
- } else {
1046
- // Dynamic fallback when no agent schema is defined
1047
- properties[field] = {
1048
- type: "string",
1049
- description: `Collected value for ${field}`,
1050
- };
1051
- }
1035
+ // Add collect fields to schema, using agent schema definitions when available
1036
+ for (const field of collectFields) {
1037
+ if (agentSchema?.properties && agentSchema.properties[field]) {
1038
+ properties[field] = agentSchema.properties[field];
1039
+ } else {
1040
+ // Dynamic fallback when no agent schema is defined
1041
+ properties[field] = {
1042
+ type: "string",
1043
+ description: `Collected value for ${field}`,
1044
+ };
1052
1045
  }
1053
-
1054
- return {
1055
- type: "object",
1056
- properties,
1057
- required: ["message"],
1058
- additionalProperties: true,
1059
- };
1060
1046
  }
1061
1047
 
1048
+ return {
1049
+ type: "object",
1050
+ properties,
1051
+ required: ["message"],
1052
+ additionalProperties: true,
1053
+ };
1054
+ }
1055
+
1062
1056
  /**
1063
1057
  * Collect available tools from all steps in the batch
1064
1058
  * @private
@@ -1114,13 +1108,13 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
1114
1108
  private async *generateUnifiedStreamingResponse(
1115
1109
  responseContext: ResponseContext<TContext, TData>
1116
1110
  ): AsyncGenerator<AgentResponseStreamChunk<TData>> {
1117
- const {
1118
- effectiveContext,
1119
- session: initialSession,
1120
- history,
1121
- selectedRoute,
1122
- selectedStep,
1123
- responseDirectives,
1111
+ const {
1112
+ effectiveContext,
1113
+ session: initialSession,
1114
+ history,
1115
+ selectedRoute,
1116
+ selectedStep,
1117
+ responseDirectives,
1124
1118
  isRouteComplete,
1125
1119
  batchSteps,
1126
1120
  batchStoppedReason,
@@ -1414,13 +1408,13 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
1414
1408
  // Get candidate steps based on current position in the route
1415
1409
  const routingEngine = this.agent.getRoutingEngine();
1416
1410
  const candidates = await routingEngine.getCandidateStepsWithConditions(
1417
- selectedRoute,
1411
+ selectedRoute,
1418
1412
  currentStep, // Pass current step instead of undefined to maintain progression
1419
1413
  createTemplateContext({ data: session.data, session, context })
1420
1414
  );
1421
-
1415
+
1422
1416
  logger.debug(`[ResponseModal] Found ${candidates.length} candidate steps${currentStep ? ' from current step ' + currentStep.id : ' (new route entry)'}`);
1423
-
1417
+
1424
1418
  if (candidates.length > 0) {
1425
1419
  nextStep = candidates[0].step;
1426
1420
  logger.debug(`[ResponseModal] Using first valid step: ${nextStep.id}${currentStep ? ' (progressing from ' + currentStep.id + ')' : ' for new route'}`);
@@ -1563,11 +1557,11 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
1563
1557
  // Get candidate steps based on current position in the route
1564
1558
  const routingEngine = this.agent.getRoutingEngine();
1565
1559
  const candidates = await routingEngine.getCandidateStepsWithConditions(
1566
- selectedRoute,
1560
+ selectedRoute,
1567
1561
  currentStep, // Pass current step instead of undefined to maintain progression
1568
1562
  createTemplateContext({ data: session.data, session, context })
1569
1563
  );
1570
-
1564
+
1571
1565
  if (candidates.length > 0) {
1572
1566
  nextStep = candidates[0].step;
1573
1567
  logger.debug(`[ResponseModal] Using first valid step: ${nextStep.id}${currentStep ? ' (progressing from ' + currentStep.id + ')' : ' for new route'}`);
@@ -1841,7 +1835,7 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
1841
1835
  // After first iteration, don't provide tools to force a text response
1842
1836
  const agentOptions = this.agent.getAgentOptions();
1843
1837
  const shouldProvideTools = toolLoopCount === 1;
1844
-
1838
+
1845
1839
  logger.debug(`[ResponseModal] Making follow-up AI call (loop ${toolLoopCount}):`, {
1846
1840
  providingTools: shouldProvideTools,
1847
1841
  toolsCount: shouldProvideTools ? availableTools.length : 0,
@@ -1991,17 +1985,17 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
1991
1985
 
1992
1986
  // Collect ALL route fields (required + optional) from structured response
1993
1987
  const allRouteFields = new Set<string>();
1994
-
1988
+
1995
1989
  // Add route required fields
1996
1990
  if (selectedRoute.requiredFields) {
1997
1991
  selectedRoute.requiredFields.forEach(field => allRouteFields.add(String(field)));
1998
1992
  }
1999
-
1993
+
2000
1994
  // Add route optional fields
2001
1995
  if (selectedRoute.optionalFields) {
2002
1996
  selectedRoute.optionalFields.forEach(field => allRouteFields.add(String(field)));
2003
1997
  }
2004
-
1998
+
2005
1999
  // Also include current step's collect fields (in case they're not in route fields)
2006
2000
  if (nextStep?.collect) {
2007
2001
  nextStep.collect.forEach(field => allRouteFields.add(String(field)));
@@ -2081,7 +2075,7 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
2081
2075
  id: endStepSpec.id || END_ROUTE_ID,
2082
2076
  collect: endStepSpec.collect,
2083
2077
  requires: endStepSpec.requires,
2084
- prompt: endStepSpec.prompt || "Summarize what was accomplished and confirm completion based on the conversation history and collected data",
2078
+ prompt: endStepSpec.prompt || "Send a brief, natural farewell message thanking the user. Do NOT list or mention any collected data, field names, or internal information.",
2085
2079
  });
2086
2080
 
2087
2081
  // Build response schema for completion (message only, no data collection)
@@ -2090,13 +2084,13 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
2090
2084
  properties: {
2091
2085
  message: {
2092
2086
  type: "string",
2093
- description: "Completion message confirming what was accomplished",
2087
+ description: "A natural, warm farewell message for the user. Must NOT contain task names, field names, collected data, or any internal/technical information.",
2094
2088
  },
2095
2089
  },
2096
2090
  required: ["message"],
2097
2091
  additionalProperties: false,
2098
2092
  };
2099
-
2093
+
2100
2094
  const templateContext = createTemplateContext({ context, session, history: historyEvents });
2101
2095
 
2102
2096
  // Build completion response prompt using ResponseEngine
@@ -2105,8 +2099,8 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
2105
2099
  ...this.agent.getGuidelines().filter(g => !g.condition),
2106
2100
  ...selectedRoute.getGuidelines().filter(g => !g.condition),
2107
2101
  ];
2108
- let completitionPrompt = "Summarize what was accomplished and confirm completion"
2109
- if(endStepSpec.prompt){
2102
+ let completitionPrompt = "Send a brief, natural farewell message. Do NOT mention internal data or task details."
2103
+ if (endStepSpec.prompt) {
2110
2104
  completitionPrompt = await render(endStepSpec.prompt, templateContext)
2111
2105
  }
2112
2106
 
@@ -2116,9 +2110,13 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
2116
2110
  rules: selectedRoute.getRules(),
2117
2111
  prohibitions: selectedRoute.getProhibitions(),
2118
2112
  directives: [
2119
- `Task completed: ${selectedRoute.title}`,
2120
- `Collected data: ${JSON.stringify(session.data, null, 2)}`,
2121
- "Do NOT ask for more information - the task is complete",
2113
+ "The conversation task has been completed successfully",
2114
+ "Generate a natural, friendly farewell message for the user",
2115
+ "Do NOT mention task names, route names, collected data, field names, or any internal/technical information",
2116
+ "Do NOT list or summarize the data you collected - the user already knows what they told you",
2117
+ "Do NOT use words like 'tarefa', 'dados coletados', 'prospecção', 'concluída' or similar internal terms",
2118
+ "Keep it brief, warm, and conversational - as if ending a natural conversation",
2119
+ "Do NOT ask for more information - the conversation is ending",
2122
2120
  completitionPrompt,
2123
2121
  ],
2124
2122
  history: historyEvents,
@@ -2197,7 +2195,7 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
2197
2195
  id: endStepSpec.id || END_ROUTE_ID,
2198
2196
  collect: endStepSpec.collect,
2199
2197
  requires: endStepSpec.requires,
2200
- prompt: endStepSpec.prompt || "Summarize what was accomplished and confirm completion based on the conversation history and collected data",
2198
+ prompt: endStepSpec.prompt || "Send a brief, natural farewell message thanking the user. Do NOT list or mention any collected data, field names, or internal information.",
2201
2199
  });
2202
2200
 
2203
2201
  // Build response schema for completion
@@ -2443,7 +2441,7 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
2443
2441
 
2444
2442
  // Fallback to legacy resolution if ToolManager not available
2445
2443
  logger.warn(`[ResponseModal] ToolManager not available, using legacy tool resolution for: ${toolName}`);
2446
-
2444
+
2447
2445
  // Check route-level tools first (if route provided)
2448
2446
  if (route) {
2449
2447
  const routeTool = route
@@ -2487,7 +2485,7 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
2487
2485
 
2488
2486
  // Fallback to legacy collection logic if ToolManager not available
2489
2487
  logger.warn(`[ResponseModal] ToolManager not available, using legacy tool collection`);
2490
-
2488
+
2491
2489
  const availableTools = new Map<string, Tool<TContext, TData>>();
2492
2490
 
2493
2491
  // Add agent-level tools
@@ -2581,7 +2579,7 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
2581
2579
  } else {
2582
2580
  // Fallback to legacy resolution if ToolManager not available
2583
2581
  logger.warn(`[ResponseModal] ToolManager not available, using legacy tool resolution for prepare/finalize: ${prepareOrFinalize}`);
2584
-
2582
+
2585
2583
  const availableTools = new Map<string, Tool<TContext, TData>>();
2586
2584
 
2587
2585
  // Add agent-level tools
@@ -179,18 +179,17 @@ export class RoutingEngine<TContext = unknown, TData = unknown> {
179
179
  }
180
180
  }
181
181
 
182
- // No candidates means route is likely complete or has no valid next steps
182
+ // No candidates means route has no valid next steps (edge case)
183
+ // Don't mark as complete based on data alone — only END_ROUTE completes a route
183
184
  if (candidates.length === 0) {
184
- const dataComplete = route.isComplete(updatedSession.data || {});
185
185
  logger.debug(
186
- `[RoutingEngine] Single-route: No valid steps found - ` +
187
- `(data: ${dataComplete ? 'complete' : 'incomplete'}, marking as ${dataComplete ? 'complete' : 'incomplete'})`
186
+ `[RoutingEngine] Single-route: No valid candidate steps found`
188
187
  );
189
188
  return {
190
189
  selectedRoute,
191
190
  selectedStep: undefined,
192
191
  session: updatedSession,
193
- isRouteComplete: dataComplete,
192
+ isRouteComplete: false,
194
193
  completedRoutes,
195
194
  };
196
195
  }
@@ -379,26 +378,9 @@ export class RoutingEngine<TContext = unknown, TData = unknown> {
379
378
  templateContext: TemplateContext<TContext, TData>
380
379
  ): Promise<CandidateStep<TContext, TData>[]> {
381
380
  const candidates: CandidateStep<TContext, TData>[] = [];
382
- const data = templateContext.data || {};
383
-
384
- // Check if all required fields are collected
385
- const allRequiredFieldsCollected = route.isComplete(data);
386
381
 
387
382
  if (!currentStep) {
388
- // Entering route for the first time
389
-
390
- // If all required fields already collected, route is immediately complete
391
- if (allRequiredFieldsCollected) {
392
- logger.debug(
393
- `[RoutingEngine] Route ${route.title} complete on entry: all required fields already collected`
394
- );
395
- // Return a completion marker - use initial step with completion flag
396
- candidates.push({
397
- step: route.initialStep,
398
- isRouteComplete: true,
399
- });
400
- return candidates;
401
- }
383
+ // Entering route for the first time — always start the step flow
402
384
 
403
385
  const initialStep = route.initialStep;
404
386
  const skipResult = await initialStep.evaluateSkipIf(templateContext);
@@ -437,68 +419,7 @@ export class RoutingEngine<TContext = unknown, TData = unknown> {
437
419
  return candidates;
438
420
  }
439
421
 
440
- // Check if all required fields are now collected (may have been collected during this step)
441
- if (allRequiredFieldsCollected) {
442
- // Required fields are complete - check if we should continue for optional fields
443
- const transitions = currentStep.getTransitions();
444
- const optionalFieldCandidates: CandidateStep<TContext, TData>[] = [];
445
-
446
- for (const transition of transitions) {
447
- const target = transition;
448
-
449
- // Check for END_ROUTE transition
450
- if (target && target.id === END_ROUTE_ID) {
451
- continue;
452
- }
453
-
454
- if (!target) continue;
455
-
456
- // Check if this step collects only optional fields
457
- const collectsOnlyOptional = target.collect && target.collect.length > 0 &&
458
- target.collect.every(field =>
459
- route.optionalFields?.includes(field)
460
- );
461
-
462
- if (collectsOnlyOptional) {
463
- // This step collects optional fields - it's a candidate
464
- const skipResult = await target.evaluateSkipIf(templateContext);
465
- if (!skipResult.shouldSkip) {
466
- optionalFieldCandidates.push({
467
- step: target,
468
- isRouteComplete: false,
469
- });
470
- }
471
- }
472
- }
473
-
474
- // If we have optional field candidates, include them along with END_ROUTE option
475
- if (optionalFieldCandidates.length > 0) {
476
- logger.debug(
477
- `[RoutingEngine] Required fields complete, but ${optionalFieldCandidates.length} optional field steps available`
478
- );
479
- // Add optional field steps as candidates
480
- candidates.push(...optionalFieldCandidates);
481
- // Also add END_ROUTE as a candidate (AI can choose to skip optional fields)
482
- candidates.push({
483
- step: currentStep,
484
- isRouteComplete: true,
485
- });
486
- return candidates;
487
- }
488
-
489
- // No optional fields to collect - route is complete
490
- logger.debug(
491
- `[RoutingEngine] Route ${route.title} complete: all required fields collected, no optional fields remain`
492
- );
493
- return [
494
- {
495
- step: currentStep,
496
- isRouteComplete: true,
497
- },
498
- ];
499
- }
500
-
501
- // Required fields not yet complete - continue normal step progression
422
+ // Continue normal step progression routes complete via END_ROUTE, not via data collection
502
423
  const transitions = currentStep.getTransitions();
503
424
  let hasEndRoute = false;
504
425
 
@@ -690,12 +611,9 @@ export class RoutingEngine<TContext = unknown, TData = unknown> {
690
611
  // Don't include steps in routing if route is complete
691
612
  activeRouteSteps = undefined;
692
613
  } else if (candidates.length === 0) {
693
- // No candidates - check if data is complete
694
- const dataComplete = activeRoute.isComplete(updatedSession.data || {});
695
- isRouteComplete = dataComplete;
614
+ // No candidates available don't end route based on data alone
696
615
  logger.debug(
697
- `[RoutingEngine] Route ${activeRoute.title} has no valid steps - ` +
698
- `marking as ${isRouteComplete ? 'complete' : 'incomplete'}`
616
+ `[RoutingEngine] Route ${activeRoute.title} has no valid candidate steps`
699
617
  );
700
618
  activeRouteSteps = undefined;
701
619
  } else {
@@ -911,77 +829,77 @@ export class RoutingEngine<TContext = unknown, TData = unknown> {
911
829
  * @returns Route that should be prioritized for continuation
912
830
  */
913
831
  selectOptimalRoute(
914
- routes: Route<TContext, TData>[],
915
- data: Partial<TData>,
916
- routeScores: Record<string, number>,
917
- currentRouteId?: string
918
- ): Route<TContext, TData> | undefined {
919
- const completionStatus = this.getRouteCompletionStatus(routes, data);
920
- const switchMargin = this.options?.routeSwitchMargin ?? 15;
921
-
922
- // Create weighted scores combining AI intent scores with completion progress
923
- const weightedScores: Array<{ route: Route<TContext, TData>; score: number }> = [];
924
-
925
- for (const route of routes) {
926
- const aiScore = routeScores[route.id] || 0;
927
- const completionProgress = completionStatus.get(route.id) || 0;
928
-
929
- // ALWAYS skip fully completed routes to prevent re-entering finished tasks
930
- if (completionProgress >= 1.0) {
931
- logger.debug(
932
- `[RoutingEngine] Excluding completed route: ${route.title} (100% complete)`
933
- );
934
- continue;
935
- }
832
+ routes: Route<TContext, TData>[],
833
+ data: Partial<TData>,
834
+ routeScores: Record<string, number>,
835
+ currentRouteId?: string
836
+ ): Route<TContext, TData> | undefined {
837
+ const completionStatus = this.getRouteCompletionStatus(routes, data);
838
+ const switchMargin = this.options?.routeSwitchMargin ?? 15;
936
839
 
937
- // Boost partially complete routes that match user intent
938
- let weightedScore = aiScore;
939
- if (completionProgress > 0 && completionProgress < 1.0) {
940
- weightedScore += (completionProgress * 20); // Up to 20 point boost
941
- }
840
+ // Create weighted scores combining AI intent scores with completion progress
841
+ const weightedScores: Array<{ route: Route<TContext, TData>; score: number }> = [];
942
842
 
943
- weightedScores.push({ route, score: weightedScore });
944
- }
843
+ for (const route of routes) {
844
+ const aiScore = routeScores[route.id] || 0;
845
+ const completionProgress = completionStatus.get(route.id) || 0;
945
846
 
946
- // Sort by weighted score descending
947
- weightedScores.sort((a, b) => b.score - a.score);
847
+ // ALWAYS skip fully completed routes to prevent re-entering finished tasks
848
+ if (completionProgress >= 1.0) {
849
+ logger.debug(
850
+ `[RoutingEngine] Excluding completed route: ${route.title} (100% complete)`
851
+ );
852
+ continue;
853
+ }
948
854
 
949
- if (weightedScores.length === 0) {
950
- return undefined;
855
+ // Boost partially complete routes that match user intent
856
+ let weightedScore = aiScore;
857
+ if (completionProgress > 0 && completionProgress < 1.0) {
858
+ weightedScore += (completionProgress * 20); // Up to 20 point boost
951
859
  }
952
860
 
953
- // Apply sticky routing: if there's a current route, only switch if the
954
- // best alternative exceeds the current route's score by the configured margin
955
- if (currentRouteId) {
956
- const currentEntry = weightedScores.find(e => e.route.id === currentRouteId);
957
- const bestEntry = weightedScores[0];
958
-
959
- if (currentEntry && bestEntry.route.id !== currentRouteId) {
960
- if (bestEntry.score < currentEntry.score + switchMargin) {
961
- logger.debug(
962
- `[RoutingEngine] Staying on current route: ${currentEntry.route.title} ` +
963
- `(current: ${currentEntry.score}, best alternative: ${bestEntry.score}, ` +
964
- `margin required: ${switchMargin})`
965
- );
966
- return currentEntry.route;
967
- }
861
+ weightedScores.push({ route, score: weightedScore });
862
+ }
863
+
864
+ // Sort by weighted score descending
865
+ weightedScores.sort((a, b) => b.score - a.score);
866
+
867
+ if (weightedScores.length === 0) {
868
+ return undefined;
869
+ }
870
+
871
+ // Apply sticky routing: if there's a current route, only switch if the
872
+ // best alternative exceeds the current route's score by the configured margin
873
+ if (currentRouteId) {
874
+ const currentEntry = weightedScores.find(e => e.route.id === currentRouteId);
875
+ const bestEntry = weightedScores[0];
876
+
877
+ if (currentEntry && bestEntry.route.id !== currentRouteId) {
878
+ if (bestEntry.score < currentEntry.score + switchMargin) {
968
879
  logger.debug(
969
- `[RoutingEngine] Switching route: ${currentEntry.route.title} → ${bestEntry.route.title} ` +
970
- `(current: ${currentEntry.score}, alternative: ${bestEntry.score}, ` +
971
- `margin: ${switchMargin})`
880
+ `[RoutingEngine] Staying on current route: ${currentEntry.route.title} ` +
881
+ `(current: ${currentEntry.score}, best alternative: ${bestEntry.score}, ` +
882
+ `margin required: ${switchMargin})`
972
883
  );
884
+ return currentEntry.route;
973
885
  }
886
+ logger.debug(
887
+ `[RoutingEngine] Switching route: ${currentEntry.route.title} → ${bestEntry.route.title} ` +
888
+ `(current: ${currentEntry.score}, alternative: ${bestEntry.score}, ` +
889
+ `margin: ${switchMargin})`
890
+ );
974
891
  }
975
-
976
- logger.debug(
977
- `[RoutingEngine] Selected optimal route: ${weightedScores[0].route.title} ` +
978
- `(AI: ${routeScores[weightedScores[0].route.id]}, ` +
979
- `Completion: ${(completionStatus.get(weightedScores[0].route.id) || 0) * 100}%, ` +
980
- `Weighted: ${weightedScores[0].score})`
981
- );
982
- return weightedScores[0].route;
983
892
  }
984
893
 
894
+ logger.debug(
895
+ `[RoutingEngine] Selected optimal route: ${weightedScores[0].route.title} ` +
896
+ `(AI: ${routeScores[weightedScores[0].route.id]}, ` +
897
+ `Completion: ${(completionStatus.get(weightedScores[0].route.id) || 0) * 100}%, ` +
898
+ `Weighted: ${weightedScores[0].score})`
899
+ );
900
+ return weightedScores[0].route;
901
+ }
902
+
985
903
  /**
986
904
  * Build prompt for step selection within a single route
987
905
  * @private