@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.
Files changed (60) hide show
  1. package/README.md +162 -30
  2. package/dist/cjs/core/Agent.d.ts +18 -0
  3. package/dist/cjs/core/Agent.d.ts.map +1 -1
  4. package/dist/cjs/core/Agent.js +218 -15
  5. package/dist/cjs/core/Agent.js.map +1 -1
  6. package/dist/cjs/core/Route.d.ts +12 -1
  7. package/dist/cjs/core/Route.d.ts.map +1 -1
  8. package/dist/cjs/core/Route.js +38 -1
  9. package/dist/cjs/core/Route.js.map +1 -1
  10. package/dist/cjs/core/RoutingEngine.d.ts.map +1 -1
  11. package/dist/cjs/core/RoutingEngine.js +3 -1
  12. package/dist/cjs/core/RoutingEngine.js.map +1 -1
  13. package/dist/cjs/core/State.js +1 -1
  14. package/dist/cjs/core/State.js.map +1 -1
  15. package/dist/cjs/index.d.ts +2 -2
  16. package/dist/cjs/index.d.ts.map +1 -1
  17. package/dist/cjs/index.js.map +1 -1
  18. package/dist/cjs/types/route.d.ts +52 -1
  19. package/dist/cjs/types/route.d.ts.map +1 -1
  20. package/dist/cjs/types/session.d.ts +17 -1
  21. package/dist/cjs/types/session.d.ts.map +1 -1
  22. package/dist/cjs/types/session.js.map +1 -1
  23. package/dist/core/Agent.d.ts +18 -0
  24. package/dist/core/Agent.d.ts.map +1 -1
  25. package/dist/core/Agent.js +219 -16
  26. package/dist/core/Agent.js.map +1 -1
  27. package/dist/core/Route.d.ts +12 -1
  28. package/dist/core/Route.d.ts.map +1 -1
  29. package/dist/core/Route.js +38 -1
  30. package/dist/core/Route.js.map +1 -1
  31. package/dist/core/RoutingEngine.d.ts.map +1 -1
  32. package/dist/core/RoutingEngine.js +3 -1
  33. package/dist/core/RoutingEngine.js.map +1 -1
  34. package/dist/core/State.js +1 -1
  35. package/dist/core/State.js.map +1 -1
  36. package/dist/index.d.ts +2 -2
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js.map +1 -1
  39. package/dist/types/route.d.ts +52 -1
  40. package/dist/types/route.d.ts.map +1 -1
  41. package/dist/types/session.d.ts +17 -1
  42. package/dist/types/session.d.ts.map +1 -1
  43. package/dist/types/session.js.map +1 -1
  44. package/docs/EXAMPLES.md +51 -2
  45. package/docs/ROUTES.md +345 -1
  46. package/docs/STATES.md +97 -7
  47. package/examples/business-onboarding.ts +10 -12
  48. package/examples/company-qna-agent.ts +4 -5
  49. package/examples/healthcare-agent.ts +63 -0
  50. package/examples/persistent-onboarding.ts +6 -8
  51. package/examples/route-transitions.ts +242 -0
  52. package/examples/travel-agent.ts +60 -0
  53. package/package.json +1 -1
  54. package/src/core/Agent.ts +338 -16
  55. package/src/core/Route.ts +53 -1
  56. package/src/core/RoutingEngine.ts +3 -1
  57. package/src/core/State.ts +1 -1
  58. package/src/index.ts +3 -1
  59. package/src/types/route.ts +58 -1
  60. 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
- const updatedExtracted = (await this.options.hooks.onExtractedUpdate(
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
- if (this.routes.length > 0) {
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 - set state to END_STATE marker and yield completion signal
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
- yield {
540
- delta: "",
541
- accumulated: "",
542
- done: true,
543
- session,
544
- toolCalls: undefined,
545
- isRouteComplete: true,
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
- if (this.routes.length > 0) {
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 - set state to END_STATE marker and return completion signal
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 { isRouteComplete: true };
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,
@@ -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
  /**
@@ -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<TExtracted>
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 };