@falai/agent 0.6.7 → 0.6.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +162 -30
- package/dist/cjs/core/Agent.d.ts +18 -0
- package/dist/cjs/core/Agent.d.ts.map +1 -1
- package/dist/cjs/core/Agent.js +218 -15
- package/dist/cjs/core/Agent.js.map +1 -1
- package/dist/cjs/core/Route.d.ts +12 -1
- package/dist/cjs/core/Route.d.ts.map +1 -1
- package/dist/cjs/core/Route.js +38 -1
- package/dist/cjs/core/Route.js.map +1 -1
- package/dist/cjs/core/RoutingEngine.d.ts.map +1 -1
- package/dist/cjs/core/RoutingEngine.js +3 -1
- package/dist/cjs/core/RoutingEngine.js.map +1 -1
- package/dist/cjs/core/State.js +1 -1
- package/dist/cjs/core/State.js.map +1 -1
- package/dist/cjs/index.d.ts +2 -2
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/types/route.d.ts +52 -1
- package/dist/cjs/types/route.d.ts.map +1 -1
- package/dist/cjs/types/session.d.ts +17 -1
- package/dist/cjs/types/session.d.ts.map +1 -1
- package/dist/cjs/types/session.js.map +1 -1
- package/dist/core/Agent.d.ts +18 -0
- package/dist/core/Agent.d.ts.map +1 -1
- package/dist/core/Agent.js +219 -16
- package/dist/core/Agent.js.map +1 -1
- package/dist/core/Route.d.ts +12 -1
- package/dist/core/Route.d.ts.map +1 -1
- package/dist/core/Route.js +38 -1
- package/dist/core/Route.js.map +1 -1
- package/dist/core/RoutingEngine.d.ts.map +1 -1
- package/dist/core/RoutingEngine.js +3 -1
- package/dist/core/RoutingEngine.js.map +1 -1
- package/dist/core/State.js +1 -1
- package/dist/core/State.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/types/route.d.ts +52 -1
- package/dist/types/route.d.ts.map +1 -1
- package/dist/types/session.d.ts +17 -1
- package/dist/types/session.d.ts.map +1 -1
- package/dist/types/session.js.map +1 -1
- package/docs/EXAMPLES.md +51 -2
- package/docs/ROUTES.md +345 -1
- package/docs/STATES.md +97 -7
- package/examples/business-onboarding.ts +10 -12
- package/examples/company-qna-agent.ts +4 -5
- package/examples/healthcare-agent.ts +63 -0
- package/examples/persistent-onboarding.ts +6 -8
- package/examples/route-transitions.ts +242 -0
- package/examples/travel-agent.ts +60 -0
- package/package.json +1 -1
- package/src/core/Agent.ts +338 -16
- package/src/core/Route.ts +53 -1
- package/src/core/RoutingEngine.ts +3 -1
- package/src/core/State.ts +1 -1
- package/src/index.ts +3 -1
- package/src/types/route.ts +58 -1
- package/src/types/session.ts +20 -2
package/src/core/Agent.ts
CHANGED
|
@@ -8,7 +8,7 @@ import type { RouteOptions } from "../types/route";
|
|
|
8
8
|
|
|
9
9
|
import type { SessionState } from "../types/session";
|
|
10
10
|
import type { AgentStructuredResponse } from "../types/ai";
|
|
11
|
-
import { createSession, enterState, mergeExtracted } from "../types/session";
|
|
11
|
+
import { createSession, enterRoute, enterState, mergeExtracted } from "../types/session";
|
|
12
12
|
import { PromptComposer } from "./PromptComposer";
|
|
13
13
|
import { logger, LoggerLevel } from "../utils/logger";
|
|
14
14
|
|
|
@@ -243,11 +243,10 @@ export class Agent<TContext = unknown> {
|
|
|
243
243
|
|
|
244
244
|
// Trigger lifecycle hook if configured
|
|
245
245
|
if (this.options.hooks?.onExtractedUpdate) {
|
|
246
|
-
|
|
246
|
+
newExtracted = (await this.options.hooks.onExtractedUpdate(
|
|
247
247
|
newExtracted,
|
|
248
248
|
previousExtracted
|
|
249
249
|
)) as Partial<TExtracted>;
|
|
250
|
-
newExtracted = updatedExtracted;
|
|
251
250
|
}
|
|
252
251
|
|
|
253
252
|
// Return updated session
|
|
@@ -363,7 +362,43 @@ export class Agent<TContext = unknown> {
|
|
|
363
362
|
let selectedState: State<TContext> | undefined;
|
|
364
363
|
let isRouteComplete = false;
|
|
365
364
|
|
|
366
|
-
|
|
365
|
+
// Check for pending transition from previous route completion
|
|
366
|
+
if (session.pendingTransition) {
|
|
367
|
+
const targetRoute = this.routes.find(
|
|
368
|
+
(r) => r.id === session.pendingTransition?.targetRouteId
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
if (targetRoute) {
|
|
372
|
+
logger.debug(
|
|
373
|
+
`[Agent] Auto-transitioning from pending transition to route: ${targetRoute.title}`
|
|
374
|
+
);
|
|
375
|
+
// Clear pending transition and enter new route
|
|
376
|
+
session = {
|
|
377
|
+
...session,
|
|
378
|
+
pendingTransition: undefined,
|
|
379
|
+
};
|
|
380
|
+
session = enterRoute(session, targetRoute.id, targetRoute.title);
|
|
381
|
+
|
|
382
|
+
// Merge initial data if available
|
|
383
|
+
if (targetRoute.initialData) {
|
|
384
|
+
session = mergeExtracted(session, targetRoute.initialData);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
selectedRoute = targetRoute;
|
|
388
|
+
} else {
|
|
389
|
+
logger.warn(
|
|
390
|
+
`[Agent] Pending transition target route not found: ${session.pendingTransition.targetRouteId}`
|
|
391
|
+
);
|
|
392
|
+
// Clear invalid transition
|
|
393
|
+
session = {
|
|
394
|
+
...session,
|
|
395
|
+
pendingTransition: undefined,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// If no pending transition or transition handled, do normal routing
|
|
401
|
+
if (this.routes.length > 0 && !selectedRoute) {
|
|
367
402
|
const orchestration = await this.routingEngine.decideRouteAndState({
|
|
368
403
|
routes: this.routes,
|
|
369
404
|
session,
|
|
@@ -531,19 +566,118 @@ export class Agent<TContext = unknown> {
|
|
|
531
566
|
};
|
|
532
567
|
}
|
|
533
568
|
} else if (isRouteComplete && selectedRoute) {
|
|
534
|
-
// Route is complete -
|
|
569
|
+
// Route is complete - generate completion message then check for onComplete transition
|
|
570
|
+
const lastUserMessage = getLastMessageFromHistory(history);
|
|
571
|
+
|
|
572
|
+
// Get endState spec from route
|
|
573
|
+
const endStateSpec = selectedRoute.endStateSpec;
|
|
574
|
+
|
|
575
|
+
// Create a temporary state for completion message generation using endState configuration
|
|
576
|
+
const completionState = new State<TContext>(
|
|
577
|
+
selectedRoute.id,
|
|
578
|
+
endStateSpec.chatState || "Summarize what was accomplished and confirm completion",
|
|
579
|
+
endStateSpec.id || END_STATE_ID,
|
|
580
|
+
endStateSpec.gather,
|
|
581
|
+
undefined,
|
|
582
|
+
endStateSpec.requiredData,
|
|
583
|
+
endStateSpec.chatState || "Summarize what was accomplished and confirm completion based on the conversation history and collected data"
|
|
584
|
+
);
|
|
585
|
+
|
|
586
|
+
// Build response schema for completion
|
|
587
|
+
const responseSchema = this.responseEngine.responseSchemaForRoute(
|
|
588
|
+
selectedRoute,
|
|
589
|
+
completionState
|
|
590
|
+
);
|
|
591
|
+
|
|
592
|
+
// Build completion response prompt
|
|
593
|
+
const completionPrompt = this.responseEngine.buildResponsePrompt(
|
|
594
|
+
selectedRoute,
|
|
595
|
+
completionState,
|
|
596
|
+
selectedRoute.getRules(),
|
|
597
|
+
selectedRoute.getProhibitions(),
|
|
598
|
+
undefined, // No directives for completion
|
|
599
|
+
history,
|
|
600
|
+
lastUserMessage,
|
|
601
|
+
{
|
|
602
|
+
name: this.options.name,
|
|
603
|
+
goal: this.options.goal,
|
|
604
|
+
description: this.options.description,
|
|
605
|
+
personality: this.options.personality,
|
|
606
|
+
}
|
|
607
|
+
);
|
|
608
|
+
|
|
609
|
+
// Stream completion message using AI provider
|
|
610
|
+
const stream = this.options.ai.generateMessageStream({
|
|
611
|
+
prompt: completionPrompt,
|
|
612
|
+
history,
|
|
613
|
+
context: effectiveContext,
|
|
614
|
+
signal,
|
|
615
|
+
parameters: {
|
|
616
|
+
jsonSchema: responseSchema,
|
|
617
|
+
schemaName: "completion_message_stream",
|
|
618
|
+
},
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
logger.debug(
|
|
622
|
+
`[Agent] Streaming completion message for route: ${selectedRoute.title}`
|
|
623
|
+
);
|
|
624
|
+
|
|
625
|
+
// Check for onComplete transition
|
|
626
|
+
const transitionConfig = await selectedRoute.evaluateOnComplete(
|
|
627
|
+
{ extracted: session.extracted },
|
|
628
|
+
effectiveContext
|
|
629
|
+
);
|
|
630
|
+
|
|
631
|
+
if (transitionConfig) {
|
|
632
|
+
// Find target route by ID or title
|
|
633
|
+
const targetRoute = this.routes.find(
|
|
634
|
+
(r) =>
|
|
635
|
+
r.id === transitionConfig.transitionTo ||
|
|
636
|
+
r.title === transitionConfig.transitionTo
|
|
637
|
+
);
|
|
638
|
+
|
|
639
|
+
if (targetRoute) {
|
|
640
|
+
// Set pending transition in session
|
|
641
|
+
session = {
|
|
642
|
+
...session,
|
|
643
|
+
pendingTransition: {
|
|
644
|
+
targetRouteId: targetRoute.id,
|
|
645
|
+
condition: transitionConfig.condition,
|
|
646
|
+
reason: "route_complete",
|
|
647
|
+
},
|
|
648
|
+
};
|
|
649
|
+
logger.debug(
|
|
650
|
+
`[Agent] Route ${selectedRoute.title} completed with pending transition to: ${targetRoute.title}`
|
|
651
|
+
);
|
|
652
|
+
} else {
|
|
653
|
+
logger.warn(
|
|
654
|
+
`[Agent] Route ${selectedRoute.title} completed but target route not found: ${transitionConfig.transitionTo}`
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// Set state to END_STATE marker
|
|
535
660
|
session = enterState(session, END_STATE_ID, "Route completed");
|
|
536
661
|
logger.debug(
|
|
537
662
|
`[Agent] Route ${selectedRoute.title} completed. Entered END_STATE state.`
|
|
538
663
|
);
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
664
|
+
|
|
665
|
+
// Stream completion chunks
|
|
666
|
+
for await (const chunk of stream) {
|
|
667
|
+
// Update current session if we have one
|
|
668
|
+
if (chunk.done && this.currentSession) {
|
|
669
|
+
this.currentSession = session;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
yield {
|
|
673
|
+
delta: chunk.delta,
|
|
674
|
+
accumulated: chunk.accumulated,
|
|
675
|
+
done: chunk.done,
|
|
676
|
+
session,
|
|
677
|
+
toolCalls: undefined,
|
|
678
|
+
isRouteComplete: true,
|
|
679
|
+
};
|
|
680
|
+
}
|
|
547
681
|
} else {
|
|
548
682
|
// Fallback: No routes defined, stream a simple response
|
|
549
683
|
const fallbackPrompt = new PromptComposer<TContext>()
|
|
@@ -689,7 +823,43 @@ export class Agent<TContext = unknown> {
|
|
|
689
823
|
let selectedState: State<TContext> | undefined;
|
|
690
824
|
let isRouteComplete = false;
|
|
691
825
|
|
|
692
|
-
|
|
826
|
+
// Check for pending transition from previous route completion
|
|
827
|
+
if (session.pendingTransition) {
|
|
828
|
+
const targetRoute = this.routes.find(
|
|
829
|
+
(r) => r.id === session.pendingTransition?.targetRouteId
|
|
830
|
+
);
|
|
831
|
+
|
|
832
|
+
if (targetRoute) {
|
|
833
|
+
logger.debug(
|
|
834
|
+
`[Agent] Auto-transitioning from pending transition to route: ${targetRoute.title}`
|
|
835
|
+
);
|
|
836
|
+
// Clear pending transition and enter new route
|
|
837
|
+
session = {
|
|
838
|
+
...session,
|
|
839
|
+
pendingTransition: undefined,
|
|
840
|
+
};
|
|
841
|
+
session = enterRoute(session, targetRoute.id, targetRoute.title);
|
|
842
|
+
|
|
843
|
+
// Merge initial data if available
|
|
844
|
+
if (targetRoute.initialData) {
|
|
845
|
+
session = mergeExtracted(session, targetRoute.initialData);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
selectedRoute = targetRoute;
|
|
849
|
+
} else {
|
|
850
|
+
logger.warn(
|
|
851
|
+
`[Agent] Pending transition target route not found: ${session.pendingTransition.targetRouteId}`
|
|
852
|
+
);
|
|
853
|
+
// Clear invalid transition
|
|
854
|
+
session = {
|
|
855
|
+
...session,
|
|
856
|
+
pendingTransition: undefined,
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// If no pending transition or transition handled, do normal routing
|
|
862
|
+
if (this.routes.length > 0 && !selectedRoute) {
|
|
693
863
|
const orchestration = await this.routingEngine.decideRouteAndState({
|
|
694
864
|
routes: this.routes,
|
|
695
865
|
session,
|
|
@@ -829,9 +999,99 @@ export class Agent<TContext = unknown> {
|
|
|
829
999
|
);
|
|
830
1000
|
}
|
|
831
1001
|
} else if (isRouteComplete && selectedRoute) {
|
|
832
|
-
// Route is complete -
|
|
1002
|
+
// Route is complete - generate completion message then check for onComplete transition
|
|
1003
|
+
const lastUserMessage = getLastMessageFromHistory(history);
|
|
1004
|
+
|
|
1005
|
+
// Get endState spec from route
|
|
1006
|
+
const endStateSpec = selectedRoute.endStateSpec;
|
|
1007
|
+
|
|
1008
|
+
// Create a temporary state for completion message generation using endState configuration
|
|
1009
|
+
const completionState = new State<TContext>(
|
|
1010
|
+
selectedRoute.id,
|
|
1011
|
+
endStateSpec.chatState || "Summarize what was accomplished and confirm completion",
|
|
1012
|
+
endStateSpec.id || END_STATE_ID,
|
|
1013
|
+
endStateSpec.gather,
|
|
1014
|
+
undefined,
|
|
1015
|
+
endStateSpec.requiredData,
|
|
1016
|
+
endStateSpec.chatState || "Summarize what was accomplished and confirm completion based on the conversation history and collected data"
|
|
1017
|
+
);
|
|
1018
|
+
|
|
1019
|
+
// Build response schema for completion
|
|
1020
|
+
const responseSchema = this.responseEngine.responseSchemaForRoute(
|
|
1021
|
+
selectedRoute,
|
|
1022
|
+
completionState
|
|
1023
|
+
);
|
|
1024
|
+
|
|
1025
|
+
// Build completion response prompt
|
|
1026
|
+
const completionPrompt = this.responseEngine.buildResponsePrompt(
|
|
1027
|
+
selectedRoute,
|
|
1028
|
+
completionState,
|
|
1029
|
+
selectedRoute.getRules(),
|
|
1030
|
+
selectedRoute.getProhibitions(),
|
|
1031
|
+
undefined, // No directives for completion
|
|
1032
|
+
history,
|
|
1033
|
+
lastUserMessage,
|
|
1034
|
+
{
|
|
1035
|
+
name: this.options.name,
|
|
1036
|
+
goal: this.options.goal,
|
|
1037
|
+
description: this.options.description,
|
|
1038
|
+
personality: this.options.personality,
|
|
1039
|
+
}
|
|
1040
|
+
);
|
|
1041
|
+
|
|
1042
|
+
// Generate completion message using AI provider
|
|
1043
|
+
const completionResult = await this.options.ai.generateMessage({
|
|
1044
|
+
prompt: completionPrompt,
|
|
1045
|
+
history,
|
|
1046
|
+
context: effectiveContext,
|
|
1047
|
+
signal,
|
|
1048
|
+
parameters: {
|
|
1049
|
+
jsonSchema: responseSchema,
|
|
1050
|
+
schemaName: "completion_message",
|
|
1051
|
+
},
|
|
1052
|
+
});
|
|
1053
|
+
|
|
1054
|
+
message = completionResult.structured?.message || completionResult.message;
|
|
1055
|
+
logger.debug(
|
|
1056
|
+
`[Agent] Generated completion message for route: ${selectedRoute.title}`
|
|
1057
|
+
);
|
|
1058
|
+
|
|
1059
|
+
// Check for onComplete transition
|
|
1060
|
+
const transitionConfig = await selectedRoute.evaluateOnComplete(
|
|
1061
|
+
{ extracted: session.extracted },
|
|
1062
|
+
effectiveContext
|
|
1063
|
+
);
|
|
1064
|
+
|
|
1065
|
+
if (transitionConfig) {
|
|
1066
|
+
// Find target route by ID or title
|
|
1067
|
+
const targetRoute = this.routes.find(
|
|
1068
|
+
(r) =>
|
|
1069
|
+
r.id === transitionConfig.transitionTo ||
|
|
1070
|
+
r.title === transitionConfig.transitionTo
|
|
1071
|
+
);
|
|
1072
|
+
|
|
1073
|
+
if (targetRoute) {
|
|
1074
|
+
// Set pending transition in session
|
|
1075
|
+
session = {
|
|
1076
|
+
...session,
|
|
1077
|
+
pendingTransition: {
|
|
1078
|
+
targetRouteId: targetRoute.id,
|
|
1079
|
+
condition: transitionConfig.condition,
|
|
1080
|
+
reason: "route_complete",
|
|
1081
|
+
},
|
|
1082
|
+
};
|
|
1083
|
+
logger.debug(
|
|
1084
|
+
`[Agent] Route ${selectedRoute.title} completed with pending transition to: ${targetRoute.title}`
|
|
1085
|
+
);
|
|
1086
|
+
} else {
|
|
1087
|
+
logger.warn(
|
|
1088
|
+
`[Agent] Route ${selectedRoute.title} completed but target route not found: ${transitionConfig.transitionTo}`
|
|
1089
|
+
);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
// Set state to END_STATE marker
|
|
833
1094
|
session = enterState(session, END_STATE_ID, "Route completed");
|
|
834
|
-
message = "";
|
|
835
1095
|
logger.debug(
|
|
836
1096
|
`[Agent] Route ${selectedRoute.title} completed. Entered END_STATE state.`
|
|
837
1097
|
);
|
|
@@ -1023,4 +1283,66 @@ export class Agent<TContext = unknown> {
|
|
|
1023
1283
|
}
|
|
1024
1284
|
return (this.currentSession.extracted as Partial<TExtracted>) || {};
|
|
1025
1285
|
}
|
|
1286
|
+
|
|
1287
|
+
/**
|
|
1288
|
+
* Manually transition to a different route
|
|
1289
|
+
* Sets a pending transition that will be executed on the next respond() call
|
|
1290
|
+
*
|
|
1291
|
+
* @param routeIdOrTitle - Route ID or title to transition to
|
|
1292
|
+
* @param session - Session state to update (uses current session if not provided)
|
|
1293
|
+
* @param condition - Optional AI-evaluated condition for the transition
|
|
1294
|
+
* @returns Updated session with pending transition
|
|
1295
|
+
*
|
|
1296
|
+
* @example
|
|
1297
|
+
* // After route completes
|
|
1298
|
+
* if (response.isRouteComplete && response.session) {
|
|
1299
|
+
* const updatedSession = agent.transitionToRoute("feedback-collection", response.session);
|
|
1300
|
+
* // Next respond() call will automatically transition to feedback route
|
|
1301
|
+
* const nextResponse = await agent.respond({ history, session: updatedSession });
|
|
1302
|
+
* }
|
|
1303
|
+
*/
|
|
1304
|
+
transitionToRoute(
|
|
1305
|
+
routeIdOrTitle: string,
|
|
1306
|
+
session?: SessionState,
|
|
1307
|
+
condition?: string
|
|
1308
|
+
): SessionState {
|
|
1309
|
+
const targetSession = session || this.currentSession;
|
|
1310
|
+
|
|
1311
|
+
if (!targetSession) {
|
|
1312
|
+
throw new Error(
|
|
1313
|
+
"No session provided and no current session available. Please provide a session to transition."
|
|
1314
|
+
);
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
// Find target route by ID or title
|
|
1318
|
+
const targetRoute = this.routes.find(
|
|
1319
|
+
(r) => r.id === routeIdOrTitle || r.title === routeIdOrTitle
|
|
1320
|
+
);
|
|
1321
|
+
|
|
1322
|
+
if (!targetRoute) {
|
|
1323
|
+
throw new Error(
|
|
1324
|
+
`Route not found: ${routeIdOrTitle}. Available routes: ${this.routes.map((r) => r.title).join(", ")}`
|
|
1325
|
+
);
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
const updatedSession: SessionState = {
|
|
1329
|
+
...targetSession,
|
|
1330
|
+
pendingTransition: {
|
|
1331
|
+
targetRouteId: targetRoute.id,
|
|
1332
|
+
condition,
|
|
1333
|
+
reason: "manual",
|
|
1334
|
+
},
|
|
1335
|
+
};
|
|
1336
|
+
|
|
1337
|
+
// Update current session if using it
|
|
1338
|
+
if (!session && this.currentSession) {
|
|
1339
|
+
this.currentSession = updatedSession;
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
logger.debug(
|
|
1343
|
+
`[Agent] Set pending manual transition to route: ${targetRoute.title}`
|
|
1344
|
+
);
|
|
1345
|
+
|
|
1346
|
+
return updatedSession;
|
|
1347
|
+
}
|
|
1026
1348
|
}
|
package/src/core/Route.ts
CHANGED
|
@@ -7,6 +7,8 @@ import type {
|
|
|
7
7
|
RouteRef,
|
|
8
8
|
TransitionSpec,
|
|
9
9
|
TransitionResult,
|
|
10
|
+
RouteTransitionConfig,
|
|
11
|
+
RouteCompletionHandler,
|
|
10
12
|
} from "../types/route";
|
|
11
13
|
import type { StructuredSchema } from "../types/schema";
|
|
12
14
|
import type { Guideline } from "../types/agent";
|
|
@@ -26,9 +28,11 @@ export class Route<TContext = unknown, TExtracted = unknown> {
|
|
|
26
28
|
public readonly rules: string[];
|
|
27
29
|
public readonly prohibitions: string[];
|
|
28
30
|
public readonly initialState: State<TContext, TExtracted>;
|
|
31
|
+
public readonly endStateSpec: Omit<TransitionSpec<TContext, TExtracted>, "state" | "condition" | "skipIf">;
|
|
29
32
|
public readonly responseOutputSchema?: StructuredSchema;
|
|
30
33
|
public readonly extractionSchema?: StructuredSchema;
|
|
31
34
|
public readonly initialData?: Partial<TExtracted>;
|
|
35
|
+
public readonly onComplete?: string | RouteTransitionConfig | RouteCompletionHandler<TContext, TExtracted>;
|
|
32
36
|
private routingExtrasSchema?: StructuredSchema;
|
|
33
37
|
private guidelines: Guideline[] = [];
|
|
34
38
|
|
|
@@ -47,12 +51,18 @@ export class Route<TContext = unknown, TExtracted = unknown> {
|
|
|
47
51
|
options.initialState?.id,
|
|
48
52
|
options.initialState?.gather,
|
|
49
53
|
options.initialState?.skipIf,
|
|
50
|
-
options.initialState?.requiredData
|
|
54
|
+
options.initialState?.requiredData,
|
|
55
|
+
options.initialState?.chatState
|
|
51
56
|
);
|
|
57
|
+
// Store endState spec (will be used when route completes)
|
|
58
|
+
this.endStateSpec = options.endState || {
|
|
59
|
+
chatState: "Summarize what was accomplished and confirm completion based on the conversation history and collected data"
|
|
60
|
+
};
|
|
52
61
|
this.routingExtrasSchema = options.routingExtrasSchema;
|
|
53
62
|
this.responseOutputSchema = options.responseOutputSchema;
|
|
54
63
|
this.extractionSchema = options.extractionSchema;
|
|
55
64
|
this.initialData = options.initialData;
|
|
65
|
+
this.onComplete = options.onComplete;
|
|
56
66
|
|
|
57
67
|
// Initialize guidelines from options
|
|
58
68
|
if (options.guidelines) {
|
|
@@ -219,4 +229,46 @@ export class Route<TContext = unknown, TExtracted = unknown> {
|
|
|
219
229
|
|
|
220
230
|
return lines.join("\n");
|
|
221
231
|
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Evaluate the onComplete handler and return transition config
|
|
235
|
+
* @param session - Current session state
|
|
236
|
+
* @param context - Agent context
|
|
237
|
+
* @returns Transition config or undefined if no transition
|
|
238
|
+
*/
|
|
239
|
+
async evaluateOnComplete(
|
|
240
|
+
session: { extracted?: Partial<TExtracted> },
|
|
241
|
+
context?: TContext
|
|
242
|
+
): Promise<RouteTransitionConfig | undefined> {
|
|
243
|
+
if (!this.onComplete) {
|
|
244
|
+
return undefined;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// String form: just route ID/title
|
|
248
|
+
if (typeof this.onComplete === "string") {
|
|
249
|
+
return {
|
|
250
|
+
transitionTo: this.onComplete,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Function form: execute and normalize result
|
|
255
|
+
if (typeof this.onComplete === "function") {
|
|
256
|
+
const result = await this.onComplete(session, context);
|
|
257
|
+
|
|
258
|
+
if (!result) {
|
|
259
|
+
return undefined;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (typeof result === "string") {
|
|
263
|
+
return {
|
|
264
|
+
transitionTo: result,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return result;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Object form: return as-is
|
|
272
|
+
return this.onComplete;
|
|
273
|
+
}
|
|
222
274
|
}
|
|
@@ -213,7 +213,9 @@ export class RoutingEngine<TContext = unknown> {
|
|
|
213
213
|
typeof transition.spec.state === "symbol"
|
|
214
214
|
) {
|
|
215
215
|
// Found END_STATE - route is complete
|
|
216
|
-
return {
|
|
216
|
+
return {
|
|
217
|
+
isRouteComplete: true
|
|
218
|
+
};
|
|
217
219
|
}
|
|
218
220
|
|
|
219
221
|
if (!target) continue;
|
package/src/core/State.ts
CHANGED
|
@@ -88,7 +88,7 @@ export class State<TContext = unknown, TExtracted = unknown> {
|
|
|
88
88
|
) {
|
|
89
89
|
const endTransition = new Transition<TContext, TExtracted>(
|
|
90
90
|
this.getRef(),
|
|
91
|
-
{ state: END_STATE, condition: spec.condition }
|
|
91
|
+
{ state: END_STATE, condition: spec.condition, chatState: spec.chatState }
|
|
92
92
|
);
|
|
93
93
|
this.transitions.push(endTransition);
|
|
94
94
|
|
package/src/index.ts
CHANGED
|
@@ -95,9 +95,11 @@ export type {
|
|
|
95
95
|
RouteOptions,
|
|
96
96
|
TransitionSpec,
|
|
97
97
|
TransitionResult,
|
|
98
|
+
RouteTransitionConfig,
|
|
99
|
+
RouteCompletionHandler,
|
|
98
100
|
} from "./types/route";
|
|
99
101
|
|
|
100
|
-
export type { SessionState } from "./types/session";
|
|
102
|
+
export type { SessionState, PendingTransition } from "./types/session";
|
|
101
103
|
export {
|
|
102
104
|
createSession,
|
|
103
105
|
enterRoute,
|
package/src/types/route.ts
CHANGED
|
@@ -28,6 +28,27 @@ export interface StateRef {
|
|
|
28
28
|
*/
|
|
29
29
|
import type { Guideline } from "./agent";
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Route transition configuration when route completes
|
|
33
|
+
*/
|
|
34
|
+
export interface RouteTransitionConfig {
|
|
35
|
+
/** Target route ID or title to transition to */
|
|
36
|
+
transitionTo: string;
|
|
37
|
+
/** Optional AI-evaluated condition for the transition */
|
|
38
|
+
condition?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Function type for dynamic route completion transitions
|
|
43
|
+
* @param session - Current session state with extracted data
|
|
44
|
+
* @param context - Agent context
|
|
45
|
+
* @returns Route ID/title to transition to, or transition config, or undefined to end
|
|
46
|
+
*/
|
|
47
|
+
export type RouteCompletionHandler<TContext = unknown, TExtracted = unknown> = (
|
|
48
|
+
session: { extracted?: Partial<TExtracted> },
|
|
49
|
+
context?: TContext
|
|
50
|
+
) => string | RouteTransitionConfig | undefined | Promise<string | RouteTransitionConfig | undefined>;
|
|
51
|
+
|
|
31
52
|
/**
|
|
32
53
|
* Options for creating a route
|
|
33
54
|
* @template TExtracted - Type of data extracted throughout the route (inferred from extractionSchema)
|
|
@@ -79,6 +100,42 @@ export interface RouteOptions<TExtracted = unknown> {
|
|
|
79
100
|
TransitionSpec<unknown, TExtracted>,
|
|
80
101
|
"toolState" | "state" | "condition"
|
|
81
102
|
>;
|
|
103
|
+
/**
|
|
104
|
+
* Configure the end state (optional)
|
|
105
|
+
* Defines what happens when the route completes (reaches END_STATE)
|
|
106
|
+
* Can include chatState for completion message, toolState for final actions, etc.
|
|
107
|
+
* Note: state, condition, skipIf properties are ignored for end state
|
|
108
|
+
*/
|
|
109
|
+
endState?: Omit<
|
|
110
|
+
TransitionSpec<unknown, TExtracted>,
|
|
111
|
+
"state" | "condition" | "skipIf"
|
|
112
|
+
>;
|
|
113
|
+
/**
|
|
114
|
+
* Optional transition when route completes (reaches END_STATE)
|
|
115
|
+
* Can be:
|
|
116
|
+
* - String: Route ID or title to transition to
|
|
117
|
+
* - Object: Transition config with optional AI-evaluated condition
|
|
118
|
+
* - Function: Dynamic logic that returns route ID, config, or undefined
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* // Simple string
|
|
122
|
+
* onComplete: "feedback-collection"
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* // With condition
|
|
126
|
+
* onComplete: {
|
|
127
|
+
* transitionTo: "feedback-collection",
|
|
128
|
+
* condition: "if booking succeeded"
|
|
129
|
+
* }
|
|
130
|
+
*
|
|
131
|
+
* @example
|
|
132
|
+
* // Dynamic function
|
|
133
|
+
* onComplete: (session) => {
|
|
134
|
+
* if (session.extracted?.success) return "feedback";
|
|
135
|
+
* return "error-recovery";
|
|
136
|
+
* }
|
|
137
|
+
*/
|
|
138
|
+
onComplete?: string | RouteTransitionConfig | RouteCompletionHandler<unknown, TExtracted>;
|
|
82
139
|
}
|
|
83
140
|
|
|
84
141
|
/**
|
|
@@ -91,7 +148,7 @@ export interface TransitionSpec<TContext = unknown, TExtracted = unknown> {
|
|
|
91
148
|
chatState?: string;
|
|
92
149
|
/** Transition to execute a tool */
|
|
93
150
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
94
|
-
toolState?: ToolRef<TContext, any[], any>;
|
|
151
|
+
toolState?: ToolRef<TContext, any[], any, TExtracted>;
|
|
95
152
|
/** Transition to a specific state or end marker */
|
|
96
153
|
state?: StateRef | symbol;
|
|
97
154
|
/**
|
package/src/types/session.ts
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
* Session state types for tracking conversation progress
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Pending route transition information
|
|
7
|
+
*/
|
|
8
|
+
export interface PendingTransition {
|
|
9
|
+
/** Target route ID to transition to */
|
|
10
|
+
targetRouteId: string;
|
|
11
|
+
/** Optional AI-evaluated condition for the transition */
|
|
12
|
+
condition?: string;
|
|
13
|
+
/** Reason for the transition */
|
|
14
|
+
reason: "route_complete" | "manual";
|
|
15
|
+
}
|
|
16
|
+
|
|
5
17
|
/**
|
|
6
18
|
* Session state tracks the current position in the conversation flow
|
|
7
19
|
* and data extracted during the route progression
|
|
@@ -45,6 +57,12 @@ export interface SessionState<TExtracted = Record<string, unknown>> {
|
|
|
45
57
|
completed: boolean;
|
|
46
58
|
}>;
|
|
47
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Pending route transition after completion
|
|
62
|
+
* Set when a route completes with onComplete handler
|
|
63
|
+
*/
|
|
64
|
+
pendingTransition?: PendingTransition;
|
|
65
|
+
|
|
48
66
|
/** Session metadata */
|
|
49
67
|
metadata?: {
|
|
50
68
|
createdAt?: Date;
|
|
@@ -163,12 +181,12 @@ export function enterState<TExtracted = Record<string, unknown>>(
|
|
|
163
181
|
*/
|
|
164
182
|
export function mergeExtracted<TExtracted = Record<string, unknown>>(
|
|
165
183
|
session: SessionState<TExtracted>,
|
|
166
|
-
extracted: Partial<
|
|
184
|
+
extracted: Partial<unknown>
|
|
167
185
|
): SessionState<TExtracted> {
|
|
168
186
|
const newExtracted = {
|
|
169
187
|
...session.extracted,
|
|
170
188
|
...extracted,
|
|
171
|
-
}
|
|
189
|
+
} as Partial<TExtracted>;
|
|
172
190
|
|
|
173
191
|
// Also update the extractedByRoute map for the current route
|
|
174
192
|
const extractedByRoute = { ...session.extractedByRoute };
|