@falai/agent 1.1.1 → 1.1.3
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/dist/cjs/core/Agent.d.ts +202 -67
- package/dist/cjs/core/Agent.d.ts.map +1 -1
- package/dist/cjs/core/Agent.js +366 -158
- package/dist/cjs/core/Agent.js.map +1 -1
- package/dist/cjs/core/BatchExecutor.d.ts.map +1 -1
- package/dist/cjs/core/BatchExecutor.js +4 -2
- package/dist/cjs/core/BatchExecutor.js.map +1 -1
- package/dist/cjs/core/BatchPromptBuilder.d.ts.map +1 -1
- package/dist/cjs/core/BatchPromptBuilder.js +15 -12
- package/dist/cjs/core/BatchPromptBuilder.js.map +1 -1
- package/dist/cjs/core/PromptComposer.d.ts.map +1 -1
- package/dist/cjs/core/PromptComposer.js +16 -8
- package/dist/cjs/core/PromptComposer.js.map +1 -1
- package/dist/cjs/core/ResponseEngine.d.ts.map +1 -1
- package/dist/cjs/core/ResponseEngine.js +6 -3
- package/dist/cjs/core/ResponseEngine.js.map +1 -1
- package/dist/cjs/core/ResponseModal.d.ts.map +1 -1
- package/dist/cjs/core/ResponseModal.js +22 -21
- package/dist/cjs/core/ResponseModal.js.map +1 -1
- package/dist/cjs/core/RoutingEngine.d.ts.map +1 -1
- package/dist/cjs/core/RoutingEngine.js +8 -73
- package/dist/cjs/core/RoutingEngine.js.map +1 -1
- package/dist/core/Agent.d.ts +202 -67
- package/dist/core/Agent.d.ts.map +1 -1
- package/dist/core/Agent.js +366 -158
- package/dist/core/Agent.js.map +1 -1
- package/dist/core/BatchExecutor.d.ts.map +1 -1
- package/dist/core/BatchExecutor.js +4 -2
- package/dist/core/BatchExecutor.js.map +1 -1
- package/dist/core/BatchPromptBuilder.d.ts.map +1 -1
- package/dist/core/BatchPromptBuilder.js +15 -12
- package/dist/core/BatchPromptBuilder.js.map +1 -1
- package/dist/core/PromptComposer.d.ts.map +1 -1
- package/dist/core/PromptComposer.js +16 -8
- package/dist/core/PromptComposer.js.map +1 -1
- package/dist/core/ResponseEngine.d.ts.map +1 -1
- package/dist/core/ResponseEngine.js +6 -3
- package/dist/core/ResponseEngine.js.map +1 -1
- package/dist/core/ResponseModal.d.ts.map +1 -1
- package/dist/core/ResponseModal.js +22 -21
- package/dist/core/ResponseModal.js.map +1 -1
- package/dist/core/RoutingEngine.d.ts.map +1 -1
- package/dist/core/RoutingEngine.js +8 -73
- package/dist/core/RoutingEngine.js.map +1 -1
- package/docs/api/README.md +2 -2
- package/docs/api/overview.md +1 -1
- package/docs/architecture/data-extraction-flow.md +17 -19
- package/docs/core/conversation-flows/data-collection.md +2 -2
- package/docs/core/error-handling.md +3 -4
- package/package.json +2 -2
- package/src/core/Agent.ts +427 -195
- package/src/core/BatchExecutor.ts +5 -2
- package/src/core/BatchPromptBuilder.ts +51 -48
- package/src/core/PromptComposer.ts +30 -13
- package/src/core/ResponseEngine.ts +56 -53
- package/src/core/ResponseModal.ts +83 -85
- 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
|
-
|
|
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,
|
|
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
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
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
|
-
|
|
1033
|
+
const agentSchema = this.agent.getSchema();
|
|
1040
1034
|
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
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'}`);
|
|
@@ -1468,8 +1462,8 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
|
|
|
1468
1462
|
const responsePrompt = await this.responseEngine.buildResponsePrompt({
|
|
1469
1463
|
route: selectedRoute,
|
|
1470
1464
|
currentStep: nextStep,
|
|
1471
|
-
rules:
|
|
1472
|
-
prohibitions:
|
|
1465
|
+
rules: selectedRoute.getRules(),
|
|
1466
|
+
prohibitions: selectedRoute.getProhibitions(),
|
|
1473
1467
|
directives: responseDirectives,
|
|
1474
1468
|
history: historyEvents, // Use Event[] for buildResponsePrompt
|
|
1475
1469
|
lastMessage: lastMessageText, // Use string for buildResponsePrompt
|
|
@@ -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'}`);
|
|
@@ -1611,8 +1605,8 @@ export class ResponseModal<TContext = unknown, TData = unknown> {
|
|
|
1611
1605
|
const responsePrompt = await this.responseEngine.buildResponsePrompt({
|
|
1612
1606
|
route: selectedRoute,
|
|
1613
1607
|
currentStep: nextStep,
|
|
1614
|
-
rules:
|
|
1615
|
-
prohibitions:
|
|
1608
|
+
rules: selectedRoute.getRules(),
|
|
1609
|
+
prohibitions: selectedRoute.getProhibitions(),
|
|
1616
1610
|
directives: responseDirectives,
|
|
1617
1611
|
history: historyEvents, // Use Event[] for buildResponsePrompt
|
|
1618
1612
|
lastMessage: lastMessageText, // Use string for buildResponsePrompt
|
|
@@ -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 || "
|
|
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: "
|
|
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 =
|
|
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
|
-
|
|
2120
|
-
|
|
2121
|
-
"Do NOT
|
|
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 || "
|
|
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
|
|
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:
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
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
|
-
|
|
938
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
947
|
-
|
|
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
|
-
|
|
950
|
-
|
|
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
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
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]
|
|
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
|