@falai/agent 0.9.0-alpha-2 → 0.9.0

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 (75) hide show
  1. package/dist/cjs/src/core/Agent.d.ts +31 -41
  2. package/dist/cjs/src/core/Agent.d.ts.map +1 -1
  3. package/dist/cjs/src/core/Agent.js +72 -1075
  4. package/dist/cjs/src/core/Agent.js.map +1 -1
  5. package/dist/cjs/src/core/ResponseModal.d.ts +205 -0
  6. package/dist/cjs/src/core/ResponseModal.d.ts.map +1 -0
  7. package/dist/cjs/src/core/ResponseModal.js +1328 -0
  8. package/dist/cjs/src/core/ResponseModal.js.map +1 -0
  9. package/dist/cjs/src/core/ResponsePipeline.js +1 -1
  10. package/dist/cjs/src/core/ResponsePipeline.js.map +1 -1
  11. package/dist/cjs/src/index.d.ts +3 -1
  12. package/dist/cjs/src/index.d.ts.map +1 -1
  13. package/dist/cjs/src/index.js +7 -1
  14. package/dist/cjs/src/index.js.map +1 -1
  15. package/dist/cjs/src/types/agent.d.ts +1 -0
  16. package/dist/cjs/src/types/agent.d.ts.map +1 -1
  17. package/dist/cjs/src/types/ai.d.ts +1 -1
  18. package/dist/cjs/src/types/ai.d.ts.map +1 -1
  19. package/dist/cjs/src/utils/clone.d.ts.map +1 -1
  20. package/dist/cjs/src/utils/clone.js +0 -4
  21. package/dist/cjs/src/utils/clone.js.map +1 -1
  22. package/dist/cjs/src/utils/history.d.ts +30 -1
  23. package/dist/cjs/src/utils/history.d.ts.map +1 -1
  24. package/dist/cjs/src/utils/history.js +169 -23
  25. package/dist/cjs/src/utils/history.js.map +1 -1
  26. package/dist/cjs/src/utils/index.d.ts +1 -1
  27. package/dist/cjs/src/utils/index.d.ts.map +1 -1
  28. package/dist/cjs/src/utils/index.js +5 -1
  29. package/dist/cjs/src/utils/index.js.map +1 -1
  30. package/dist/src/core/Agent.d.ts +31 -41
  31. package/dist/src/core/Agent.d.ts.map +1 -1
  32. package/dist/src/core/Agent.js +73 -1076
  33. package/dist/src/core/Agent.js.map +1 -1
  34. package/dist/src/core/ResponseModal.d.ts +205 -0
  35. package/dist/src/core/ResponseModal.d.ts.map +1 -0
  36. package/dist/src/core/ResponseModal.js +1323 -0
  37. package/dist/src/core/ResponseModal.js.map +1 -0
  38. package/dist/src/core/ResponsePipeline.js +1 -1
  39. package/dist/src/core/ResponsePipeline.js.map +1 -1
  40. package/dist/src/index.d.ts +3 -1
  41. package/dist/src/index.d.ts.map +1 -1
  42. package/dist/src/index.js +2 -1
  43. package/dist/src/index.js.map +1 -1
  44. package/dist/src/types/agent.d.ts +1 -0
  45. package/dist/src/types/agent.d.ts.map +1 -1
  46. package/dist/src/types/ai.d.ts +1 -1
  47. package/dist/src/types/ai.d.ts.map +1 -1
  48. package/dist/src/utils/clone.d.ts.map +1 -1
  49. package/dist/src/utils/clone.js +0 -4
  50. package/dist/src/utils/clone.js.map +1 -1
  51. package/dist/src/utils/history.d.ts +30 -1
  52. package/dist/src/utils/history.d.ts.map +1 -1
  53. package/dist/src/utils/history.js +165 -23
  54. package/dist/src/utils/history.js.map +1 -1
  55. package/dist/src/utils/index.d.ts +1 -1
  56. package/dist/src/utils/index.d.ts.map +1 -1
  57. package/dist/src/utils/index.js +1 -1
  58. package/dist/src/utils/index.js.map +1 -1
  59. package/docs/README.md +2 -1
  60. package/docs/api/README.md +160 -0
  61. package/docs/api/overview.md +66 -1
  62. package/docs/guides/migration/README.md +72 -0
  63. package/docs/guides/migration/response-modal-refactor.md +518 -0
  64. package/examples/advanced-patterns/streaming-responses.ts +169 -96
  65. package/examples/core-concepts/modern-streaming-api.ts +309 -0
  66. package/package.json +1 -1
  67. package/src/core/Agent.ts +95 -1488
  68. package/src/core/ResponseModal.ts +1722 -0
  69. package/src/core/ResponsePipeline.ts +1 -1
  70. package/src/index.ts +11 -0
  71. package/src/types/agent.ts +1 -0
  72. package/src/types/ai.ts +1 -1
  73. package/src/utils/clone.ts +6 -8
  74. package/src/utils/history.ts +190 -27
  75. package/src/utils/index.ts +4 -0
package/src/core/Agent.ts CHANGED
@@ -10,27 +10,19 @@ import type {
10
10
  Event,
11
11
  RouteOptions,
12
12
  SessionState,
13
- AgentStructuredResponse,
14
13
  Template,
15
- StepRef,
16
- History,
17
14
  AgentResponseStreamChunk,
18
15
  AgentResponse,
19
16
  StructuredSchema,
20
17
  ValidationError,
21
18
  ValidationResult,
22
19
  } from "../types";
23
- import { EventKind, MessageRole } from "../types/history";
20
+ import type { StreamOptions, GenerateOptions, RespondParams } from "./ResponseModal";
24
21
  import {
25
- enterRoute,
26
- enterStep,
27
22
  mergeCollected,
28
23
  logger,
29
24
  LoggerLevel,
30
25
  render,
31
- getLastMessageFromHistory,
32
- normalizeHistory,
33
- cloneDeep,
34
26
  } from "../utils";
35
27
 
36
28
  import { Route } from "./Route";
@@ -38,10 +30,8 @@ import { Step } from "./Step";
38
30
  import { PersistenceManager } from "./PersistenceManager";
39
31
  import { SessionManager } from "./SessionManager";
40
32
  import { RoutingEngine } from "./RoutingEngine";
41
- import { ResponseEngine } from "./ResponseEngine";
42
33
  import { ToolExecutor } from "./ToolExecutor";
43
- import { ResponsePipeline } from "./ResponsePipeline";
44
- import { END_ROUTE_ID } from "../constants";
34
+ import { ResponseModal } from "./ResponseModal";
45
35
 
46
36
  /**
47
37
  * Error thrown when data validation fails
@@ -75,8 +65,7 @@ export class Agent<TContext = any, TData = any> {
75
65
  private context: TContext | undefined;
76
66
  private persistenceManager: PersistenceManager<TData> | undefined;
77
67
  private routingEngine: RoutingEngine<TContext, TData>;
78
- private responseEngine: ResponseEngine<TContext, TData>;
79
- private responsePipeline: ResponsePipeline<TContext, TData>;
68
+ private responseModal: ResponseModal<TContext, TData>;
80
69
  private currentSession?: SessionState<TData>;
81
70
  private knowledgeBase: Record<string, unknown> = {};
82
71
  private schema?: StructuredSchema;
@@ -125,22 +114,15 @@ export class Agent<TContext = any, TData = any> {
125
114
  // Initialize current session if provided
126
115
  this.currentSession = options.session;
127
116
 
128
- // Initialize routing and response engines
117
+ // Initialize routing engine
129
118
  this.routingEngine = new RoutingEngine<TContext, TData>({
130
119
  maxCandidates: 5,
131
120
  allowRouteSwitch: true,
132
121
  switchThreshold: 70,
133
122
  });
134
- this.responseEngine = new ResponseEngine<TContext, TData>();
135
- this.responsePipeline = new ResponsePipeline<TContext, TData>(
136
- options,
137
- this.routes,
138
- this.tools,
139
- this.routingEngine,
140
- this.updateContext.bind(this),
141
- this.updateData.bind(this),
142
- this.updateCollectedData.bind(this)
143
- );
123
+
124
+ // Initialize ResponseModal for handling all response generation
125
+ this.responseModal = new ResponseModal<TContext, TData>(this);
144
126
 
145
127
  // Initialize persistence if configured
146
128
  if (options.persistence) {
@@ -560,1396 +542,122 @@ export class Agent<TContext = any, TData = any> {
560
542
  /**
561
543
  * Generate a response based on history and context as a stream
562
544
  */
563
- async *respondStream(params: {
564
- history: History;
565
- step?: StepRef;
566
- session?: SessionState<TData>;
567
- contextOverride?: Partial<TContext>;
568
- signal?: AbortSignal;
569
- }): AsyncGenerator<AgentResponseStreamChunk<TData>> {
570
- const { history: simpleHistory, signal } = params;
571
- const history = normalizeHistory(simpleHistory);
572
-
573
- // Prepare context and session using the response pipeline
574
- this.responsePipeline.setContext(this.context);
575
- this.responsePipeline.setCurrentSession(this.currentSession);
576
- let session: SessionState;
577
- const responseContext = await this.responsePipeline.prepareResponseContext({
578
- contextOverride: params.contextOverride,
579
- session: params.session ? cloneDeep(params.session) : undefined,
580
- });
581
- const { effectiveContext } = responseContext;
582
- session = responseContext.session;
583
-
584
- // Merge agent's collected data into session (agent data takes precedence)
585
- if (Object.keys(this.collectedData).length > 0) {
586
- session = mergeCollected(session, this.collectedData);
587
- logger.debug("[Agent] Merged agent collected data into session:", this.collectedData);
588
- }
589
-
590
- // Update our stored context if it was modified by beforeRespond hook
591
- this.context = this.responsePipeline.getStoredContext();
592
-
593
- // PHASE 1: PREPARE - Execute prepare function if current step has one
594
- if (session.currentRoute && session.currentStep) {
595
- const currentRoute = this.routes.find(
596
- (r) => r.id === session.currentRoute?.id
597
- );
598
- if (currentRoute) {
599
- const currentStep = currentRoute.getStep(session.currentStep.id);
600
- if (currentStep?.prepare) {
601
- logger.debug(`[Agent] Executing prepare for step: ${currentStep.id}`);
602
- await this.executePrepareFinalize(
603
- currentStep.prepare,
604
- effectiveContext,
605
- session.data,
606
- currentRoute,
607
- currentStep
608
- );
609
- }
610
- }
611
- }
612
-
613
- // PHASE 2: ROUTING + STEP SELECTION - Use response pipeline
614
- const routingResult =
615
- await this.responsePipeline.handleRoutingAndStepSelection({
616
- session,
617
- history,
618
- context: effectiveContext,
619
- signal,
620
- });
621
- const selectedRoute = routingResult.selectedRoute;
622
- const selectedStep = routingResult.selectedStep;
623
- const responseDirectives = routingResult.responseDirectives;
624
- const isRouteComplete = routingResult.isRouteComplete;
625
- session = routingResult.session;
626
-
627
- // PHASE 3: DETERMINE NEXT STEP - Use pipeline method
628
- const stepResult = this.responsePipeline.determineNextStep({
629
- selectedRoute,
630
- selectedStep,
631
- session,
632
- isRouteComplete,
633
- });
634
- const nextStep = stepResult.nextStep;
635
- session = stepResult.session;
636
-
637
- if (selectedRoute && !isRouteComplete) {
638
- // PHASE 4: RESPONSE GENERATION - Stream message using selected route and step
639
- // Get last user message
640
- const lastUserMessage = getLastMessageFromHistory(history);
641
-
642
- // Build response schema for this route (with collect fields from step)
643
- const responseSchema = this.responseEngine.responseSchemaForRoute(
644
- selectedRoute,
645
- nextStep,
646
- this.schema
647
- );
648
-
649
- // Check if selected route and next step are defined
650
- if (!selectedRoute || !nextStep) {
651
- logger.error("[Agent] Selected route or next step is not defined", {
652
- selectedRoute,
653
- nextStep,
654
- });
655
- throw new Error("Selected route or next step is not defined");
656
- }
657
-
658
- // Build response prompt
659
- const responsePrompt = await this.responseEngine.buildResponsePrompt({
660
- route: selectedRoute,
661
- currentStep: nextStep,
662
- rules: selectedRoute.getRules(),
663
- prohibitions: selectedRoute.getProhibitions(),
664
- directives: responseDirectives,
665
- history,
666
- lastMessage: lastUserMessage,
667
- agentOptions: this.options,
668
- // Combine agent and route properties according to the specified logic
669
- combinedGuidelines: [
670
- ...this.getGuidelines(),
671
- ...selectedRoute.getGuidelines(),
672
- ],
673
- combinedTerms: this.mergeTerms(
674
- this.getTerms(),
675
- selectedRoute.getTerms()
676
- ),
677
- context: effectiveContext,
678
- session,
679
- agentSchema: this.schema,
680
- });
681
-
682
- // Collect available tools for AI
683
- const availableTools = this.collectAvailableTools(
684
- selectedRoute,
685
- nextStep
686
- );
687
-
688
- // Generate message stream using AI provider
689
- const stream = this.options.provider.generateMessageStream({
690
- prompt: responsePrompt,
691
- history,
692
- context: effectiveContext,
693
- tools: availableTools,
694
- signal,
695
- parameters: {
696
- jsonSchema: responseSchema,
697
- schemaName: "response_stream_output",
698
- },
699
- });
700
-
701
- // Stream chunks to caller
702
- for await (const chunk of stream) {
703
- let toolCalls:
704
- | Array<{ toolName: string; arguments: Record<string, unknown> }>
705
- | undefined = undefined;
706
-
707
- // Extract tool calls from AI response on final chunk
708
- if (chunk.done && chunk.structured?.toolCalls) {
709
- toolCalls = chunk.structured.toolCalls;
710
-
711
- // Execute dynamic tool calls
712
- if (toolCalls.length > 0) {
713
- logger.debug(
714
- `[Agent] Executing ${toolCalls.length} dynamic tool calls`
715
- );
716
-
717
- for (const toolCall of toolCalls) {
718
- const tool = this.findAvailableTool(
719
- toolCall.toolName,
720
- selectedRoute
721
- );
722
- if (!tool) {
723
- logger.warn(`[Agent] Tool not found: ${toolCall.toolName}`);
724
- continue;
725
- }
726
-
727
- const toolExecutor = new ToolExecutor<TContext, TData>();
728
- const result = await toolExecutor.executeTool({
729
- tool: tool,
730
- context: effectiveContext,
731
- updateContext: this.updateContext.bind(this),
732
- updateData: this.updateCollectedData.bind(this),
733
- history,
734
- data: session.data,
735
- toolArguments: toolCall.arguments,
736
- });
737
-
738
- // Update context with tool results
739
- if (result.contextUpdate) {
740
- await this.updateContext(
741
- result.contextUpdate as Partial<TContext>
742
- );
743
- }
744
-
745
- // Update collected data with tool results
746
- if (result.dataUpdate) {
747
- session = await this.updateData(session, result.dataUpdate as Partial<TData>);
748
- logger.debug(
749
- `[Agent] Tool updated collected data:`,
750
- result.dataUpdate
751
- );
752
- }
753
-
754
- logger.debug(
755
- `[Agent] Executed dynamic tool: ${result.toolName} (success: ${result.success})`
756
- );
757
- }
758
- }
759
- }
760
-
761
- // TOOL LOOP: Allow AI to make follow-up tool calls after initial tool execution (streaming)
762
- const MAX_TOOL_LOOPS = 5;
763
- let toolLoopCount = 0;
764
- let hasToolCalls = toolCalls && toolCalls.length > 0;
765
-
766
- while (hasToolCalls && toolLoopCount < MAX_TOOL_LOOPS) {
767
- toolLoopCount++;
768
- logger.debug(
769
- `[Agent] Starting streaming tool loop ${toolLoopCount}/${MAX_TOOL_LOOPS}`
770
- );
771
-
772
- // Add tool execution results to history so AI knows what happened
773
- const toolResultsEvents: Event[] = [];
774
- for (const toolCall of toolCalls || []) {
775
- const tool = this.findAvailableTool(
776
- toolCall.toolName,
777
- selectedRoute
778
- );
779
- if (tool) {
780
- toolResultsEvents.push({
781
- kind: EventKind.TOOL,
782
- source: MessageRole.AGENT,
783
- timestamp: new Date().toISOString(),
784
- data: {
785
- tool_calls: [
786
- {
787
- tool_id: toolCall.toolName,
788
- arguments: toolCall.arguments,
789
- result: {
790
- data: "Tool executed successfully",
791
- },
792
- },
793
- ],
794
- },
795
- });
796
- }
797
- }
798
-
799
- // Create updated history with tool results
800
- const updatedHistory = [...history, ...toolResultsEvents];
801
-
802
- // Make follow-up streaming AI call to see if more tools are needed
803
- const followUpStream = this.options.provider.generateMessageStream({
804
- prompt: responsePrompt,
805
- history: updatedHistory,
806
- context: effectiveContext,
807
- tools: availableTools,
808
- parameters: {
809
- jsonSchema: responseSchema,
810
- schemaName: "tool_followup",
811
- },
812
- signal,
813
- });
814
-
815
- let followUpToolCalls:
816
- | Array<{ toolName: string; arguments: Record<string, unknown> }>
817
- | undefined;
818
-
819
- for await (const followUpChunk of followUpStream) {
820
- // Extract tool calls from follow-up stream
821
- if (followUpChunk.done && followUpChunk.structured?.toolCalls) {
822
- followUpToolCalls = followUpChunk.structured.toolCalls;
823
- }
824
- }
825
-
826
- hasToolCalls = followUpToolCalls && followUpToolCalls.length > 0;
827
-
828
- if (hasToolCalls) {
829
- logger.debug(
830
- `[Agent] Follow-up streaming call produced ${followUpToolCalls!.length
831
- } additional tool calls`
832
- );
833
-
834
- // Execute the follow-up tool calls
835
- for (const toolCall of followUpToolCalls!) {
836
- const tool = this.findAvailableTool(
837
- toolCall.toolName,
838
- selectedRoute
839
- );
840
- if (!tool) {
841
- logger.warn(
842
- `[Agent] Tool not found in streaming follow-up: ${toolCall.toolName}`
843
- );
844
- continue;
845
- }
846
-
847
- const toolExecutor = new ToolExecutor<TContext, TData>();
848
- const result = await toolExecutor.executeTool({
849
- tool: tool,
850
- context: effectiveContext,
851
- updateContext: this.updateContext.bind(this),
852
- updateData: this.updateCollectedData.bind(this),
853
- history: updatedHistory,
854
- data: session.data,
855
- toolArguments: toolCall.arguments,
856
- });
857
-
858
- // Update context with follow-up tool results
859
- if (result.contextUpdate) {
860
- await this.updateContext(
861
- result.contextUpdate as Partial<TContext>
862
- );
863
- }
864
-
865
- if (result.dataUpdate) {
866
- session = await this.updateData(session, result.dataUpdate as Partial<TData>);
867
- logger.debug(
868
- `[Agent] Streaming follow-up tool updated collected data:`,
869
- result.dataUpdate
870
- );
871
- }
872
-
873
- logger.debug(
874
- `[Agent] Executed streaming follow-up tool: ${result.toolName} (success: ${result.success})`
875
- );
876
- }
877
-
878
- // Update toolCalls for next iteration
879
- toolCalls = followUpToolCalls;
880
- } else {
881
- logger.debug(
882
- `[Agent] Streaming tool loop completed after ${toolLoopCount} iterations`
883
- );
884
- // Update toolCalls for final response
885
- toolCalls = followUpToolCalls || [];
886
- break;
887
- }
888
- }
889
-
890
- if (toolLoopCount >= MAX_TOOL_LOOPS) {
891
- logger.warn(
892
- `[Agent] Streaming tool loop limit reached (${MAX_TOOL_LOOPS}), stopping`
893
- );
894
- }
895
-
896
- // Extract collected data on final chunk
897
- if (chunk.done && chunk.structured && nextStep.collect) {
898
- const collectedData: Record<string, unknown> = {};
899
- // The structured response includes both base fields and collected extraction fields
900
- const structuredData = chunk.structured as AgentStructuredResponse &
901
- Record<string, unknown>;
902
-
903
- for (const field of nextStep.collect) {
904
- const fieldKey = String(field);
905
- if (fieldKey in structuredData) {
906
- collectedData[fieldKey] = structuredData[fieldKey];
907
- }
908
- }
909
-
910
- // Merge collected data into session using agent-level data validation
911
- if (Object.keys(collectedData).length > 0) {
912
- // Update agent-level collected data with validation
913
- await this.updateCollectedData(collectedData as Partial<TData>);
914
-
915
- // Update session with validated data
916
- session = await this.updateData(session, collectedData as Partial<TData>);
917
- logger.debug(`[Agent] Collected data:`, collectedData);
918
- }
919
- }
920
-
921
- // Extract any additional data from structured response on final chunk
922
- if (
923
- chunk.done &&
924
- chunk.structured &&
925
- typeof chunk.structured === "object" &&
926
- "contextUpdate" in chunk.structured
927
- ) {
928
- await this.updateContext(
929
- (chunk.structured as { contextUpdate?: Partial<TContext> })
930
- .contextUpdate as Partial<TContext>
931
- );
932
- }
933
-
934
- // Auto-save session step on final chunk
935
- if (
936
- chunk.done &&
937
- this.persistenceManager &&
938
- session.id &&
939
- this.options.persistence?.autoSave !== false
940
- ) {
941
- await this.persistenceManager.saveSessionState(session.id, session);
942
- logger.debug(
943
- `[Agent] Auto-saved session step to persistence: ${session.id}`
944
- );
945
- }
946
-
947
- // Execute finalize function on final chunk
948
- if (chunk.done && session.currentRoute && session.currentStep) {
949
- const currentRoute = this.routes.find(
950
- (r) => r.id === session.currentRoute?.id
951
- );
952
- if (currentRoute) {
953
- const currentStep = currentRoute.getStep(session.currentStep.id);
954
- if (currentStep?.finalize) {
955
- logger.debug(
956
- `[Agent] Executing finalize for step: ${currentStep.id}`
957
- );
958
- await this.executePrepareFinalize(
959
- currentStep.finalize,
960
- effectiveContext,
961
- session.data,
962
- currentRoute,
963
- currentStep
964
- );
965
- }
966
- }
967
- }
968
-
969
- // Update current session if we have one
970
- if (chunk.done && this.currentSession) {
971
- this.currentSession = session;
972
- }
973
-
974
- yield {
975
- delta: chunk.delta,
976
- accumulated: chunk.accumulated,
977
- done: chunk.done,
978
- session, // Return updated session
979
- toolCalls,
980
- isRouteComplete,
981
- metadata: chunk.metadata,
982
- structured: chunk.structured,
983
- };
984
- }
985
- } else if (isRouteComplete && selectedRoute) {
986
- // Route is complete - generate completion message then check for onComplete transition
987
- const lastUserMessage = getLastMessageFromHistory(history);
988
-
989
- // Get endStep spec from route
990
- const endStepSpec = selectedRoute.endStepSpec;
991
-
992
- // Create a temporary step for completion message generation using endStep configuration
993
- const completionStep = new Step<TContext, TData>(selectedRoute.id, {
994
- description: endStepSpec.description,
995
- id: endStepSpec.id || END_ROUTE_ID,
996
- collect: endStepSpec.collect,
997
- requires: endStepSpec.requires,
998
- prompt:
999
- endStepSpec.prompt ||
1000
- "Summarize what was accomplished and confirm completion based on the conversation history and collected data",
1001
- });
1002
-
1003
- // Build response schema for completion
1004
- const responseSchema = this.responseEngine.responseSchemaForRoute(
1005
- selectedRoute,
1006
- completionStep,
1007
- this.schema
1008
- );
1009
- const templateContext = {
1010
- context: effectiveContext,
1011
- session,
1012
- history,
1013
- };
1014
-
1015
- // Build completion response prompt
1016
- const completionPrompt = await this.responseEngine.buildResponsePrompt({
1017
- route: selectedRoute,
1018
- currentStep: completionStep,
1019
- rules: selectedRoute.getRules(),
1020
- prohibitions: selectedRoute.getProhibitions(),
1021
- directives: undefined, // No directives for completion
1022
- history,
1023
- lastMessage: lastUserMessage,
1024
- agentOptions: this.options,
1025
- // Combine agent and route properties according to the specified logic
1026
- combinedGuidelines: [
1027
- ...this.getGuidelines(),
1028
- ...selectedRoute.getGuidelines(),
1029
- ],
1030
- combinedTerms: this.mergeTerms(
1031
- this.getTerms(),
1032
- selectedRoute.getTerms()
1033
- ),
1034
- context: effectiveContext,
1035
- session,
1036
- agentSchema: this.schema,
1037
- });
1038
-
1039
- // Stream completion message using AI provider
1040
- const stream = this.options.provider.generateMessageStream({
1041
- prompt: completionPrompt,
1042
- history,
1043
- context: effectiveContext,
1044
- signal,
1045
- parameters: {
1046
- jsonSchema: responseSchema,
1047
- schemaName: "completion_message_stream",
1048
- },
1049
- });
1050
-
1051
- logger.debug(
1052
- `[Agent] Streaming completion message for route: ${selectedRoute.title}`
1053
- );
1054
-
1055
- // Check for onComplete transition
1056
- const transitionConfig = await selectedRoute.evaluateOnComplete(
1057
- { data: session.data },
1058
- effectiveContext
1059
- );
1060
-
1061
- if (transitionConfig) {
1062
- // Find target route by ID or title
1063
- const targetRoute = this.routes.find(
1064
- (r) =>
1065
- r.id === transitionConfig.nextStep ||
1066
- r.title === transitionConfig.nextStep
1067
- );
1068
-
1069
- if (targetRoute) {
1070
- const renderedCondition = await render(
1071
- transitionConfig.condition,
1072
- templateContext
1073
- );
1074
- // Set pending transition in session
1075
- session = {
1076
- ...session,
1077
- pendingTransition: {
1078
- targetRouteId: targetRoute.id,
1079
- condition: renderedCondition,
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.nextStep}`
1089
- );
1090
- }
1091
- }
1092
-
1093
- // Set step to END_ROUTE marker
1094
- session = enterStep(session, END_ROUTE_ID, "Route completed");
1095
- logger.debug(
1096
- `[Agent] Route ${selectedRoute.title} completed. Entered END_ROUTE step.`
1097
- );
1098
-
1099
- // Stream completion chunks
1100
- for await (const chunk of stream) {
1101
- // Update current session if we have one
1102
- if (chunk.done && this.currentSession) {
1103
- this.currentSession = session;
1104
- }
1105
-
1106
- yield {
1107
- delta: chunk.delta,
1108
- accumulated: chunk.accumulated,
1109
- done: chunk.done,
1110
- session,
1111
- toolCalls: undefined,
1112
- isRouteComplete: true,
1113
- metadata: chunk.metadata,
1114
- structured: chunk.structured,
1115
- };
1116
- }
1117
- } else {
1118
- // Fallback: No routes defined, stream a simple response
1119
- const fallbackPrompt = await this.responseEngine.buildFallbackPrompt({
1120
- history,
1121
- agentOptions: this.options,
1122
- terms: this.terms,
1123
- guidelines: this.guidelines,
1124
- context: effectiveContext,
1125
- session,
1126
- });
1127
-
1128
- const stream = this.options.provider.generateMessageStream({
1129
- prompt: fallbackPrompt,
1130
- history,
1131
- context: effectiveContext,
1132
- signal,
1133
- parameters: {
1134
- jsonSchema: {
1135
- type: "object",
1136
- properties: {
1137
- message: { type: "string" },
1138
- },
1139
- required: ["message"],
1140
- additionalProperties: false,
1141
- },
1142
- schemaName: "fallback_stream_response",
1143
- },
1144
- });
1145
-
1146
- for await (const chunk of stream) {
1147
- // Update current session if we have one
1148
- if (chunk.done && this.currentSession) {
1149
- this.currentSession = session;
1150
- }
1151
-
1152
- yield {
1153
- delta: chunk.delta,
1154
- accumulated: chunk.accumulated,
1155
- done: chunk.done,
1156
- session, // Return updated session
1157
- toolCalls: undefined,
1158
- isRouteComplete: false,
1159
- metadata: chunk.metadata,
1160
- structured: chunk.structured,
1161
- };
1162
- }
1163
- }
545
+ async *respondStream(params: RespondParams<TContext, TData>): AsyncGenerator<AgentResponseStreamChunk<TData>> {
546
+ // Delegate to ResponseModal
547
+ yield* this.responseModal.respondStream(params);
1164
548
  }
1165
549
 
1166
550
  /**
1167
551
  * Generate a response based on history and context
1168
552
  */
1169
- async respond(params: {
1170
- history: History;
1171
- step?: StepRef;
1172
- session?: SessionState<TData>;
1173
- contextOverride?: Partial<TContext>;
1174
- signal?: AbortSignal;
1175
- }): Promise<AgentResponse<TData>> {
1176
- const { history: simpleHistory, contextOverride, signal } = params;
1177
- const history = normalizeHistory(simpleHistory);
1178
-
1179
- // Get current context (may fetch from provider)
1180
- let currentContext = await this.getContext();
1181
-
1182
- // Call beforeRespond hook if configured
1183
- if (this.options.hooks?.beforeRespond && currentContext !== undefined) {
1184
- currentContext = await this.options.hooks.beforeRespond(currentContext);
1185
- // Update stored context with the result from beforeRespond
1186
- this.context = currentContext;
1187
- }
1188
-
1189
- // Merge context with override
1190
- const effectiveContext = {
1191
- ...(currentContext as Record<string, unknown>),
1192
- ...(contextOverride as Record<string, unknown>),
1193
- } as TContext;
1194
-
1195
- // Initialize or get session (use current session if available)
1196
- let session =
1197
- cloneDeep(params.session) ||
1198
- cloneDeep(this.currentSession) ||
1199
- (await this.session.getOrCreate());
1200
-
1201
- // Merge agent's collected data into session (agent data takes precedence)
1202
- if (Object.keys(this.collectedData).length > 0) {
1203
- session = mergeCollected(session, this.collectedData);
1204
- logger.debug("[Agent] Merged agent collected data into session:", this.collectedData);
1205
- }
1206
-
1207
- // PHASE 1: PREPARE - Execute prepare function if current step has one
1208
- if (session.currentRoute && session.currentStep) {
1209
- const currentRoute = this.routes.find(
1210
- (r) => r.id === session.currentRoute?.id
1211
- );
1212
- if (currentRoute) {
1213
- const currentStep = currentRoute.getStep(session.currentStep.id);
1214
- if (currentStep?.prepare) {
1215
- logger.debug(`[Agent] Executing prepare for step: ${currentStep.id}`);
1216
- await this.executePrepareFinalize(
1217
- currentStep.prepare,
1218
- effectiveContext,
1219
- session.data,
1220
- currentRoute,
1221
- currentStep
1222
- );
1223
- }
1224
- }
1225
- }
1226
-
1227
- // PHASE 2: ROUTING + STEP SELECTION - Determine which route and step to use (combined)
1228
- let selectedRoute: Route<TContext, TData> | undefined;
1229
- let responseDirectives: string[] | undefined;
1230
- let selectedStep: Step<TContext, TData> | undefined;
1231
- let isRouteComplete = false;
1232
-
1233
- // Check for pending transition from previous route completion
1234
- if (session.pendingTransition) {
1235
- const targetRoute = this.routes.find(
1236
- (r) => r.id === session.pendingTransition?.targetRouteId
1237
- );
1238
-
1239
- if (targetRoute) {
1240
- logger.debug(
1241
- `[Agent] Auto-transitioning from pending transition to route: ${targetRoute.title}`
1242
- );
1243
- // Clear pending transition and enter new route
1244
- session = {
1245
- ...session,
1246
- pendingTransition: undefined,
1247
- };
1248
- session = enterRoute(session, targetRoute.id, targetRoute.title);
1249
-
1250
- // Merge initial data if available
1251
- if (targetRoute.initialData) {
1252
- session = mergeCollected(session, targetRoute.initialData);
1253
- }
1254
-
1255
- selectedRoute = targetRoute;
1256
- } else {
1257
- logger.warn(
1258
- `[Agent] Pending transition target route not found: ${session.pendingTransition.targetRouteId}`
1259
- );
1260
- // Clear invalid transition
1261
- session = {
1262
- ...session,
1263
- pendingTransition: undefined,
1264
- };
1265
- }
1266
- }
1267
-
1268
- // If no pending transition or transition handled, do normal routing
1269
- if (this.routes.length > 0 && !selectedRoute) {
1270
- const orchestration = await this.routingEngine.decideRouteAndStep({
1271
- routes: this.routes,
1272
- session,
1273
- history,
1274
- agentOptions: this.options,
1275
- provider: this.options.provider,
1276
- context: effectiveContext,
1277
- signal,
1278
- });
1279
-
1280
- selectedRoute = orchestration.selectedRoute;
1281
- selectedStep = orchestration.selectedStep;
1282
- responseDirectives = orchestration.responseDirectives;
1283
- session = orchestration.session;
1284
- isRouteComplete = orchestration.isRouteComplete || false;
1285
-
1286
- // Log if route is complete
1287
- if (isRouteComplete) {
1288
- logger.debug(
1289
- `[Agent] Route complete: all required data collected, END_ROUTE reached`
1290
- );
1291
- }
1292
- }
1293
-
1294
- // PHASE 3: DETERMINE NEXT STEP - Use step from combined decision or get initial step
1295
- let message: string;
1296
- let toolCalls:
1297
- | Array<{ toolName: string; arguments: Record<string, unknown> }>
1298
- | undefined = undefined;
1299
- let responsePrompt: string;
1300
- let availableTools: Array<{
1301
- id: string;
1302
- name: string;
1303
- description?: string;
1304
- parameters?: unknown;
1305
- }> = [];
1306
- let responseSchema: StructuredSchema | undefined;
1307
- let nextStep: Step<TContext, TData> | undefined;
1308
-
1309
- // Get last user message (needed for both route and completion handling)
1310
- const lastUserMessage = getLastMessageFromHistory(history);
1311
-
1312
- if (selectedRoute && !isRouteComplete) {
1313
- // If we have a selected step from the combined routing decision, use it
1314
- if (selectedStep) {
1315
- nextStep = selectedStep;
1316
- } else {
1317
- // New route or no step selected - get initial step or first valid step
1318
- const candidates = this.routingEngine.getCandidateSteps(
1319
- selectedRoute,
1320
- undefined,
1321
- session.data || {}
1322
- );
1323
- if (candidates.length > 0) {
1324
- nextStep = candidates[0].step;
1325
- logger.debug(
1326
- `[Agent] Using first valid step: ${nextStep.id} for new route`
1327
- );
1328
- } else {
1329
- // Fallback to initial step even if it should be skipped
1330
- nextStep = selectedRoute.initialStep;
1331
- logger.warn(
1332
- `[Agent] No valid steps found, using initial step: ${nextStep.id}`
1333
- );
1334
- }
1335
- }
1336
-
1337
- // Update session with next step
1338
- session = enterStep(session, nextStep.id, nextStep.description);
1339
- logger.debug(`[Agent] Entered step: ${nextStep.id}`);
1340
-
1341
- // PHASE 4: RESPONSE GENERATION - Generate message using selected route and step
1342
- // Get last user message
1343
- const lastUserMessage = getLastMessageFromHistory(history);
1344
-
1345
- // Build response schema for this route (with collect fields from step)
1346
- responseSchema = this.responseEngine.responseSchemaForRoute(
1347
- selectedRoute,
1348
- nextStep,
1349
- this.schema
1350
- );
1351
-
1352
- // Build response prompt
1353
- responsePrompt = await this.responseEngine.buildResponsePrompt({
1354
- route: selectedRoute,
1355
- currentStep: nextStep,
1356
- rules: selectedRoute.getRules(),
1357
- prohibitions: selectedRoute.getProhibitions(),
1358
- directives: responseDirectives,
1359
- history,
1360
- lastMessage: lastUserMessage,
1361
- agentOptions: this.options,
1362
- // Combine agent and route properties according to the specified logic
1363
- combinedGuidelines: [
1364
- ...this.getGuidelines(),
1365
- ...selectedRoute.getGuidelines(),
1366
- ],
1367
- combinedTerms: this.mergeTerms(
1368
- this.getTerms(),
1369
- selectedRoute.getTerms()
1370
- ),
1371
- context: effectiveContext,
1372
- session,
1373
- agentSchema: this.schema,
1374
- });
1375
-
1376
- // Collect available tools for AI
1377
- availableTools = this.collectAvailableTools(
1378
- selectedRoute,
1379
- nextStep
1380
- );
1381
- } else {
1382
- // No route selected - generate basic response without route context
1383
- logger.debug(`[Agent] No route selected, generating basic response`);
1384
-
1385
- // Build basic response prompt without route context
1386
- responsePrompt = await this.responseEngine.buildFallbackPrompt({
1387
- history,
1388
- agentOptions: this.options,
1389
- terms: this.getTerms(),
1390
- guidelines: this.getGuidelines(),
1391
- context: effectiveContext,
1392
- session,
1393
- });
1394
-
1395
- // Use agent-level tools only
1396
- availableTools = this.collectAvailableTools();
1397
- responseSchema = undefined;
1398
- }
1399
-
1400
- // Generate message using AI provider (common for both route and no-route cases)
1401
- const result = await this.options.provider.generateMessage({
1402
- prompt: responsePrompt,
1403
- history,
1404
- context: effectiveContext,
1405
- tools: availableTools,
1406
- signal,
1407
- parameters: responseSchema
1408
- ? {
1409
- jsonSchema: responseSchema,
1410
- schemaName: "response_output",
1411
- }
1412
- : undefined,
1413
- });
1414
-
1415
- message = result.structured?.message || result.message;
1416
-
1417
- // Process dynamic tool calls from AI response (common for both route and no-route cases)
1418
- if (result.structured?.toolCalls) {
1419
- toolCalls = result.structured.toolCalls;
1420
-
1421
- // Execute dynamic tool calls
1422
- if (toolCalls.length > 0) {
1423
- logger.debug(
1424
- `[Agent] Executing ${toolCalls.length} dynamic tool calls`
1425
- );
1426
-
1427
- for (const toolCall of toolCalls) {
1428
- const tool = this.findAvailableTool(toolCall.toolName, selectedRoute);
1429
- if (!tool) {
1430
- logger.warn(`[Agent] Tool not found: ${toolCall.toolName}`);
1431
- continue;
1432
- }
1433
-
1434
- const toolExecutor = new ToolExecutor<TContext, TData>();
1435
- const toolResult = await toolExecutor.executeTool({
1436
- tool: tool,
1437
- context: effectiveContext,
1438
- updateContext: this.updateContext.bind(this),
1439
- updateData: this.updateCollectedData.bind(this),
1440
- history,
1441
- data: session.data,
1442
- toolArguments: toolCall.arguments,
1443
- });
1444
-
1445
- // Update context with tool results
1446
- if (toolResult.contextUpdate) {
1447
- await this.updateContext(
1448
- toolResult.contextUpdate as Partial<TContext>
1449
- );
1450
- }
1451
-
1452
- // Update collected data with tool results
1453
- if (toolResult.dataUpdate) {
1454
- session = await this.updateData(session, toolResult.dataUpdate as Partial<TData>);
1455
- logger.debug(
1456
- `[Agent] Tool updated collected data:`,
1457
- toolResult.dataUpdate
1458
- );
1459
- }
1460
-
1461
- logger.debug(
1462
- `[Agent] Executed dynamic tool: ${toolResult.toolName} (success: ${toolResult.success})`
1463
- );
1464
- }
1465
- }
1466
- }
1467
-
1468
- // TOOL LOOP: Allow AI to make follow-up tool calls after initial tool execution
1469
- const MAX_TOOL_LOOPS = 5;
1470
- let toolLoopCount = 0;
1471
- let hasToolCalls = toolCalls && toolCalls.length > 0;
1472
-
1473
- while (hasToolCalls && toolLoopCount < MAX_TOOL_LOOPS) {
1474
- toolLoopCount++;
1475
- logger.debug(
1476
- `[Agent] Starting tool loop ${toolLoopCount}/${MAX_TOOL_LOOPS}`
1477
- );
1478
-
1479
- // Add tool execution results to history so AI knows what happened
1480
- const toolResultsEvents: Event[] = [];
1481
- for (const toolCall of toolCalls || []) {
1482
- const tool = this.findAvailableTool(toolCall.toolName, selectedRoute);
1483
- if (tool) {
1484
- toolResultsEvents.push({
1485
- kind: EventKind.TOOL,
1486
- source: MessageRole.AGENT,
1487
- timestamp: new Date().toISOString(),
1488
- data: {
1489
- tool_calls: [
1490
- {
1491
- tool_id: toolCall.toolName,
1492
- arguments: toolCall.arguments,
1493
- result: {
1494
- data: "Tool executed successfully",
1495
- },
1496
- },
1497
- ],
1498
- },
1499
- });
1500
- }
1501
- }
1502
-
1503
- // Create updated history with tool results
1504
- const updatedHistory = [...history, ...toolResultsEvents];
1505
-
1506
- // Make follow-up AI call to see if more tools are needed
1507
- const followUpResult = await this.options.provider.generateMessage({
1508
- prompt: responsePrompt,
1509
- history: updatedHistory,
1510
- context: effectiveContext,
1511
- tools: availableTools,
1512
- parameters: {
1513
- jsonSchema: responseSchema as StructuredSchema,
1514
- schemaName: "tool_followup",
1515
- },
1516
- signal,
1517
- });
1518
-
1519
- // Check if follow-up call has more tool calls
1520
- const followUpToolCalls = followUpResult.structured?.toolCalls;
1521
- hasToolCalls = followUpToolCalls && followUpToolCalls.length > 0;
1522
-
1523
- if (hasToolCalls) {
1524
- logger.debug(
1525
- `[Agent] Follow-up call produced ${followUpToolCalls!.length
1526
- } additional tool calls`
1527
- );
1528
-
1529
- // Execute the follow-up tool calls
1530
- for (const toolCall of followUpToolCalls!) {
1531
- const tool = this.findAvailableTool(toolCall.toolName, selectedRoute);
1532
- if (!tool) {
1533
- logger.warn(
1534
- `[Agent] Tool not found in follow-up: ${toolCall.toolName}`
1535
- );
1536
- continue;
1537
- }
1538
-
1539
- const toolExecutor = new ToolExecutor<TContext, TData>();
1540
- const toolResult = await toolExecutor.executeTool({
1541
- tool: tool,
1542
- context: effectiveContext,
1543
- updateContext: this.updateContext.bind(this),
1544
- updateData: this.updateCollectedData.bind(this),
1545
- history: updatedHistory,
1546
- data: session.data,
1547
- toolArguments: toolCall.arguments,
1548
- });
1549
-
1550
- // Update context with follow-up tool results
1551
- if (toolResult.contextUpdate) {
1552
- await this.updateContext(
1553
- toolResult.contextUpdate as Partial<TContext>
1554
- );
1555
- }
1556
-
1557
- if (toolResult.dataUpdate) {
1558
- session = await this.updateData(session, toolResult.dataUpdate as Partial<TData>);
1559
- logger.debug(
1560
- `[Agent] Follow-up tool updated collected data:`,
1561
- toolResult.dataUpdate
1562
- );
1563
- }
1564
-
1565
- logger.debug(
1566
- `[Agent] Executed follow-up tool: ${toolResult.toolName} (success: ${toolResult.success})`
1567
- );
1568
- }
1569
-
1570
- // Update toolCalls for next iteration or final response
1571
- toolCalls = followUpToolCalls;
1572
- } else {
1573
- logger.debug(
1574
- `[Agent] Tool loop completed after ${toolLoopCount} iterations`
1575
- );
1576
- // Update final message and toolCalls from follow-up result if no more tools
1577
- message = followUpResult.structured?.message || followUpResult.message;
1578
- toolCalls = followUpToolCalls || [];
1579
- break;
1580
- }
1581
- }
1582
-
1583
- if (toolLoopCount >= MAX_TOOL_LOOPS) {
1584
- logger.warn(
1585
- `[Agent] Tool loop limit reached (${MAX_TOOL_LOOPS}), stopping`
1586
- );
1587
- }
1588
-
1589
- // Extract collected data from final response (only for route-based interactions)
1590
- if (selectedRoute && result.structured && nextStep?.collect) {
1591
- const collectedData: Record<string, unknown> = {};
1592
- // The structured response includes both base fields and collected extraction fields
1593
- const structuredData = result.structured as AgentStructuredResponse &
1594
- Record<string, unknown>;
1595
-
1596
- for (const field of nextStep.collect) {
1597
- const fieldKey = String(field);
1598
- if (fieldKey in structuredData) {
1599
- collectedData[fieldKey] = structuredData[fieldKey];
1600
- }
1601
- }
1602
-
1603
- // Merge collected data into session using agent-level data validation
1604
- if (Object.keys(collectedData).length > 0) {
1605
- // Update agent-level collected data with validation
1606
- await this.updateCollectedData(collectedData as Partial<TData>);
1607
-
1608
- // Update session with validated data
1609
- session = await this.updateData(session, collectedData as Partial<TData>);
1610
- logger.debug(`[Agent] Collected data:`, collectedData);
1611
- }
1612
- }
1613
-
1614
- // Extract any additional data from structured response
1615
- if (
1616
- result.structured &&
1617
- typeof result.structured === "object" &&
1618
- "contextUpdate" in result.structured
1619
- ) {
1620
- await this.updateContext(
1621
- (result.structured as { contextUpdate?: Partial<TContext> })
1622
- .contextUpdate as Partial<TContext>
1623
- );
1624
- }
553
+ async respond(params: RespondParams<TContext, TData>): Promise<AgentResponse<TData>> {
554
+ // Delegate to ResponseModal
555
+ return this.responseModal.respond(params);
556
+ }
1625
557
 
1626
- // Handle route completion if route is complete
1627
- if (isRouteComplete) {
1628
- // Route is complete - generate completion message then check for onComplete transition
1629
-
1630
- // Get endStep spec from route
1631
- const endStepSpec = selectedRoute!.endStepSpec;
1632
-
1633
- // Create a temporary step for completion message generation using endStep configuration
1634
- const completionStep = new Step<TContext, TData>(selectedRoute!.id, {
1635
- description: endStepSpec.description,
1636
- id: endStepSpec.id || END_ROUTE_ID,
1637
- collect: endStepSpec.collect,
1638
- requires: endStepSpec.requires,
1639
- prompt:
1640
- endStepSpec.prompt ||
1641
- "Summarize what was accomplished and confirm completion based on the conversation history and collected data",
1642
- });
558
+ /**
559
+ * Get all routes
560
+ */
561
+ getRoutes(): Route<TContext, TData>[] {
562
+ return [...this.routes];
563
+ }
1643
564
 
1644
- if (!selectedRoute) {
1645
- throw new Error("Selected route is not defined");
1646
- }
565
+ /**
566
+ * Get agent options
567
+ * @internal Used by ResponseModal
568
+ */
569
+ getAgentOptions(): AgentOptions<TContext, TData> {
570
+ return this.options;
571
+ }
1647
572
 
1648
- // Build response schema for completion
1649
- const responseSchema = this.responseEngine.responseSchemaForRoute(
1650
- selectedRoute,
1651
- completionStep,
1652
- this.schema
1653
- );
1654
- const templateContext = {
1655
- context: effectiveContext,
1656
- session,
1657
- history,
1658
- };
1659
-
1660
- // Build completion response prompt
1661
- const completionPrompt = await this.responseEngine.buildResponsePrompt({
1662
- route: selectedRoute,
1663
- currentStep: completionStep,
1664
- rules: selectedRoute.getRules(),
1665
- prohibitions: selectedRoute.getProhibitions(),
1666
- directives: undefined, // No directives for completion
1667
- history,
1668
- lastMessage: lastUserMessage,
1669
- agentOptions: this.options,
1670
- // Combine agent and route properties according to the specified logic
1671
- combinedGuidelines: [
1672
- ...this.getGuidelines(),
1673
- ...selectedRoute.getGuidelines(),
1674
- ],
1675
- combinedTerms: this.mergeTerms(
1676
- this.getTerms(),
1677
- selectedRoute.getTerms()
1678
- ),
1679
- context: effectiveContext,
1680
- session,
1681
- agentSchema: this.schema,
1682
- });
573
+ /**
574
+ * Get routing engine
575
+ * @internal Used by ResponseModal
576
+ */
577
+ getRoutingEngine(): RoutingEngine<TContext, TData> {
578
+ return this.routingEngine;
579
+ }
1683
580
 
1684
- // Generate completion message using AI provider
1685
- const completionResult = await this.options.provider.generateMessage({
1686
- prompt: completionPrompt,
1687
- history,
1688
- context: effectiveContext,
1689
- signal,
1690
- parameters: {
1691
- jsonSchema: responseSchema,
1692
- schemaName: "completion_message",
1693
- },
1694
- });
581
+ /**
582
+ * Get the updateData method bound to this agent
583
+ * @internal Used by ResponseModal
584
+ */
585
+ getUpdateDataMethod(): (session: SessionState<TData>, dataUpdate: Partial<TData>) => Promise<SessionState<TData>> {
586
+ return this.updateData.bind(this);
587
+ }
1695
588
 
1696
- message =
1697
- completionResult.structured?.message || completionResult.message;
1698
- logger.debug(
1699
- `[Agent] Generated completion message for route: ${selectedRoute.title}`
1700
- );
1701
589
 
1702
- // Check for onComplete transition
1703
- const transitionConfig = await selectedRoute.evaluateOnComplete(
1704
- { data: session.data },
1705
- effectiveContext
1706
- );
1707
590
 
1708
- if (transitionConfig) {
1709
- // Find target route by ID or title
1710
- const targetRoute = this.routes.find(
1711
- (r) =>
1712
- r.id === transitionConfig.nextStep ||
1713
- r.title === transitionConfig.nextStep
1714
- );
591
+ /**
592
+ * Get all terms
593
+ */
594
+ getTerms(): Term<TContext, TData>[] {
595
+ return [...this.terms];
596
+ }
1715
597
 
1716
- if (targetRoute) {
1717
- const renderedCondition = await render(
1718
- transitionConfig.condition,
1719
- templateContext
1720
- );
1721
- // Set pending transition in session
1722
- session = {
1723
- ...session,
1724
- pendingTransition: {
1725
- targetRouteId: targetRoute.id,
1726
- condition: renderedCondition,
1727
- reason: "route_complete",
1728
- },
1729
- };
1730
- logger.debug(
1731
- `[Agent] Route ${selectedRoute.title} completed with pending transition to: ${targetRoute.title}`
1732
- );
1733
- } else {
1734
- logger.warn(
1735
- `[Agent] Route ${selectedRoute.title} completed but target route not found: ${transitionConfig.nextStep}`
1736
- );
1737
- }
1738
- }
598
+ /**
599
+ * Get all tools
600
+ */
601
+ getTools(): Tool<TContext, TData, unknown[], unknown>[] {
602
+ return [...this.tools];
603
+ }
1739
604
 
1740
- // Set step to END_ROUTE marker
1741
- session = enterStep(session, END_ROUTE_ID, "Route completed");
1742
- logger.debug(
1743
- `[Agent] Route ${selectedRoute.title} completed. Entered END_ROUTE step.`
1744
- );
1745
- } else {
1746
- // Fallback: No routes defined, generate a simple response
1747
- const fallbackPrompt = await this.responseEngine.buildFallbackPrompt({
1748
- history,
1749
- agentOptions: this.options,
1750
- terms: this.terms,
1751
- guidelines: this.guidelines,
1752
- context: effectiveContext,
1753
- session,
1754
- });
1755
605
 
1756
- const result = await this.options.provider.generateMessage({
1757
- prompt: fallbackPrompt,
1758
- history,
1759
- context: effectiveContext,
1760
- signal,
1761
- parameters: {
1762
- jsonSchema: {
1763
- type: "object",
1764
- properties: {
1765
- message: { type: "string" },
1766
- },
1767
- required: ["message"],
1768
- additionalProperties: false,
1769
- },
1770
- schemaName: "fallback_response",
1771
- },
1772
- });
1773
606
 
1774
- message = result.structured?.message || result.message;
1775
- }
1776
607
 
1777
- // Auto-save session step to persistence if configured
1778
- if (
1779
- this.persistenceManager &&
1780
- session.id &&
1781
- this.options.persistence?.autoSave !== false
1782
- ) {
1783
- await this.persistenceManager.saveSessionState(session.id, session);
1784
- logger.debug(
1785
- `[Agent] Auto-saved session step to persistence: ${session.id}`
1786
- );
1787
- }
1788
608
 
1789
- // Execute finalize function
1790
- if (session.currentRoute && session.currentStep) {
1791
- const currentRoute = this.routes.find(
1792
- (r) => r.id === session.currentRoute?.id
1793
- );
1794
- if (currentRoute) {
1795
- const currentStep = currentRoute.getStep(session.currentStep.id);
1796
- if (currentStep?.finalize) {
1797
- logger.debug(
1798
- `[Agent] Executing finalize for step: ${currentStep.id}`
1799
- );
1800
- await this.executePrepareFinalize(
1801
- currentStep.finalize,
1802
- effectiveContext,
1803
- session.data,
1804
- currentRoute,
1805
- currentStep
1806
- );
1807
- }
1808
- }
1809
- }
1810
609
 
1811
- // Update current session if we have one
1812
- if (this.currentSession) {
1813
- this.currentSession = session;
1814
- }
1815
610
 
1816
- return {
1817
- message,
1818
- session, // Return updated session with route/step info
1819
- toolCalls,
1820
- isRouteComplete, // Indicates if the route has reached END_ROUTE with all data collected
1821
- };
611
+ /**
612
+ * Get all guidelines
613
+ */
614
+ getGuidelines(): Guideline<TContext, TData>[] {
615
+ return [...this.guidelines];
1822
616
  }
1823
617
 
1824
618
  /**
1825
- * Get all routes
619
+ * Get the agent's knowledge base
1826
620
  */
1827
- getRoutes(): Route<TContext, TData>[] {
1828
- return [...this.routes];
621
+ getKnowledgeBase(): Record<string, unknown> {
622
+ return { ...this.knowledgeBase };
1829
623
  }
1830
624
 
625
+
626
+
1831
627
  /**
1832
- * Get all terms
628
+ * Get the persistence manager (if configured)
1833
629
  */
1834
- getTerms(): Term<TContext, TData>[] {
1835
- return [...this.terms];
630
+ getPersistenceManager(): PersistenceManager<TData> | undefined {
631
+ return this.persistenceManager;
1836
632
  }
1837
633
 
1838
634
  /**
1839
- * Get all tools
635
+ * Check if persistence is enabled
1840
636
  */
1841
- getTools(): Tool<TContext, TData, unknown[], unknown>[] {
1842
- return [...this.tools];
637
+ hasPersistence(): boolean {
638
+ return this.persistenceManager !== undefined;
1843
639
  }
1844
640
 
1845
641
  /**
1846
- * Find an available tool by name for the given route
1847
- * Route-level tools take precedence over agent-level tools
1848
- * @private
642
+ * Set the current session for convenience methods
643
+ * @param session - Session step to use for subsequent calls
1849
644
  */
1850
- private findAvailableTool(
1851
- toolName: string,
1852
- route?: Route<TContext, TData>
1853
- ): Tool<TContext, TData, unknown[], unknown> | undefined {
1854
- // Check route-level tools first (if route provided)
1855
- if (route) {
1856
- const routeTool = route
1857
- .getTools()
1858
- .find((tool) => tool.id === toolName || tool.name === toolName);
1859
- if (routeTool) return routeTool;
1860
- }
1861
-
1862
- // Fall back to agent-level tools
1863
- return this.tools.find(
1864
- (tool) => tool.id === toolName || tool.name === toolName
1865
- );
645
+ setCurrentSession(session: SessionState): void {
646
+ this.currentSession = session;
1866
647
  }
1867
648
 
1868
649
  /**
1869
- * Collect all available tools for the given route and step context
1870
- * @private
650
+ * Get the current session (if set)
1871
651
  */
1872
- private collectAvailableTools(
1873
- route?: Route<TContext, TData>,
1874
- step?: Step<TContext, TData>
1875
- ): Array<{
1876
- id: string;
1877
- name: string;
1878
- description?: string;
1879
- parameters?: unknown;
1880
- }> {
1881
- const availableTools = new Map<
1882
- string,
1883
- Tool<TContext, TData, unknown[], unknown>
1884
- >();
1885
-
1886
- // Add agent-level tools
1887
- this.tools.forEach((tool) => {
1888
- availableTools.set(tool.id, tool);
1889
- });
1890
-
1891
- // Add route-level tools (these take precedence)
1892
- if (route) {
1893
- route.getTools().forEach((tool) => {
1894
- availableTools.set(tool.id, tool);
1895
- });
1896
- }
1897
-
1898
- // Filter by step-level allowed tools if specified
1899
- if (step?.tools) {
1900
- const allowedToolIds = new Set<string>();
1901
- const stepTools: Tool<TContext, TData, unknown[], unknown>[] = [];
1902
-
1903
- for (const toolRef of step.tools) {
1904
- if (typeof toolRef === "string") {
1905
- // Reference to registered tool
1906
- allowedToolIds.add(toolRef);
1907
- } else {
1908
- // Inline tool definition
1909
- if (toolRef.id) {
1910
- allowedToolIds.add(toolRef.id);
1911
- stepTools.push(toolRef);
1912
- }
1913
- }
1914
- }
1915
-
1916
- // If step specifies tools, only include those
1917
- if (allowedToolIds.size > 0) {
1918
- const filteredTools = new Map<
1919
- string,
1920
- Tool<TContext, TData, unknown[], unknown>
1921
- >();
1922
- for (const toolId of allowedToolIds) {
1923
- const tool = availableTools.get(toolId);
1924
- if (tool) {
1925
- filteredTools.set(toolId, tool);
1926
- }
1927
- }
1928
- // Add inline tools
1929
- stepTools.forEach((tool) => {
1930
- if (tool.id) {
1931
- filteredTools.set(tool.id, tool);
1932
- }
1933
- });
1934
- availableTools.clear();
1935
- filteredTools.forEach((tool, id) => availableTools.set(id, tool));
1936
- }
1937
- }
1938
-
1939
- // Convert to the format expected by AI providers
1940
- return Array.from(availableTools.values()).map((tool) => ({
1941
- id: tool.id,
1942
- name: tool.name || tool.id,
1943
- description: tool.description,
1944
- parameters: tool.parameters,
1945
- }));
652
+ getCurrentSession(): SessionState | undefined {
653
+ return this.currentSession;
1946
654
  }
1947
655
 
1948
656
  /**
1949
657
  * Execute a prepare or finalize function/tool
1950
- * @private
658
+ * @internal Used by ResponseModal
1951
659
  */
1952
- private async executePrepareFinalize(
660
+ async executePrepareFinalize(
1953
661
  prepareOrFinalize:
1954
662
  | string
1955
663
  | Tool<TContext, TData, unknown[], unknown>
@@ -1971,10 +679,7 @@ export class Agent<TContext = any, TData = any> {
1971
679
 
1972
680
  if (typeof prepareOrFinalize === "string") {
1973
681
  // Tool ID - find it in available tools
1974
- const availableTools = new Map<
1975
- string,
1976
- Tool<TContext, TData, unknown[], unknown>
1977
- >();
682
+ const availableTools = new Map<string, Tool<TContext, TData, unknown[], unknown>>();
1978
683
 
1979
684
  // Add agent-level tools
1980
685
  this.tools.forEach((t) => {
@@ -2033,76 +738,6 @@ export class Agent<TContext = any, TData = any> {
2033
738
  }
2034
739
  }
2035
740
 
2036
- /**
2037
- * Get all guidelines
2038
- */
2039
- getGuidelines(): Guideline<TContext, TData>[] {
2040
- return [...this.guidelines];
2041
- }
2042
-
2043
- /**
2044
- * Get the agent's knowledge base
2045
- */
2046
- getKnowledgeBase(): Record<string, unknown> {
2047
- return { ...this.knowledgeBase };
2048
- }
2049
-
2050
- /**
2051
- * Merge terms with route-specific taking precedence on conflicts
2052
- * @private
2053
- */
2054
- private mergeTerms(
2055
- agentTerms: Term<TContext, TData>[],
2056
- routeTerms: Term<TContext, TData>[]
2057
- ): Term<TContext, TData>[] {
2058
- const merged = new Map<string, Term<TContext, TData>>();
2059
-
2060
- // Add agent terms first
2061
- agentTerms.forEach((term) => {
2062
- const name =
2063
- typeof term.name === "string" ? term.name : term.name.toString();
2064
- merged.set(name, term);
2065
- });
2066
-
2067
- // Add route terms (these take precedence)
2068
- routeTerms.forEach((term) => {
2069
- const name =
2070
- typeof term.name === "string" ? term.name : term.name.toString();
2071
- merged.set(name, term);
2072
- });
2073
-
2074
- return Array.from(merged.values());
2075
- }
2076
-
2077
- /**
2078
- * Get the persistence manager (if configured)
2079
- */
2080
- getPersistenceManager(): PersistenceManager<TData> | undefined {
2081
- return this.persistenceManager;
2082
- }
2083
-
2084
- /**
2085
- * Check if persistence is enabled
2086
- */
2087
- hasPersistence(): boolean {
2088
- return this.persistenceManager !== undefined;
2089
- }
2090
-
2091
- /**
2092
- * Set the current session for convenience methods
2093
- * @param session - Session step to use for subsequent calls
2094
- */
2095
- setCurrentSession(session: SessionState): void {
2096
- this.currentSession = session;
2097
- }
2098
-
2099
- /**
2100
- * Get the current session (if set)
2101
- */
2102
- getCurrentSession(): SessionState | undefined {
2103
- return this.currentSession;
2104
- }
2105
-
2106
741
  /**
2107
742
  * Clear the current session
2108
743
  */
@@ -2205,53 +840,25 @@ export class Agent<TContext = any, TData = any> {
2205
840
  */
2206
841
  async chat(
2207
842
  message?: string,
2208
- options?: {
2209
- history?: History; // Optional: override session history for this response
2210
- contextOverride?: Partial<TContext>;
2211
- signal?: AbortSignal;
2212
- }
843
+ options?: GenerateOptions<TContext>
2213
844
  ): Promise<AgentResponse<TData>> {
2214
- // Determine which history to use
2215
- let history: History;
2216
- if (options?.history) {
2217
- // Use provided history for this response only
2218
- history = options.history;
2219
- } else {
2220
- // Add user message to session history if provided
2221
- if (message) {
2222
- await this.session.addMessage("user", message);
2223
- }
2224
- history = this.session.getHistory();
2225
- }
2226
-
2227
- // Get or create session
2228
- let session = await this.session.getOrCreate();
2229
-
2230
- // Merge agent's collected data into session (agent data takes precedence)
2231
- if (Object.keys(this.collectedData).length > 0) {
2232
- session = mergeCollected(session, this.collectedData);
2233
- // Update the session manager with the merged data
2234
- await this.session.setData(this.collectedData);
2235
- logger.debug("[Agent] Merged agent collected data into chat session:", this.collectedData);
2236
- }
845
+ // Delegate to ResponseModal.generate()
846
+ return this.responseModal.generate(message, options);
847
+ }
2237
848
 
2238
- // Use existing respond method with session-managed history
2239
- const result = await this.respond({
2240
- history,
2241
- session,
849
+ /**
850
+ * Modern streaming API - simple interface like chat() but returns a stream
851
+ * Automatically manages conversation history through the session
852
+ */
853
+ async *stream(
854
+ message?: string,
855
+ options?: StreamOptions<TContext>
856
+ ): AsyncGenerator<AgentResponseStreamChunk<TData>> {
857
+ // Delegate to ResponseModal with the same options structure as chat()
858
+ yield* this.responseModal.stream(message, {
859
+ history: options?.history,
2242
860
  contextOverride: options?.contextOverride,
2243
861
  signal: options?.signal,
2244
862
  });
2245
-
2246
- // Add agent response to session history (only if not using override history)
2247
- if (!options?.history) {
2248
- await this.session.addMessage("assistant", result.message);
2249
- }
2250
-
2251
- // Ensure the result includes the current session
2252
- return {
2253
- ...result,
2254
- session: result.session || this.session.current,
2255
- };
2256
863
  }
2257
- }
864
+ }