@falai/agent 0.9.0-alpha-1 → 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 (217) hide show
  1. package/README.md +34 -22
  2. package/dist/cjs/src/core/Agent.d.ts +77 -59
  3. package/dist/cjs/src/core/Agent.d.ts.map +1 -1
  4. package/dist/cjs/src/core/Agent.js +284 -1060
  5. package/dist/cjs/src/core/Agent.js.map +1 -1
  6. package/dist/cjs/src/core/PersistenceManager.d.ts.map +1 -1
  7. package/dist/cjs/src/core/PersistenceManager.js +48 -25
  8. package/dist/cjs/src/core/PersistenceManager.js.map +1 -1
  9. package/dist/cjs/src/core/PromptComposer.d.ts +1 -1
  10. package/dist/cjs/src/core/PromptComposer.d.ts.map +1 -1
  11. package/dist/cjs/src/core/PromptComposer.js.map +1 -1
  12. package/dist/cjs/src/core/ResponseEngine.d.ts +13 -12
  13. package/dist/cjs/src/core/ResponseEngine.d.ts.map +1 -1
  14. package/dist/cjs/src/core/ResponseEngine.js +4 -4
  15. package/dist/cjs/src/core/ResponseEngine.js.map +1 -1
  16. package/dist/cjs/src/core/ResponseModal.d.ts +205 -0
  17. package/dist/cjs/src/core/ResponseModal.d.ts.map +1 -0
  18. package/dist/cjs/src/core/ResponseModal.js +1328 -0
  19. package/dist/cjs/src/core/ResponseModal.js.map +1 -0
  20. package/dist/cjs/src/core/ResponsePipeline.d.ts +66 -38
  21. package/dist/cjs/src/core/ResponsePipeline.d.ts.map +1 -1
  22. package/dist/cjs/src/core/ResponsePipeline.js +72 -4
  23. package/dist/cjs/src/core/ResponsePipeline.js.map +1 -1
  24. package/dist/cjs/src/core/Route.d.ts +24 -5
  25. package/dist/cjs/src/core/Route.d.ts.map +1 -1
  26. package/dist/cjs/src/core/Route.js +45 -1
  27. package/dist/cjs/src/core/Route.js.map +1 -1
  28. package/dist/cjs/src/core/RoutingEngine.d.ts +31 -6
  29. package/dist/cjs/src/core/RoutingEngine.d.ts.map +1 -1
  30. package/dist/cjs/src/core/RoutingEngine.js +113 -9
  31. package/dist/cjs/src/core/RoutingEngine.js.map +1 -1
  32. package/dist/cjs/src/core/SessionManager.d.ts +14 -4
  33. package/dist/cjs/src/core/SessionManager.d.ts.map +1 -1
  34. package/dist/cjs/src/core/SessionManager.js +25 -5
  35. package/dist/cjs/src/core/SessionManager.js.map +1 -1
  36. package/dist/cjs/src/core/Step.d.ts +10 -10
  37. package/dist/cjs/src/core/Step.d.ts.map +1 -1
  38. package/dist/cjs/src/core/Step.js.map +1 -1
  39. package/dist/cjs/src/core/ToolExecutor.d.ts +4 -2
  40. package/dist/cjs/src/core/ToolExecutor.d.ts.map +1 -1
  41. package/dist/cjs/src/core/ToolExecutor.js +13 -3
  42. package/dist/cjs/src/core/ToolExecutor.js.map +1 -1
  43. package/dist/cjs/src/index.d.ts +3 -1
  44. package/dist/cjs/src/index.d.ts.map +1 -1
  45. package/dist/cjs/src/index.js +7 -1
  46. package/dist/cjs/src/index.js.map +1 -1
  47. package/dist/cjs/src/types/agent.d.ts +42 -21
  48. package/dist/cjs/src/types/agent.d.ts.map +1 -1
  49. package/dist/cjs/src/types/agent.js.map +1 -1
  50. package/dist/cjs/src/types/ai.d.ts +1 -1
  51. package/dist/cjs/src/types/ai.d.ts.map +1 -1
  52. package/dist/cjs/src/types/index.d.ts +1 -1
  53. package/dist/cjs/src/types/index.d.ts.map +1 -1
  54. package/dist/cjs/src/types/index.js.map +1 -1
  55. package/dist/cjs/src/types/persistence.d.ts +0 -1
  56. package/dist/cjs/src/types/persistence.d.ts.map +1 -1
  57. package/dist/cjs/src/types/route.d.ts +22 -16
  58. package/dist/cjs/src/types/route.d.ts.map +1 -1
  59. package/dist/cjs/src/types/session.d.ts +6 -11
  60. package/dist/cjs/src/types/session.d.ts.map +1 -1
  61. package/dist/cjs/src/types/tool.d.ts +12 -6
  62. package/dist/cjs/src/types/tool.d.ts.map +1 -1
  63. package/dist/cjs/src/utils/clone.d.ts.map +1 -1
  64. package/dist/cjs/src/utils/clone.js +0 -4
  65. package/dist/cjs/src/utils/clone.js.map +1 -1
  66. package/dist/cjs/src/utils/history.d.ts +30 -1
  67. package/dist/cjs/src/utils/history.d.ts.map +1 -1
  68. package/dist/cjs/src/utils/history.js +169 -23
  69. package/dist/cjs/src/utils/history.js.map +1 -1
  70. package/dist/cjs/src/utils/index.d.ts +1 -1
  71. package/dist/cjs/src/utils/index.d.ts.map +1 -1
  72. package/dist/cjs/src/utils/index.js +5 -1
  73. package/dist/cjs/src/utils/index.js.map +1 -1
  74. package/dist/cjs/src/utils/session.d.ts +2 -2
  75. package/dist/cjs/src/utils/session.d.ts.map +1 -1
  76. package/dist/cjs/src/utils/session.js +6 -26
  77. package/dist/cjs/src/utils/session.js.map +1 -1
  78. package/dist/src/core/Agent.d.ts +77 -59
  79. package/dist/src/core/Agent.d.ts.map +1 -1
  80. package/dist/src/core/Agent.js +285 -1061
  81. package/dist/src/core/Agent.js.map +1 -1
  82. package/dist/src/core/PersistenceManager.d.ts.map +1 -1
  83. package/dist/src/core/PersistenceManager.js +48 -25
  84. package/dist/src/core/PersistenceManager.js.map +1 -1
  85. package/dist/src/core/PromptComposer.d.ts +1 -1
  86. package/dist/src/core/PromptComposer.d.ts.map +1 -1
  87. package/dist/src/core/PromptComposer.js.map +1 -1
  88. package/dist/src/core/ResponseEngine.d.ts +13 -12
  89. package/dist/src/core/ResponseEngine.d.ts.map +1 -1
  90. package/dist/src/core/ResponseEngine.js +4 -4
  91. package/dist/src/core/ResponseEngine.js.map +1 -1
  92. package/dist/src/core/ResponseModal.d.ts +205 -0
  93. package/dist/src/core/ResponseModal.d.ts.map +1 -0
  94. package/dist/src/core/ResponseModal.js +1323 -0
  95. package/dist/src/core/ResponseModal.js.map +1 -0
  96. package/dist/src/core/ResponsePipeline.d.ts +66 -38
  97. package/dist/src/core/ResponsePipeline.d.ts.map +1 -1
  98. package/dist/src/core/ResponsePipeline.js +72 -4
  99. package/dist/src/core/ResponsePipeline.js.map +1 -1
  100. package/dist/src/core/Route.d.ts +24 -5
  101. package/dist/src/core/Route.d.ts.map +1 -1
  102. package/dist/src/core/Route.js +45 -1
  103. package/dist/src/core/Route.js.map +1 -1
  104. package/dist/src/core/RoutingEngine.d.ts +31 -6
  105. package/dist/src/core/RoutingEngine.d.ts.map +1 -1
  106. package/dist/src/core/RoutingEngine.js +113 -9
  107. package/dist/src/core/RoutingEngine.js.map +1 -1
  108. package/dist/src/core/SessionManager.d.ts +14 -4
  109. package/dist/src/core/SessionManager.d.ts.map +1 -1
  110. package/dist/src/core/SessionManager.js +25 -5
  111. package/dist/src/core/SessionManager.js.map +1 -1
  112. package/dist/src/core/Step.d.ts +10 -10
  113. package/dist/src/core/Step.d.ts.map +1 -1
  114. package/dist/src/core/Step.js.map +1 -1
  115. package/dist/src/core/ToolExecutor.d.ts +4 -2
  116. package/dist/src/core/ToolExecutor.d.ts.map +1 -1
  117. package/dist/src/core/ToolExecutor.js +13 -3
  118. package/dist/src/core/ToolExecutor.js.map +1 -1
  119. package/dist/src/index.d.ts +3 -1
  120. package/dist/src/index.d.ts.map +1 -1
  121. package/dist/src/index.js +2 -1
  122. package/dist/src/index.js.map +1 -1
  123. package/dist/src/types/agent.d.ts +42 -21
  124. package/dist/src/types/agent.d.ts.map +1 -1
  125. package/dist/src/types/agent.js.map +1 -1
  126. package/dist/src/types/ai.d.ts +1 -1
  127. package/dist/src/types/ai.d.ts.map +1 -1
  128. package/dist/src/types/index.d.ts +1 -1
  129. package/dist/src/types/index.d.ts.map +1 -1
  130. package/dist/src/types/index.js.map +1 -1
  131. package/dist/src/types/persistence.d.ts +0 -1
  132. package/dist/src/types/persistence.d.ts.map +1 -1
  133. package/dist/src/types/route.d.ts +22 -16
  134. package/dist/src/types/route.d.ts.map +1 -1
  135. package/dist/src/types/session.d.ts +6 -11
  136. package/dist/src/types/session.d.ts.map +1 -1
  137. package/dist/src/types/tool.d.ts +12 -6
  138. package/dist/src/types/tool.d.ts.map +1 -1
  139. package/dist/src/utils/clone.d.ts.map +1 -1
  140. package/dist/src/utils/clone.js +0 -4
  141. package/dist/src/utils/clone.js.map +1 -1
  142. package/dist/src/utils/history.d.ts +30 -1
  143. package/dist/src/utils/history.d.ts.map +1 -1
  144. package/dist/src/utils/history.js +165 -23
  145. package/dist/src/utils/history.js.map +1 -1
  146. package/dist/src/utils/index.d.ts +1 -1
  147. package/dist/src/utils/index.d.ts.map +1 -1
  148. package/dist/src/utils/index.js +1 -1
  149. package/dist/src/utils/index.js.map +1 -1
  150. package/dist/src/utils/session.d.ts +2 -2
  151. package/dist/src/utils/session.d.ts.map +1 -1
  152. package/dist/src/utils/session.js +6 -26
  153. package/dist/src/utils/session.js.map +1 -1
  154. package/docs/README.md +5 -4
  155. package/docs/api/README.md +195 -4
  156. package/docs/api/overview.md +232 -13
  157. package/docs/core/agent/README.md +162 -17
  158. package/docs/core/agent/context-management.md +39 -15
  159. package/docs/core/agent/session-management.md +49 -16
  160. package/docs/core/ai-integration/prompt-composition.md +38 -14
  161. package/docs/core/ai-integration/response-processing.md +28 -17
  162. package/docs/core/conversation-flows/data-collection.md +103 -25
  163. package/docs/core/conversation-flows/route-dsl.md +45 -22
  164. package/docs/core/conversation-flows/routes.md +74 -18
  165. package/docs/core/conversation-flows/step-transitions.md +3 -3
  166. package/docs/core/conversation-flows/steps.md +39 -15
  167. package/docs/core/routing/intelligent-routing.md +18 -9
  168. package/docs/core/tools/tool-definition.md +8 -8
  169. package/docs/core/tools/tool-execution.md +26 -26
  170. package/docs/core/tools/tool-scoping.md +5 -5
  171. package/docs/guides/getting-started/README.md +54 -32
  172. package/docs/guides/migration/README.md +72 -0
  173. package/docs/guides/migration/response-modal-refactor.md +518 -0
  174. package/examples/advanced-patterns/knowledge-based-agent.ts +37 -28
  175. package/examples/advanced-patterns/persistent-onboarding.ts +70 -41
  176. package/examples/advanced-patterns/route-lifecycle-hooks.ts +28 -2
  177. package/examples/advanced-patterns/streaming-responses.ts +197 -119
  178. package/examples/ai-providers/anthropic-integration.ts +40 -33
  179. package/examples/ai-providers/openai-integration.ts +25 -25
  180. package/examples/conversation-flows/completion-transitions.ts +36 -32
  181. package/examples/core-concepts/basic-agent.ts +76 -78
  182. package/examples/core-concepts/modern-streaming-api.ts +309 -0
  183. package/examples/core-concepts/schema-driven-extraction.ts +20 -16
  184. package/examples/core-concepts/session-management.ts +65 -53
  185. package/examples/integrations/database-integration.ts +49 -34
  186. package/examples/integrations/healthcare-integration.ts +96 -91
  187. package/examples/integrations/search-integration.ts +79 -82
  188. package/examples/integrations/server-session-management.ts +25 -17
  189. package/examples/persistence/database-persistence.ts +61 -45
  190. package/examples/persistence/memory-sessions.ts +52 -63
  191. package/examples/persistence/redis-persistence.ts +81 -95
  192. package/examples/tools/basic-tools.ts +73 -62
  193. package/examples/tools/data-enrichment-tools.ts +52 -44
  194. package/package.json +1 -1
  195. package/src/core/Agent.ts +396 -1499
  196. package/src/core/PersistenceManager.ts +51 -27
  197. package/src/core/PromptComposer.ts +1 -1
  198. package/src/core/ResponseEngine.ts +21 -19
  199. package/src/core/ResponseModal.ts +1722 -0
  200. package/src/core/ResponsePipeline.ts +175 -60
  201. package/src/core/Route.ts +58 -6
  202. package/src/core/RoutingEngine.ts +174 -27
  203. package/src/core/SessionManager.ts +32 -8
  204. package/src/core/Step.ts +20 -12
  205. package/src/core/ToolExecutor.ts +19 -5
  206. package/src/index.ts +11 -0
  207. package/src/types/agent.ts +47 -23
  208. package/src/types/ai.ts +1 -1
  209. package/src/types/index.ts +2 -0
  210. package/src/types/persistence.ts +0 -1
  211. package/src/types/route.ts +22 -16
  212. package/src/types/session.ts +6 -12
  213. package/src/types/tool.ts +15 -9
  214. package/src/utils/clone.ts +6 -8
  215. package/src/utils/history.ts +190 -27
  216. package/src/utils/index.ts +4 -0
  217. package/src/utils/session.ts +6 -31
package/src/core/Agent.ts CHANGED
@@ -10,25 +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,
17
+ ValidationError,
18
+ ValidationResult,
20
19
  } from "../types";
21
- import { EventKind, MessageRole } from "../types/history";
20
+ import type { StreamOptions, GenerateOptions, RespondParams } from "./ResponseModal";
22
21
  import {
23
- enterRoute,
24
- enterStep,
25
22
  mergeCollected,
26
23
  logger,
27
24
  LoggerLevel,
28
25
  render,
29
- getLastMessageFromHistory,
30
- normalizeHistory,
31
- cloneDeep,
32
26
  } from "../utils";
33
27
 
34
28
  import { Route } from "./Route";
@@ -36,32 +30,51 @@ import { Step } from "./Step";
36
30
  import { PersistenceManager } from "./PersistenceManager";
37
31
  import { SessionManager } from "./SessionManager";
38
32
  import { RoutingEngine } from "./RoutingEngine";
39
- import { ResponseEngine } from "./ResponseEngine";
40
33
  import { ToolExecutor } from "./ToolExecutor";
41
- import { ResponsePipeline } from "./ResponsePipeline";
42
- import { END_ROUTE_ID } from "../constants";
34
+ import { ResponseModal } from "./ResponseModal";
43
35
 
44
36
  /**
45
- * Main Agent class with generic context support
37
+ * Error thrown when data validation fails
46
38
  */
47
- export class Agent<TContext = unknown> {
48
- private terms: Term<TContext>[] = [];
49
- private guidelines: Guideline<TContext>[] = [];
50
- private tools: Tool<TContext, unknown[], unknown, unknown>[] = [];
51
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
52
- private routes: Route<TContext, any>[] = [];
39
+ class DataValidationError extends Error {
40
+ constructor(public errors: ValidationError[], message?: string) {
41
+ super(message || "Data validation failed");
42
+ this.name = "DataValidationError";
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Error thrown when route configuration is invalid
48
+ */
49
+ class RouteConfigurationError extends Error {
50
+ constructor(public routeTitle: string, public invalidFields: string[], message?: string) {
51
+ super(message || `Route configuration error in '${routeTitle}'`);
52
+ this.name = "RouteConfigurationError";
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Main Agent class with generic context and data support
58
+ */
59
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
60
+ export class Agent<TContext = any, TData = any> {
61
+ private terms: Term<TContext, TData>[] = [];
62
+ private guidelines: Guideline<TContext, TData>[] = [];
63
+ private tools: Tool<TContext, TData, unknown[], unknown>[] = [];
64
+ private routes: Route<TContext, TData>[] = [];
53
65
  private context: TContext | undefined;
54
- private persistenceManager: PersistenceManager | undefined;
55
- private routingEngine: RoutingEngine<TContext>;
56
- private responseEngine: ResponseEngine<TContext>;
57
- private responsePipeline: ResponsePipeline<TContext>;
58
- private currentSession?: SessionState;
66
+ private persistenceManager: PersistenceManager<TData> | undefined;
67
+ private routingEngine: RoutingEngine<TContext, TData>;
68
+ private responseModal: ResponseModal<TContext, TData>;
69
+ private currentSession?: SessionState<TData>;
59
70
  private knowledgeBase: Record<string, unknown> = {};
71
+ private schema?: StructuredSchema;
72
+ private collectedData: Partial<TData> = {};
60
73
 
61
74
  /** Public session manager for easy session management */
62
- public session: SessionManager<unknown>;
75
+ public session: SessionManager<TData>;
63
76
 
64
- constructor(private readonly options: AgentOptions<TContext>) {
77
+ constructor(private readonly options: AgentOptions<TContext, TData>) {
65
78
  // Set log level based on debug option
66
79
  if (options.debug) {
67
80
  logger.setLevel(LoggerLevel.DEBUG);
@@ -74,40 +87,74 @@ export class Agent<TContext = unknown> {
74
87
  );
75
88
  }
76
89
 
90
+ // Initialize and validate agent-level schema if provided
91
+ if (options.schema) {
92
+ this.schema = options.schema;
93
+ this.validateSchema(this.schema);
94
+ logger.debug("[Agent] Agent-level schema initialized and validated");
95
+ }
96
+
77
97
  // Initialize context if provided
78
98
  this.context = options.context;
79
99
 
100
+ // Initialize collected data with initial data if provided
101
+ if (options.initialData) {
102
+ if (this.schema) {
103
+ const validation = this.validateData(options.initialData);
104
+ if (!validation.valid) {
105
+ throw new Error(
106
+ `Initial data validation failed: ${validation.errors.map(e => e.message).join(', ')}`
107
+ );
108
+ }
109
+ }
110
+ this.collectedData = { ...options.initialData };
111
+ logger.debug("[Agent] Initial data set:", this.collectedData);
112
+ }
113
+
80
114
  // Initialize current session if provided
81
115
  this.currentSession = options.session;
82
116
 
83
- // Initialize routing and response engines
84
- this.routingEngine = new RoutingEngine<TContext>({
117
+ // Initialize routing engine
118
+ this.routingEngine = new RoutingEngine<TContext, TData>({
85
119
  maxCandidates: 5,
86
120
  allowRouteSwitch: true,
87
121
  switchThreshold: 70,
88
122
  });
89
- this.responseEngine = new ResponseEngine<TContext>();
90
- this.responsePipeline = new ResponsePipeline<TContext>(
91
- options,
92
- this.routes,
93
- this.tools,
94
- this.routingEngine,
95
- this.updateContext.bind(this),
96
- this.updateData.bind(this)
97
- );
123
+
124
+ // Initialize ResponseModal for handling all response generation
125
+ this.responseModal = new ResponseModal<TContext, TData>(this);
98
126
 
99
127
  // Initialize persistence if configured
100
128
  if (options.persistence) {
101
- this.persistenceManager = new PersistenceManager(options.persistence);
129
+ try {
130
+ // Validate persistence configuration
131
+ if (!options.persistence.adapter) {
132
+ throw new Error("Persistence adapter is required when persistence is configured");
133
+ }
102
134
 
103
- // Initialize the adapter if it has an initialize method
104
- if (options.persistence.adapter.initialize) {
105
- options.persistence.adapter.initialize().catch((error) => {
106
- logger.error(
107
- "[Agent] Persistence adapter initialization failed:",
108
- error
109
- );
110
- });
135
+ if (!options.persistence.adapter.sessionRepository) {
136
+ throw new Error("Persistence adapter must provide a sessionRepository");
137
+ }
138
+
139
+ if (!options.persistence.adapter.messageRepository) {
140
+ throw new Error("Persistence adapter must provide a messageRepository");
141
+ }
142
+
143
+ this.persistenceManager = new PersistenceManager<TData>(options.persistence);
144
+
145
+ // Initialize the adapter if it has an initialize method
146
+ if (options.persistence.adapter.initialize) {
147
+ options.persistence.adapter.initialize().catch((error) => {
148
+ logger.error(
149
+ "[Agent] Persistence adapter initialization failed:",
150
+ error instanceof Error ? error.message : String(error)
151
+ );
152
+ });
153
+ }
154
+ } catch (error) {
155
+ const errorMessage = error instanceof Error ? error.message : String(error);
156
+ logger.error("[Agent] Failed to initialize persistence:", errorMessage);
157
+ throw new Error(`Failed to initialize persistence: ${errorMessage}`);
111
158
  }
112
159
  }
113
160
 
@@ -132,7 +179,7 @@ export class Agent<TContext = unknown> {
132
179
 
133
180
  if (options.routes) {
134
181
  options.routes.forEach((routeOptions) => {
135
- this.createRoute<unknown>(routeOptions);
182
+ this.createRoute(routeOptions);
136
183
  });
137
184
  }
138
185
 
@@ -142,7 +189,7 @@ export class Agent<TContext = unknown> {
142
189
  }
143
190
 
144
191
  // Initialize session manager
145
- this.session = new SessionManager(this.persistenceManager);
192
+ this.session = new SessionManager<TData>(this.persistenceManager);
146
193
 
147
194
  // Store sessionId for later use in getOrCreate calls
148
195
  if (options.sessionId) {
@@ -153,6 +200,143 @@ export class Agent<TContext = unknown> {
153
200
  }
154
201
  }
155
202
 
203
+ /**
204
+ * Validate the agent-level schema structure
205
+ * @private
206
+ */
207
+ private validateSchema(schema: StructuredSchema): void {
208
+ if (!schema || typeof schema !== 'object') {
209
+ throw new Error(
210
+ "Agent schema must be a valid JSON Schema object. " +
211
+ "Provide a schema with 'type': 'object' and 'properties' to define the data structure."
212
+ );
213
+ }
214
+
215
+ if (schema.type !== 'object') {
216
+ throw new Error(
217
+ `Agent schema must be of type 'object', but received '${String(schema.type)}'. ` +
218
+ "Agent-level schemas must define object structures for data collection."
219
+ );
220
+ }
221
+
222
+ if (!schema.properties || typeof schema.properties !== 'object') {
223
+ throw new Error(
224
+ "Agent schema must have a 'properties' field defining the data fields. " +
225
+ "Example: { type: 'object', properties: { name: { type: 'string' }, email: { type: 'string' } } }"
226
+ );
227
+ }
228
+
229
+ logger.debug("[Agent] Schema validation passed");
230
+ }
231
+
232
+ /**
233
+ * Validate data against the agent-level schema
234
+ */
235
+ validateData(data: Partial<TData>): ValidationResult {
236
+ if (!this.schema) {
237
+ // No schema defined, consider all data valid
238
+ return { valid: true, errors: [], warnings: [] };
239
+ }
240
+
241
+ const errors: ValidationError[] = [];
242
+ const warnings: ValidationError[] = [];
243
+
244
+ // Basic validation - check if provided fields exist in schema
245
+ if (this.schema.properties) {
246
+ for (const [key, value] of Object.entries(data)) {
247
+ if (!(key in this.schema.properties)) {
248
+ errors.push({
249
+ field: key,
250
+ value,
251
+ message: `Field '${key}' is not defined in agent schema`,
252
+ schemaPath: `properties.${key}`
253
+ });
254
+ }
255
+ }
256
+ }
257
+
258
+ // Check required fields if specified
259
+ if (this.schema.required && Array.isArray(this.schema.required)) {
260
+ for (const requiredField of this.schema.required) {
261
+ if (!(requiredField in data) || data[requiredField as keyof TData] === undefined) {
262
+ warnings.push({
263
+ field: requiredField,
264
+ value: undefined,
265
+ message: `Required field '${requiredField}' is missing`,
266
+ schemaPath: `required`
267
+ });
268
+ }
269
+ }
270
+ }
271
+
272
+ return {
273
+ valid: errors.length === 0,
274
+ errors,
275
+ warnings
276
+ };
277
+ }
278
+
279
+ /**
280
+ * Check if a field is valid according to the agent schema
281
+ * @param field - The field key to validate
282
+ * @returns true if field exists in schema or no schema is defined, false otherwise
283
+ */
284
+ isValidSchemaField(field: keyof TData): boolean {
285
+ if (!this.schema || !this.schema.properties) {
286
+ // No schema defined, consider all fields valid
287
+ return true;
288
+ }
289
+
290
+ return field as string in this.schema.properties;
291
+ }
292
+
293
+ /**
294
+ * Get the current collected data
295
+ */
296
+ getCollectedData(): Partial<TData> {
297
+ return { ...this.collectedData };
298
+ }
299
+
300
+ /**
301
+ * Update collected data with validation
302
+ */
303
+ async updateCollectedData(updates: Partial<TData>): Promise<void> {
304
+ // Validate the updates
305
+ const validation = this.validateData(updates);
306
+ if (!validation.valid) {
307
+ const errorMessages = validation.errors.map(e => e.message).join(', ');
308
+ throw new DataValidationError(validation.errors, `Data validation failed: ${errorMessages}`);
309
+ }
310
+
311
+ // Log warnings if any
312
+ if (validation.warnings.length > 0) {
313
+ const warningMessages = validation.warnings.map(w => w.message).join(', ');
314
+ logger.warn(`[Agent] Data validation warnings: ${warningMessages}`);
315
+ }
316
+
317
+ // Merge updates with current data
318
+ const previousData = { ...this.collectedData };
319
+ this.collectedData = {
320
+ ...this.collectedData,
321
+ ...updates
322
+ };
323
+
324
+ // Trigger agent-level lifecycle hook if configured
325
+ if (this.options.hooks?.onDataUpdate) {
326
+ this.collectedData = await this.options.hooks.onDataUpdate(
327
+ this.collectedData,
328
+ previousData
329
+ );
330
+ }
331
+
332
+ // Update current session if it exists to keep it in sync
333
+ if (this.currentSession) {
334
+ this.currentSession = mergeCollected(this.currentSession, this.collectedData);
335
+ }
336
+
337
+ logger.debug("[Agent] Collected data updated:", updates);
338
+ }
339
+
156
340
  /**
157
341
  * Get agent name
158
342
  */
@@ -182,12 +366,41 @@ export class Agent<TContext = unknown> {
182
366
  }
183
367
 
184
368
  /**
185
- * Create a new route (journey)
186
- * @template TData - Type of data collected throughout the route
369
+ * Create a new route (journey) using agent-level data type
187
370
  */
188
- createRoute<TData = unknown>(
371
+ createRoute(
189
372
  options: RouteOptions<TContext, TData>
190
373
  ): Route<TContext, TData> {
374
+ // Validate that requiredFields exist in agent schema
375
+ if (options.requiredFields && this.schema?.properties) {
376
+ const invalidRequiredFields = options.requiredFields.filter(
377
+ field => !(String(field) in this.schema!.properties!)
378
+ );
379
+ if (invalidRequiredFields.length > 0) {
380
+ throw new RouteConfigurationError(
381
+ options.title,
382
+ invalidRequiredFields.map(f => String(f)),
383
+ `Invalid required fields in route '${options.title}': ${invalidRequiredFields.join(', ')}. ` +
384
+ `Must be valid keys from agent schema. Available fields: ${Object.keys(this.schema.properties).join(', ')}.`
385
+ );
386
+ }
387
+ }
388
+
389
+ // Validate that optionalFields exist in agent schema
390
+ if (options.optionalFields && this.schema?.properties) {
391
+ const invalidOptionalFields = options.optionalFields.filter(
392
+ field => !(String(field) in this.schema!.properties!)
393
+ );
394
+ if (invalidOptionalFields.length > 0) {
395
+ throw new RouteConfigurationError(
396
+ options.title,
397
+ invalidOptionalFields.map(f => String(f)),
398
+ `Invalid optional fields in route '${options.title}': ${invalidOptionalFields.join(', ')}. ` +
399
+ `Must be valid keys from agent schema. Available fields: ${Object.keys(this.schema.properties).join(', ')}.`
400
+ );
401
+ }
402
+ }
403
+
191
404
  const route = new Route<TContext, TData>(options);
192
405
  this.routes.push(route);
193
406
  return route;
@@ -196,7 +409,7 @@ export class Agent<TContext = unknown> {
196
409
  /**
197
410
  * Create a domain term for the glossary
198
411
  */
199
- createTerm(term: Term<TContext>): this {
412
+ createTerm(term: Term<TContext, TData>): this {
200
413
  this.terms.push(term);
201
414
  return this;
202
415
  }
@@ -204,7 +417,7 @@ export class Agent<TContext = unknown> {
204
417
  /**
205
418
  * Create a behavioral guideline
206
419
  */
207
- createGuideline(guideline: Guideline<TContext>): this {
420
+ createGuideline(guideline: Guideline<TContext, TData>): this {
208
421
  const guidelineWithId = {
209
422
  ...guideline,
210
423
  id: guideline.id || `guideline_${this.guidelines.length}`,
@@ -217,7 +430,7 @@ export class Agent<TContext = unknown> {
217
430
  /**
218
431
  * Register a tool at the agent level
219
432
  */
220
- createTool(tool: Tool<TContext, unknown[], unknown, unknown>): this {
433
+ createTool(tool: Tool<TContext, TData, unknown[], unknown>): this {
221
434
  this.tools.push(tool);
222
435
  return this;
223
436
  }
@@ -225,7 +438,7 @@ export class Agent<TContext = unknown> {
225
438
  /**
226
439
  * Register multiple tools at the agent level
227
440
  */
228
- registerTools(tools: Tool<TContext, unknown[], unknown, unknown>[]): this {
441
+ registerTools(tools: Tool<TContext, TData, unknown[], unknown>[]): this {
229
442
  tools.forEach((tool) => this.createTool(tool));
230
443
  return this;
231
444
  }
@@ -267,7 +480,7 @@ export class Agent<TContext = unknown> {
267
480
  * Triggers both agent-level and route-specific onDataUpdate lifecycle hooks if configured
268
481
  * @internal
269
482
  */
270
- private async updateData<TData = unknown>(
483
+ private async updateData(
271
484
  session: SessionState<TData>,
272
485
  dataUpdate: Partial<TData>
273
486
  ): Promise<SessionState<TData>> {
@@ -297,9 +510,12 @@ export class Agent<TContext = unknown> {
297
510
  newCollected = (await this.options.hooks.onDataUpdate(
298
511
  newCollected,
299
512
  previousCollected
300
- )) as Partial<TData>;
513
+ ));
301
514
  }
302
515
 
516
+ // Update agent's collected data to stay in sync
517
+ this.collectedData = { ...newCollected };
518
+
303
519
  // Return updated session
304
520
  return mergeCollected(session, newCollected);
305
521
  }
@@ -316,1370 +532,141 @@ export class Agent<TContext = unknown> {
316
532
  // Otherwise return the stored context
317
533
  return this.context;
318
534
  }
535
+ /**
536
+ * Get current schema
537
+ */
538
+ getSchema(): StructuredSchema | undefined {
539
+ return this.schema;
540
+ }
319
541
 
320
542
  /**
321
543
  * Generate a response based on history and context as a stream
322
544
  */
323
- async *respondStream<TData = Record<string, unknown>>(params: {
324
- history: History;
325
- step?: StepRef;
326
- session?: SessionState<TData>;
327
- contextOverride?: Partial<TContext>;
328
- signal?: AbortSignal;
329
- }): AsyncGenerator<AgentResponseStreamChunk<TData>> {
330
- const { history: simpleHistory, signal } = params;
331
- const history = normalizeHistory(simpleHistory);
332
-
333
- // Prepare context and session using the response pipeline
334
- this.responsePipeline.setContext(this.context);
335
- this.responsePipeline.setCurrentSession(this.currentSession);
336
- let session: SessionState;
337
- const responseContext = await this.responsePipeline.prepareResponseContext({
338
- contextOverride: params.contextOverride,
339
- session: params.session ? cloneDeep(params.session) : undefined,
340
- });
341
- const { effectiveContext } = responseContext;
342
- session = responseContext.session;
343
- // Update our stored context if it was modified by beforeRespond hook
344
- this.context = this.responsePipeline.getStoredContext();
345
-
346
- // PHASE 1: PREPARE - Execute prepare function if current step has one
347
- if (session.currentRoute && session.currentStep) {
348
- const currentRoute = this.routes.find(
349
- (r) => r.id === session.currentRoute?.id
350
- );
351
- if (currentRoute) {
352
- const currentStep = currentRoute.getStep(session.currentStep.id);
353
- if (currentStep?.prepare) {
354
- logger.debug(`[Agent] Executing prepare for step: ${currentStep.id}`);
355
- await this.executePrepareFinalize(
356
- currentStep.prepare,
357
- effectiveContext,
358
- session.data,
359
- currentRoute,
360
- currentStep
361
- );
362
- }
363
- }
364
- }
365
-
366
- // PHASE 2: ROUTING + STEP SELECTION - Use response pipeline
367
- const routingResult =
368
- await this.responsePipeline.handleRoutingAndStepSelection({
369
- session,
370
- history,
371
- context: effectiveContext,
372
- signal,
373
- });
374
- const selectedRoute = routingResult.selectedRoute;
375
- const selectedStep = routingResult.selectedStep;
376
- const responseDirectives = routingResult.responseDirectives;
377
- const isRouteComplete = routingResult.isRouteComplete;
378
- session = routingResult.session;
379
-
380
- // PHASE 3: DETERMINE NEXT STEP - Use pipeline method
381
- const stepResult = this.responsePipeline.determineNextStep({
382
- selectedRoute,
383
- selectedStep,
384
- session,
385
- isRouteComplete,
386
- });
387
- const nextStep = stepResult.nextStep;
388
- session = stepResult.session;
389
-
390
- if (selectedRoute && !isRouteComplete) {
391
- // PHASE 4: RESPONSE GENERATION - Stream message using selected route and step
392
- // Get last user message
393
- const lastUserMessage = getLastMessageFromHistory(history);
394
-
395
- // Build response schema for this route (with collect fields from step)
396
- const responseSchema = this.responseEngine.responseSchemaForRoute(
397
- selectedRoute,
398
- nextStep
399
- );
400
-
401
- // Check if selected route and next step are defined
402
- if (!selectedRoute || !nextStep) {
403
- logger.error("[Agent] Selected route or next step is not defined", {
404
- selectedRoute,
405
- nextStep,
406
- });
407
- throw new Error("Selected route or next step is not defined");
408
- }
409
-
410
- // Build response prompt
411
- const responsePrompt = await this.responseEngine.buildResponsePrompt({
412
- route: selectedRoute,
413
- currentStep: nextStep,
414
- rules: selectedRoute.getRules(),
415
- prohibitions: selectedRoute.getProhibitions(),
416
- directives: responseDirectives,
417
- history,
418
- lastMessage: lastUserMessage,
419
- agentOptions: this.options,
420
- // Combine agent and route properties according to the specified logic
421
- combinedGuidelines: [
422
- ...this.getGuidelines(),
423
- ...selectedRoute.getGuidelines(),
424
- ],
425
- combinedTerms: this.mergeTerms(
426
- this.getTerms(),
427
- selectedRoute.getTerms()
428
- ),
429
- context: effectiveContext,
430
- session,
431
- });
432
-
433
- // Collect available tools for AI
434
- const availableTools = this.collectAvailableTools(
435
- selectedRoute,
436
- nextStep
437
- );
438
-
439
- // Generate message stream using AI provider
440
- const stream = this.options.provider.generateMessageStream({
441
- prompt: responsePrompt,
442
- history,
443
- context: effectiveContext,
444
- tools: availableTools,
445
- signal,
446
- parameters: {
447
- jsonSchema: responseSchema,
448
- schemaName: "response_stream_output",
449
- },
450
- });
451
-
452
- // Stream chunks to caller
453
- for await (const chunk of stream) {
454
- let toolCalls:
455
- | Array<{ toolName: string; arguments: Record<string, unknown> }>
456
- | undefined = undefined;
457
-
458
- // Extract tool calls from AI response on final chunk
459
- if (chunk.done && chunk.structured?.toolCalls) {
460
- toolCalls = chunk.structured.toolCalls;
461
-
462
- // Execute dynamic tool calls
463
- if (toolCalls.length > 0) {
464
- logger.debug(
465
- `[Agent] Executing ${toolCalls.length} dynamic tool calls`
466
- );
467
-
468
- for (const toolCall of toolCalls) {
469
- const tool = this.findAvailableTool(
470
- toolCall.toolName,
471
- selectedRoute
472
- );
473
- if (!tool) {
474
- logger.warn(`[Agent] Tool not found: ${toolCall.toolName}`);
475
- continue;
476
- }
477
-
478
- const toolExecutor = new ToolExecutor<TContext, unknown>();
479
- const result = await toolExecutor.executeTool({
480
- tool: tool,
481
- context: effectiveContext,
482
- updateContext: this.updateContext.bind(this),
483
- history,
484
- data: session.data,
485
- toolArguments: toolCall.arguments,
486
- });
487
-
488
- // Update context with tool results
489
- if (result.contextUpdate) {
490
- await this.updateContext(
491
- result.contextUpdate as Partial<TContext>
492
- );
493
- }
494
-
495
- // Update collected data with tool results
496
- if (result.dataUpdate) {
497
- session = await this.updateData(session, result.dataUpdate);
498
- logger.debug(
499
- `[Agent] Tool updated collected data:`,
500
- result.dataUpdate
501
- );
502
- }
503
-
504
- logger.debug(
505
- `[Agent] Executed dynamic tool: ${result.toolName} (success: ${result.success})`
506
- );
507
- }
508
- }
509
- }
510
-
511
- // TOOL LOOP: Allow AI to make follow-up tool calls after initial tool execution (streaming)
512
- const MAX_TOOL_LOOPS = 5;
513
- let toolLoopCount = 0;
514
- let hasToolCalls = toolCalls && toolCalls.length > 0;
515
-
516
- while (hasToolCalls && toolLoopCount < MAX_TOOL_LOOPS) {
517
- toolLoopCount++;
518
- logger.debug(
519
- `[Agent] Starting streaming tool loop ${toolLoopCount}/${MAX_TOOL_LOOPS}`
520
- );
521
-
522
- // Add tool execution results to history so AI knows what happened
523
- const toolResultsEvents: Event[] = [];
524
- for (const toolCall of toolCalls || []) {
525
- const tool = this.findAvailableTool(
526
- toolCall.toolName,
527
- selectedRoute
528
- );
529
- if (tool) {
530
- toolResultsEvents.push({
531
- kind: EventKind.TOOL,
532
- source: MessageRole.AGENT,
533
- timestamp: new Date().toISOString(),
534
- data: {
535
- tool_calls: [
536
- {
537
- tool_id: toolCall.toolName,
538
- arguments: toolCall.arguments,
539
- result: {
540
- data: "Tool executed successfully",
541
- },
542
- },
543
- ],
544
- },
545
- });
546
- }
547
- }
548
-
549
- // Create updated history with tool results
550
- const updatedHistory = [...history, ...toolResultsEvents];
551
-
552
- // Make follow-up streaming AI call to see if more tools are needed
553
- const followUpStream = this.options.provider.generateMessageStream({
554
- prompt: responsePrompt,
555
- history: updatedHistory,
556
- context: effectiveContext,
557
- tools: availableTools,
558
- parameters: {
559
- jsonSchema: responseSchema,
560
- schemaName: "tool_followup",
561
- },
562
- signal,
563
- });
564
-
565
- let followUpToolCalls:
566
- | Array<{ toolName: string; arguments: Record<string, unknown> }>
567
- | undefined;
568
-
569
- for await (const followUpChunk of followUpStream) {
570
- // Extract tool calls from follow-up stream
571
- if (followUpChunk.done && followUpChunk.structured?.toolCalls) {
572
- followUpToolCalls = followUpChunk.structured.toolCalls;
573
- }
574
- }
575
-
576
- hasToolCalls = followUpToolCalls && followUpToolCalls.length > 0;
577
-
578
- if (hasToolCalls) {
579
- logger.debug(
580
- `[Agent] Follow-up streaming call produced ${
581
- followUpToolCalls!.length
582
- } additional tool calls`
583
- );
584
-
585
- // Execute the follow-up tool calls
586
- for (const toolCall of followUpToolCalls!) {
587
- const tool = this.findAvailableTool(
588
- toolCall.toolName,
589
- selectedRoute
590
- );
591
- if (!tool) {
592
- logger.warn(
593
- `[Agent] Tool not found in streaming follow-up: ${toolCall.toolName}`
594
- );
595
- continue;
596
- }
597
-
598
- const toolExecutor = new ToolExecutor<TContext, unknown>();
599
- const result = await toolExecutor.executeTool({
600
- tool: tool,
601
- context: effectiveContext,
602
- updateContext: this.updateContext.bind(this),
603
- history: updatedHistory,
604
- data: session.data,
605
- toolArguments: toolCall.arguments,
606
- });
607
-
608
- // Update context with follow-up tool results
609
- if (result.contextUpdate) {
610
- await this.updateContext(
611
- result.contextUpdate as Partial<TContext>
612
- );
613
- }
614
-
615
- if (result.dataUpdate) {
616
- session = await this.updateData(session, result.dataUpdate);
617
- logger.debug(
618
- `[Agent] Streaming follow-up tool updated collected data:`,
619
- result.dataUpdate
620
- );
621
- }
622
-
623
- logger.debug(
624
- `[Agent] Executed streaming follow-up tool: ${result.toolName} (success: ${result.success})`
625
- );
626
- }
627
-
628
- // Update toolCalls for next iteration
629
- toolCalls = followUpToolCalls;
630
- } else {
631
- logger.debug(
632
- `[Agent] Streaming tool loop completed after ${toolLoopCount} iterations`
633
- );
634
- // Update toolCalls for final response
635
- toolCalls = followUpToolCalls || [];
636
- break;
637
- }
638
- }
639
-
640
- if (toolLoopCount >= MAX_TOOL_LOOPS) {
641
- logger.warn(
642
- `[Agent] Streaming tool loop limit reached (${MAX_TOOL_LOOPS}), stopping`
643
- );
644
- }
645
-
646
- // Extract collected data on final chunk
647
- if (chunk.done && chunk.structured && nextStep.collect) {
648
- const collectedData: Record<string, unknown> = {};
649
- // The structured response includes both base fields and collected extraction fields
650
- const structuredData = chunk.structured as AgentStructuredResponse &
651
- Record<string, unknown>;
652
-
653
- for (const field of nextStep.collect) {
654
- if (field in structuredData) {
655
- collectedData[field] = structuredData[field];
656
- }
657
- }
658
-
659
- // Merge collected data into session
660
- if (Object.keys(collectedData).length > 0) {
661
- session = await this.updateData(session, collectedData);
662
- logger.debug(`[Agent] Collected data:`, collectedData);
663
- }
664
- }
665
-
666
- // Extract any additional data from structured response on final chunk
667
- if (
668
- chunk.done &&
669
- chunk.structured &&
670
- typeof chunk.structured === "object" &&
671
- "contextUpdate" in chunk.structured
672
- ) {
673
- await this.updateContext(
674
- (chunk.structured as { contextUpdate?: Partial<TContext> })
675
- .contextUpdate as Partial<TContext>
676
- );
677
- }
678
-
679
- // Auto-save session step on final chunk
680
- if (
681
- chunk.done &&
682
- this.persistenceManager &&
683
- session.id &&
684
- this.options.persistence?.autoSave !== false
685
- ) {
686
- await this.persistenceManager.saveSessionState(session.id, session);
687
- logger.debug(
688
- `[Agent] Auto-saved session step to persistence: ${session.id}`
689
- );
690
- }
691
-
692
- // Execute finalize function on final chunk
693
- if (chunk.done && session.currentRoute && session.currentStep) {
694
- const currentRoute = this.routes.find(
695
- (r) => r.id === session.currentRoute?.id
696
- );
697
- if (currentRoute) {
698
- const currentStep = currentRoute.getStep(session.currentStep.id);
699
- if (currentStep?.finalize) {
700
- logger.debug(
701
- `[Agent] Executing finalize for step: ${currentStep.id}`
702
- );
703
- await this.executePrepareFinalize(
704
- currentStep.finalize,
705
- effectiveContext,
706
- session.data,
707
- currentRoute,
708
- currentStep
709
- );
710
- }
711
- }
712
- }
713
-
714
- // Update current session if we have one
715
- if (chunk.done && this.currentSession) {
716
- this.currentSession = session;
717
- }
718
-
719
- yield {
720
- delta: chunk.delta,
721
- accumulated: chunk.accumulated,
722
- done: chunk.done,
723
- session, // Return updated session
724
- toolCalls,
725
- isRouteComplete,
726
- metadata: chunk.metadata,
727
- structured: chunk.structured,
728
- };
729
- }
730
- } else if (isRouteComplete && selectedRoute) {
731
- // Route is complete - generate completion message then check for onComplete transition
732
- const lastUserMessage = getLastMessageFromHistory(history);
733
-
734
- // Get endStep spec from route
735
- const endStepSpec = selectedRoute.endStepSpec;
736
-
737
- // Create a temporary step for completion message generation using endStep configuration
738
- const completionStep = new Step<TContext, unknown>(selectedRoute.id, {
739
- description: endStepSpec.description,
740
- id: endStepSpec.id || END_ROUTE_ID,
741
- collect: endStepSpec.collect,
742
- requires: endStepSpec.requires,
743
- prompt:
744
- endStepSpec.prompt ||
745
- "Summarize what was accomplished and confirm completion based on the conversation history and collected data",
746
- });
747
-
748
- // Build response schema for completion
749
- const responseSchema = this.responseEngine.responseSchemaForRoute(
750
- selectedRoute,
751
- completionStep
752
- );
753
- const templateContext = {
754
- context: effectiveContext,
755
- session,
756
- history,
757
- };
758
-
759
- // Build completion response prompt
760
- const completionPrompt = await this.responseEngine.buildResponsePrompt({
761
- route: selectedRoute,
762
- currentStep: completionStep,
763
- rules: selectedRoute.getRules(),
764
- prohibitions: selectedRoute.getProhibitions(),
765
- directives: undefined, // No directives for completion
766
- history,
767
- lastMessage: lastUserMessage,
768
- agentOptions: this.options,
769
- // Combine agent and route properties according to the specified logic
770
- combinedGuidelines: [
771
- ...this.getGuidelines(),
772
- ...selectedRoute.getGuidelines(),
773
- ],
774
- combinedTerms: this.mergeTerms(
775
- this.getTerms(),
776
- selectedRoute.getTerms()
777
- ),
778
- context: effectiveContext,
779
- session,
780
- });
781
-
782
- // Stream completion message using AI provider
783
- const stream = this.options.provider.generateMessageStream({
784
- prompt: completionPrompt,
785
- history,
786
- context: effectiveContext,
787
- signal,
788
- parameters: {
789
- jsonSchema: responseSchema,
790
- schemaName: "completion_message_stream",
791
- },
792
- });
793
-
794
- logger.debug(
795
- `[Agent] Streaming completion message for route: ${selectedRoute.title}`
796
- );
797
-
798
- // Check for onComplete transition
799
- const transitionConfig = await selectedRoute.evaluateOnComplete(
800
- { data: session.data },
801
- effectiveContext
802
- );
803
-
804
- if (transitionConfig) {
805
- // Find target route by ID or title
806
- const targetRoute = this.routes.find(
807
- (r) =>
808
- r.id === transitionConfig.nextStep ||
809
- r.title === transitionConfig.nextStep
810
- );
811
-
812
- if (targetRoute) {
813
- const renderedCondition = await render(
814
- transitionConfig.condition,
815
- templateContext
816
- );
817
- // Set pending transition in session
818
- session = {
819
- ...session,
820
- pendingTransition: {
821
- targetRouteId: targetRoute.id,
822
- condition: renderedCondition,
823
- reason: "route_complete",
824
- },
825
- };
826
- logger.debug(
827
- `[Agent] Route ${selectedRoute.title} completed with pending transition to: ${targetRoute.title}`
828
- );
829
- } else {
830
- logger.warn(
831
- `[Agent] Route ${selectedRoute.title} completed but target route not found: ${transitionConfig.nextStep}`
832
- );
833
- }
834
- }
835
-
836
- // Set step to END_ROUTE marker
837
- session = enterStep(session, END_ROUTE_ID, "Route completed");
838
- logger.debug(
839
- `[Agent] Route ${selectedRoute.title} completed. Entered END_ROUTE step.`
840
- );
841
-
842
- // Stream completion chunks
843
- for await (const chunk of stream) {
844
- // Update current session if we have one
845
- if (chunk.done && this.currentSession) {
846
- this.currentSession = session;
847
- }
848
-
849
- yield {
850
- delta: chunk.delta,
851
- accumulated: chunk.accumulated,
852
- done: chunk.done,
853
- session,
854
- toolCalls: undefined,
855
- isRouteComplete: true,
856
- metadata: chunk.metadata,
857
- structured: chunk.structured,
858
- };
859
- }
860
- } else {
861
- // Fallback: No routes defined, stream a simple response
862
- const fallbackPrompt = await this.responseEngine.buildFallbackPrompt({
863
- history,
864
- agentOptions: this.options,
865
- terms: this.terms,
866
- guidelines: this.guidelines,
867
- context: effectiveContext,
868
- session,
869
- });
870
-
871
- const stream = this.options.provider.generateMessageStream({
872
- prompt: fallbackPrompt,
873
- history,
874
- context: effectiveContext,
875
- signal,
876
- parameters: {
877
- jsonSchema: {
878
- type: "object",
879
- properties: {
880
- message: { type: "string" },
881
- },
882
- required: ["message"],
883
- additionalProperties: false,
884
- },
885
- schemaName: "fallback_stream_response",
886
- },
887
- });
888
-
889
- for await (const chunk of stream) {
890
- // Update current session if we have one
891
- if (chunk.done && this.currentSession) {
892
- this.currentSession = session;
893
- }
894
-
895
- yield {
896
- delta: chunk.delta,
897
- accumulated: chunk.accumulated,
898
- done: chunk.done,
899
- session, // Return updated session
900
- toolCalls: undefined,
901
- isRouteComplete: false,
902
- metadata: chunk.metadata,
903
- structured: chunk.structured,
904
- };
905
- }
906
- }
545
+ async *respondStream(params: RespondParams<TContext, TData>): AsyncGenerator<AgentResponseStreamChunk<TData>> {
546
+ // Delegate to ResponseModal
547
+ yield* this.responseModal.respondStream(params);
907
548
  }
908
549
 
909
550
  /**
910
551
  * Generate a response based on history and context
911
552
  */
912
- async respond<TData = Record<string, unknown>>(params: {
913
- history: History;
914
- step?: StepRef;
915
- session?: SessionState<TData>;
916
- contextOverride?: Partial<TContext>;
917
- signal?: AbortSignal;
918
- }): Promise<AgentResponse<TData>> {
919
- const { history: simpleHistory, contextOverride, signal } = params;
920
- const history = normalizeHistory(simpleHistory);
921
-
922
- // Get current context (may fetch from provider)
923
- let currentContext = await this.getContext();
924
-
925
- // Call beforeRespond hook if configured
926
- if (this.options.hooks?.beforeRespond && currentContext !== undefined) {
927
- currentContext = await this.options.hooks.beforeRespond(currentContext);
928
- // Update stored context with the result from beforeRespond
929
- this.context = currentContext;
930
- }
931
-
932
- // Merge context with override
933
- const effectiveContext = {
934
- ...(currentContext as Record<string, unknown>),
935
- ...(contextOverride as Record<string, unknown>),
936
- } as TContext;
937
-
938
- // Initialize or get session (use current session if available)
939
- let session =
940
- cloneDeep(params.session) ||
941
- cloneDeep(this.currentSession) ||
942
- (await this.session.getOrCreate());
943
-
944
- // PHASE 1: PREPARE - Execute prepare function if current step has one
945
- if (session.currentRoute && session.currentStep) {
946
- const currentRoute = this.routes.find(
947
- (r) => r.id === session.currentRoute?.id
948
- );
949
- if (currentRoute) {
950
- const currentStep = currentRoute.getStep(session.currentStep.id);
951
- if (currentStep?.prepare) {
952
- logger.debug(`[Agent] Executing prepare for step: ${currentStep.id}`);
953
- await this.executePrepareFinalize(
954
- currentStep.prepare,
955
- effectiveContext,
956
- session.data,
957
- currentRoute,
958
- currentStep
959
- );
960
- }
961
- }
962
- }
963
-
964
- // PHASE 2: ROUTING + STEP SELECTION - Determine which route and step to use (combined)
965
- let selectedRoute: Route<TContext, unknown> | undefined;
966
- let responseDirectives: string[] | undefined;
967
- let selectedStep: Step<TContext, unknown> | undefined;
968
- let isRouteComplete = false;
969
-
970
- // Check for pending transition from previous route completion
971
- if (session.pendingTransition) {
972
- const targetRoute = this.routes.find(
973
- (r) => r.id === session.pendingTransition?.targetRouteId
974
- );
975
-
976
- if (targetRoute) {
977
- logger.debug(
978
- `[Agent] Auto-transitioning from pending transition to route: ${targetRoute.title}`
979
- );
980
- // Clear pending transition and enter new route
981
- session = {
982
- ...session,
983
- pendingTransition: undefined,
984
- };
985
- session = enterRoute(session, targetRoute.id, targetRoute.title);
986
-
987
- // Merge initial data if available
988
- if (targetRoute.initialData) {
989
- session = mergeCollected(session, targetRoute.initialData);
990
- }
991
-
992
- selectedRoute = targetRoute;
993
- } else {
994
- logger.warn(
995
- `[Agent] Pending transition target route not found: ${session.pendingTransition.targetRouteId}`
996
- );
997
- // Clear invalid transition
998
- session = {
999
- ...session,
1000
- pendingTransition: undefined,
1001
- };
1002
- }
1003
- }
1004
-
1005
- // If no pending transition or transition handled, do normal routing
1006
- if (this.routes.length > 0 && !selectedRoute) {
1007
- const orchestration = await this.routingEngine.decideRouteAndStep({
1008
- routes: this.routes,
1009
- session,
1010
- history,
1011
- agentOptions: this.options,
1012
- provider: this.options.provider,
1013
- context: effectiveContext,
1014
- signal,
1015
- });
1016
-
1017
- selectedRoute = orchestration.selectedRoute;
1018
- selectedStep = orchestration.selectedStep;
1019
- responseDirectives = orchestration.responseDirectives;
1020
- session = orchestration.session;
1021
- isRouteComplete = orchestration.isRouteComplete || false;
1022
-
1023
- // Log if route is complete
1024
- if (isRouteComplete) {
1025
- logger.debug(
1026
- `[Agent] Route complete: all required data collected, END_ROUTE reached`
1027
- );
1028
- }
1029
- }
1030
-
1031
- // PHASE 3: DETERMINE NEXT STEP - Use step from combined decision or get initial step
1032
- let message: string;
1033
- let toolCalls:
1034
- | Array<{ toolName: string; arguments: Record<string, unknown> }>
1035
- | undefined = undefined;
1036
- let responsePrompt: string;
1037
- let availableTools: Tool<TContext, unknown[], unknown, unknown>[] = [];
1038
- let responseSchema: StructuredSchema | undefined;
1039
- let nextStep: Step<TContext, unknown> | undefined;
1040
-
1041
- // Get last user message (needed for both route and completion handling)
1042
- const lastUserMessage = getLastMessageFromHistory(history);
1043
-
1044
- if (selectedRoute && !isRouteComplete) {
1045
- // If we have a selected step from the combined routing decision, use it
1046
- if (selectedStep) {
1047
- nextStep = selectedStep;
1048
- } else {
1049
- // New route or no step selected - get initial step or first valid step
1050
- const candidates = this.routingEngine.getCandidateSteps(
1051
- selectedRoute,
1052
- undefined,
1053
- session.data || {}
1054
- );
1055
- if (candidates.length > 0) {
1056
- nextStep = candidates[0].step;
1057
- logger.debug(
1058
- `[Agent] Using first valid step: ${nextStep.id} for new route`
1059
- );
1060
- } else {
1061
- // Fallback to initial step even if it should be skipped
1062
- nextStep = selectedRoute.initialStep;
1063
- logger.warn(
1064
- `[Agent] No valid steps found, using initial step: ${nextStep.id}`
1065
- );
1066
- }
1067
- }
1068
-
1069
- // Update session with next step
1070
- session = enterStep(session, nextStep.id, nextStep.description);
1071
- logger.debug(`[Agent] Entered step: ${nextStep.id}`);
1072
-
1073
- // PHASE 4: RESPONSE GENERATION - Generate message using selected route and step
1074
- // Get last user message
1075
- const lastUserMessage = getLastMessageFromHistory(history);
1076
-
1077
- // Build response schema for this route (with collect fields from step)
1078
- responseSchema = this.responseEngine.responseSchemaForRoute(
1079
- selectedRoute,
1080
- nextStep
1081
- );
1082
-
1083
- // Build response prompt
1084
- responsePrompt = await this.responseEngine.buildResponsePrompt({
1085
- route: selectedRoute,
1086
- currentStep: nextStep,
1087
- rules: selectedRoute.getRules(),
1088
- prohibitions: selectedRoute.getProhibitions(),
1089
- directives: responseDirectives,
1090
- history,
1091
- lastMessage: lastUserMessage,
1092
- agentOptions: this.options,
1093
- // Combine agent and route properties according to the specified logic
1094
- combinedGuidelines: [
1095
- ...this.getGuidelines(),
1096
- ...selectedRoute.getGuidelines(),
1097
- ],
1098
- combinedTerms: this.mergeTerms(
1099
- this.getTerms(),
1100
- selectedRoute.getTerms()
1101
- ),
1102
- context: effectiveContext,
1103
- session,
1104
- });
1105
-
1106
- // Collect available tools for AI
1107
- availableTools = this.collectAvailableTools(
1108
- selectedRoute,
1109
- nextStep
1110
- ) as Tool<TContext, unknown[], unknown, unknown>[];
1111
- } else {
1112
- // No route selected - generate basic response without route context
1113
- logger.debug(`[Agent] No route selected, generating basic response`);
1114
-
1115
- // Build basic response prompt without route context
1116
- responsePrompt = await this.responseEngine.buildFallbackPrompt({
1117
- history,
1118
- agentOptions: this.options,
1119
- terms: this.getTerms(),
1120
- guidelines: this.getGuidelines(),
1121
- context: effectiveContext,
1122
- session,
1123
- });
1124
-
1125
- // Use agent-level tools only
1126
- availableTools = [...this.tools];
1127
- responseSchema = undefined;
1128
- }
1129
-
1130
- // Generate message using AI provider (common for both route and no-route cases)
1131
- const result = await this.options.provider.generateMessage({
1132
- prompt: responsePrompt,
1133
- history,
1134
- context: effectiveContext,
1135
- tools: availableTools,
1136
- signal,
1137
- parameters: responseSchema
1138
- ? {
1139
- jsonSchema: responseSchema,
1140
- schemaName: "response_output",
1141
- }
1142
- : undefined,
1143
- });
1144
-
1145
- message = result.structured?.message || result.message;
1146
-
1147
- // Process dynamic tool calls from AI response (common for both route and no-route cases)
1148
- if (result.structured?.toolCalls) {
1149
- toolCalls = result.structured.toolCalls;
1150
-
1151
- // Execute dynamic tool calls
1152
- if (toolCalls.length > 0) {
1153
- logger.debug(
1154
- `[Agent] Executing ${toolCalls.length} dynamic tool calls`
1155
- );
1156
-
1157
- for (const toolCall of toolCalls) {
1158
- const tool = this.findAvailableTool(toolCall.toolName, selectedRoute);
1159
- if (!tool) {
1160
- logger.warn(`[Agent] Tool not found: ${toolCall.toolName}`);
1161
- continue;
1162
- }
1163
-
1164
- const toolExecutor = new ToolExecutor<TContext, unknown>();
1165
- const toolResult = await toolExecutor.executeTool({
1166
- tool: tool,
1167
- context: effectiveContext,
1168
- updateContext: this.updateContext.bind(this),
1169
- history,
1170
- data: session.data,
1171
- toolArguments: toolCall.arguments,
1172
- });
1173
-
1174
- // Update context with tool results
1175
- if (toolResult.contextUpdate) {
1176
- await this.updateContext(
1177
- toolResult.contextUpdate as Partial<TContext>
1178
- );
1179
- }
1180
-
1181
- // Update collected data with tool results
1182
- if (toolResult.dataUpdate) {
1183
- session = await this.updateData(session, toolResult.dataUpdate);
1184
- logger.debug(
1185
- `[Agent] Tool updated collected data:`,
1186
- toolResult.dataUpdate
1187
- );
1188
- }
1189
-
1190
- logger.debug(
1191
- `[Agent] Executed dynamic tool: ${toolResult.toolName} (success: ${toolResult.success})`
1192
- );
1193
- }
1194
- }
1195
- }
1196
-
1197
- // TOOL LOOP: Allow AI to make follow-up tool calls after initial tool execution
1198
- const MAX_TOOL_LOOPS = 5;
1199
- let toolLoopCount = 0;
1200
- let hasToolCalls = toolCalls && toolCalls.length > 0;
1201
-
1202
- while (hasToolCalls && toolLoopCount < MAX_TOOL_LOOPS) {
1203
- toolLoopCount++;
1204
- logger.debug(
1205
- `[Agent] Starting tool loop ${toolLoopCount}/${MAX_TOOL_LOOPS}`
1206
- );
1207
-
1208
- // Add tool execution results to history so AI knows what happened
1209
- const toolResultsEvents: Event[] = [];
1210
- for (const toolCall of toolCalls || []) {
1211
- const tool = this.findAvailableTool(toolCall.toolName, selectedRoute);
1212
- if (tool) {
1213
- toolResultsEvents.push({
1214
- kind: EventKind.TOOL,
1215
- source: MessageRole.AGENT,
1216
- timestamp: new Date().toISOString(),
1217
- data: {
1218
- tool_calls: [
1219
- {
1220
- tool_id: toolCall.toolName,
1221
- arguments: toolCall.arguments,
1222
- result: {
1223
- data: "Tool executed successfully",
1224
- },
1225
- },
1226
- ],
1227
- },
1228
- });
1229
- }
1230
- }
1231
-
1232
- // Create updated history with tool results
1233
- const updatedHistory = [...history, ...toolResultsEvents];
1234
-
1235
- // Make follow-up AI call to see if more tools are needed
1236
- const followUpResult = await this.options.provider.generateMessage({
1237
- prompt: responsePrompt,
1238
- history: updatedHistory,
1239
- context: effectiveContext,
1240
- tools: availableTools,
1241
- parameters: {
1242
- jsonSchema: responseSchema as StructuredSchema,
1243
- schemaName: "tool_followup",
1244
- },
1245
- signal,
1246
- });
1247
-
1248
- // Check if follow-up call has more tool calls
1249
- const followUpToolCalls = followUpResult.structured?.toolCalls;
1250
- hasToolCalls = followUpToolCalls && followUpToolCalls.length > 0;
1251
-
1252
- if (hasToolCalls) {
1253
- logger.debug(
1254
- `[Agent] Follow-up call produced ${
1255
- followUpToolCalls!.length
1256
- } additional tool calls`
1257
- );
1258
-
1259
- // Execute the follow-up tool calls
1260
- for (const toolCall of followUpToolCalls!) {
1261
- const tool = this.findAvailableTool(toolCall.toolName, selectedRoute);
1262
- if (!tool) {
1263
- logger.warn(
1264
- `[Agent] Tool not found in follow-up: ${toolCall.toolName}`
1265
- );
1266
- continue;
1267
- }
1268
-
1269
- const toolExecutor = new ToolExecutor<TContext, unknown>();
1270
- const toolResult = await toolExecutor.executeTool({
1271
- tool: tool,
1272
- context: effectiveContext,
1273
- updateContext: this.updateContext.bind(this),
1274
- history: updatedHistory,
1275
- data: session.data,
1276
- toolArguments: toolCall.arguments,
1277
- });
1278
-
1279
- // Update context with follow-up tool results
1280
- if (toolResult.contextUpdate) {
1281
- await this.updateContext(
1282
- toolResult.contextUpdate as Partial<TContext>
1283
- );
1284
- }
1285
-
1286
- if (toolResult.dataUpdate) {
1287
- session = await this.updateData(session, toolResult.dataUpdate);
1288
- logger.debug(
1289
- `[Agent] Follow-up tool updated collected data:`,
1290
- toolResult.dataUpdate
1291
- );
1292
- }
1293
-
1294
- logger.debug(
1295
- `[Agent] Executed follow-up tool: ${toolResult.toolName} (success: ${toolResult.success})`
1296
- );
1297
- }
1298
-
1299
- // Update toolCalls for next iteration or final response
1300
- toolCalls = followUpToolCalls;
1301
- } else {
1302
- logger.debug(
1303
- `[Agent] Tool loop completed after ${toolLoopCount} iterations`
1304
- );
1305
- // Update final message and toolCalls from follow-up result if no more tools
1306
- message = followUpResult.structured?.message || followUpResult.message;
1307
- toolCalls = followUpToolCalls || [];
1308
- break;
1309
- }
1310
- }
1311
-
1312
- if (toolLoopCount >= MAX_TOOL_LOOPS) {
1313
- logger.warn(
1314
- `[Agent] Tool loop limit reached (${MAX_TOOL_LOOPS}), stopping`
1315
- );
1316
- }
1317
-
1318
- // Extract collected data from final response (only for route-based interactions)
1319
- if (selectedRoute && result.structured && nextStep?.collect) {
1320
- const collectedData: Record<string, unknown> = {};
1321
- // The structured response includes both base fields and collected extraction fields
1322
- const structuredData = result.structured as AgentStructuredResponse &
1323
- Record<string, unknown>;
1324
-
1325
- for (const field of nextStep.collect) {
1326
- if (field in structuredData) {
1327
- collectedData[field] = structuredData[field];
1328
- }
1329
- }
553
+ async respond(params: RespondParams<TContext, TData>): Promise<AgentResponse<TData>> {
554
+ // Delegate to ResponseModal
555
+ return this.responseModal.respond(params);
556
+ }
1330
557
 
1331
- // Merge collected data into session
1332
- if (Object.keys(collectedData).length > 0) {
1333
- session = await this.updateData(session, collectedData);
1334
- logger.debug(`[Agent] Collected data:`, collectedData);
1335
- }
1336
- }
558
+ /**
559
+ * Get all routes
560
+ */
561
+ getRoutes(): Route<TContext, TData>[] {
562
+ return [...this.routes];
563
+ }
1337
564
 
1338
- // Extract any additional data from structured response
1339
- if (
1340
- result.structured &&
1341
- typeof result.structured === "object" &&
1342
- "contextUpdate" in result.structured
1343
- ) {
1344
- await this.updateContext(
1345
- (result.structured as { contextUpdate?: Partial<TContext> })
1346
- .contextUpdate as Partial<TContext>
1347
- );
1348
- }
565
+ /**
566
+ * Get agent options
567
+ * @internal Used by ResponseModal
568
+ */
569
+ getAgentOptions(): AgentOptions<TContext, TData> {
570
+ return this.options;
571
+ }
1349
572
 
1350
- // Handle route completion if route is complete
1351
- if (isRouteComplete) {
1352
- // Route is complete - generate completion message then check for onComplete transition
1353
-
1354
- // Get endStep spec from route
1355
- const endStepSpec = selectedRoute!.endStepSpec;
1356
-
1357
- // Create a temporary step for completion message generation using endStep configuration
1358
- const completionStep = new Step<TContext, unknown>(selectedRoute!.id, {
1359
- description: endStepSpec.description,
1360
- id: endStepSpec.id || END_ROUTE_ID,
1361
- collect: endStepSpec.collect,
1362
- requires: endStepSpec.requires,
1363
- prompt:
1364
- endStepSpec.prompt ||
1365
- "Summarize what was accomplished and confirm completion based on the conversation history and collected data",
1366
- });
573
+ /**
574
+ * Get routing engine
575
+ * @internal Used by ResponseModal
576
+ */
577
+ getRoutingEngine(): RoutingEngine<TContext, TData> {
578
+ return this.routingEngine;
579
+ }
1367
580
 
1368
- // Build response schema for completion
1369
- const responseSchema = this.responseEngine.responseSchemaForRoute(
1370
- selectedRoute!,
1371
- completionStep
1372
- );
1373
- const templateContext = {
1374
- context: effectiveContext,
1375
- session,
1376
- history,
1377
- };
1378
- if (!selectedRoute) {
1379
- throw new Error("Selected route is not defined");
1380
- }
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
+ }
1381
588
 
1382
- // Build completion response prompt
1383
- const completionPrompt = await this.responseEngine.buildResponsePrompt({
1384
- route: selectedRoute,
1385
- currentStep: completionStep,
1386
- rules: selectedRoute.getRules(),
1387
- prohibitions: selectedRoute.getProhibitions(),
1388
- directives: undefined, // No directives for completion
1389
- history,
1390
- lastMessage: lastUserMessage,
1391
- agentOptions: this.options,
1392
- // Combine agent and route properties according to the specified logic
1393
- combinedGuidelines: [
1394
- ...this.getGuidelines(),
1395
- ...selectedRoute.getGuidelines(),
1396
- ],
1397
- combinedTerms: this.mergeTerms(
1398
- this.getTerms(),
1399
- selectedRoute.getTerms()
1400
- ),
1401
- context: effectiveContext,
1402
- session,
1403
- });
1404
589
 
1405
- // Generate completion message using AI provider
1406
- const completionResult = await this.options.provider.generateMessage({
1407
- prompt: completionPrompt,
1408
- history,
1409
- context: effectiveContext,
1410
- signal,
1411
- parameters: {
1412
- jsonSchema: responseSchema,
1413
- schemaName: "completion_message",
1414
- },
1415
- });
1416
590
 
1417
- message =
1418
- completionResult.structured?.message || completionResult.message;
1419
- logger.debug(
1420
- `[Agent] Generated completion message for route: ${selectedRoute.title}`
1421
- );
591
+ /**
592
+ * Get all terms
593
+ */
594
+ getTerms(): Term<TContext, TData>[] {
595
+ return [...this.terms];
596
+ }
1422
597
 
1423
- // Check for onComplete transition
1424
- const transitionConfig = await selectedRoute.evaluateOnComplete(
1425
- { data: session.data },
1426
- effectiveContext
1427
- );
598
+ /**
599
+ * Get all tools
600
+ */
601
+ getTools(): Tool<TContext, TData, unknown[], unknown>[] {
602
+ return [...this.tools];
603
+ }
1428
604
 
1429
- if (transitionConfig) {
1430
- // Find target route by ID or title
1431
- const targetRoute = this.routes.find(
1432
- (r) =>
1433
- r.id === transitionConfig.nextStep ||
1434
- r.title === transitionConfig.nextStep
1435
- );
1436
605
 
1437
- if (targetRoute) {
1438
- const renderedCondition = await render(
1439
- transitionConfig.condition,
1440
- templateContext
1441
- );
1442
- // Set pending transition in session
1443
- session = {
1444
- ...session,
1445
- pendingTransition: {
1446
- targetRouteId: targetRoute.id,
1447
- condition: renderedCondition,
1448
- reason: "route_complete",
1449
- },
1450
- };
1451
- logger.debug(
1452
- `[Agent] Route ${selectedRoute.title} completed with pending transition to: ${targetRoute.title}`
1453
- );
1454
- } else {
1455
- logger.warn(
1456
- `[Agent] Route ${selectedRoute.title} completed but target route not found: ${transitionConfig.nextStep}`
1457
- );
1458
- }
1459
- }
1460
606
 
1461
- // Set step to END_ROUTE marker
1462
- session = enterStep(session, END_ROUTE_ID, "Route completed");
1463
- logger.debug(
1464
- `[Agent] Route ${selectedRoute.title} completed. Entered END_ROUTE step.`
1465
- );
1466
- } else {
1467
- // Fallback: No routes defined, generate a simple response
1468
- const fallbackPrompt = await this.responseEngine.buildFallbackPrompt({
1469
- history,
1470
- agentOptions: this.options,
1471
- terms: this.terms,
1472
- guidelines: this.guidelines,
1473
- context: effectiveContext,
1474
- session,
1475
- });
1476
607
 
1477
- const result = await this.options.provider.generateMessage({
1478
- prompt: fallbackPrompt,
1479
- history,
1480
- context: effectiveContext,
1481
- signal,
1482
- parameters: {
1483
- jsonSchema: {
1484
- type: "object",
1485
- properties: {
1486
- message: { type: "string" },
1487
- },
1488
- required: ["message"],
1489
- additionalProperties: false,
1490
- },
1491
- schemaName: "fallback_response",
1492
- },
1493
- });
1494
608
 
1495
- message = result.structured?.message || result.message;
1496
- }
1497
609
 
1498
- // Auto-save session step to persistence if configured
1499
- if (
1500
- this.persistenceManager &&
1501
- session.id &&
1502
- this.options.persistence?.autoSave !== false
1503
- ) {
1504
- await this.persistenceManager.saveSessionState(session.id, session);
1505
- logger.debug(
1506
- `[Agent] Auto-saved session step to persistence: ${session.id}`
1507
- );
1508
- }
1509
610
 
1510
- // Execute finalize function
1511
- if (session.currentRoute && session.currentStep) {
1512
- const currentRoute = this.routes.find(
1513
- (r) => r.id === session.currentRoute?.id
1514
- );
1515
- if (currentRoute) {
1516
- const currentStep = currentRoute.getStep(session.currentStep.id);
1517
- if (currentStep?.finalize) {
1518
- logger.debug(
1519
- `[Agent] Executing finalize for step: ${currentStep.id}`
1520
- );
1521
- await this.executePrepareFinalize(
1522
- currentStep.finalize,
1523
- effectiveContext,
1524
- session.data,
1525
- currentRoute,
1526
- currentStep
1527
- );
1528
- }
1529
- }
1530
- }
1531
-
1532
- // Update current session if we have one
1533
- if (this.currentSession) {
1534
- this.currentSession = session;
1535
- }
1536
-
1537
- return {
1538
- message,
1539
- session, // Return updated session with route/step info
1540
- toolCalls,
1541
- isRouteComplete, // Indicates if the route has reached END_ROUTE with all data collected
1542
- };
611
+ /**
612
+ * Get all guidelines
613
+ */
614
+ getGuidelines(): Guideline<TContext, TData>[] {
615
+ return [...this.guidelines];
1543
616
  }
1544
617
 
1545
618
  /**
1546
- * Get all routes
619
+ * Get the agent's knowledge base
1547
620
  */
1548
- getRoutes(): Route<TContext, unknown>[] {
1549
- return [...this.routes];
621
+ getKnowledgeBase(): Record<string, unknown> {
622
+ return { ...this.knowledgeBase };
1550
623
  }
1551
624
 
625
+
626
+
1552
627
  /**
1553
- * Get all terms
628
+ * Get the persistence manager (if configured)
1554
629
  */
1555
- getTerms(): Term<TContext>[] {
1556
- return [...this.terms];
630
+ getPersistenceManager(): PersistenceManager<TData> | undefined {
631
+ return this.persistenceManager;
1557
632
  }
1558
633
 
1559
634
  /**
1560
- * Get all tools
635
+ * Check if persistence is enabled
1561
636
  */
1562
- getTools(): Tool<TContext, unknown[], unknown, unknown>[] {
1563
- return [...this.tools];
637
+ hasPersistence(): boolean {
638
+ return this.persistenceManager !== undefined;
1564
639
  }
1565
640
 
1566
641
  /**
1567
- * Find an available tool by name for the given route
1568
- * Route-level tools take precedence over agent-level tools
1569
- * @private
642
+ * Set the current session for convenience methods
643
+ * @param session - Session step to use for subsequent calls
1570
644
  */
1571
- private findAvailableTool(
1572
- toolName: string,
1573
- route?: Route<TContext, unknown>
1574
- ): Tool<TContext, unknown[], unknown, unknown> | undefined {
1575
- // Check route-level tools first (if route provided)
1576
- if (route) {
1577
- const routeTool = route
1578
- .getTools()
1579
- .find((tool) => tool.id === toolName || tool.name === toolName);
1580
- if (routeTool) return routeTool;
1581
- }
1582
-
1583
- // Fall back to agent-level tools
1584
- return this.tools.find(
1585
- (tool) => tool.id === toolName || tool.name === toolName
1586
- );
645
+ setCurrentSession(session: SessionState): void {
646
+ this.currentSession = session;
1587
647
  }
1588
648
 
1589
649
  /**
1590
- * Collect all available tools for the given route and step context
1591
- * @private
650
+ * Get the current session (if set)
1592
651
  */
1593
- private collectAvailableTools(
1594
- route?: Route<TContext, unknown>,
1595
- step?: Step<TContext, unknown>
1596
- ): Array<{
1597
- id: string;
1598
- name: string;
1599
- description?: string;
1600
- parameters?: unknown;
1601
- }> {
1602
- const availableTools = new Map<
1603
- string,
1604
- Tool<TContext, unknown[], unknown, unknown>
1605
- >();
1606
-
1607
- // Add agent-level tools
1608
- this.tools.forEach((tool) => {
1609
- availableTools.set(tool.id, tool);
1610
- });
1611
-
1612
- // Add route-level tools (these take precedence)
1613
- if (route) {
1614
- route.getTools().forEach((tool) => {
1615
- availableTools.set(tool.id, tool);
1616
- });
1617
- }
1618
-
1619
- // Filter by step-level allowed tools if specified
1620
- if (step?.tools) {
1621
- const allowedToolIds = new Set<string>();
1622
- const stepTools: Tool<TContext, unknown[], unknown, unknown>[] = [];
1623
-
1624
- for (const toolRef of step.tools) {
1625
- if (typeof toolRef === "string") {
1626
- // Reference to registered tool
1627
- allowedToolIds.add(toolRef);
1628
- } else {
1629
- // Inline tool definition
1630
- if (toolRef.id) {
1631
- allowedToolIds.add(toolRef.id);
1632
- stepTools.push(toolRef);
1633
- }
1634
- }
1635
- }
1636
-
1637
- // If step specifies tools, only include those
1638
- if (allowedToolIds.size > 0) {
1639
- const filteredTools = new Map<
1640
- string,
1641
- Tool<TContext, unknown[], unknown, unknown>
1642
- >();
1643
- for (const toolId of allowedToolIds) {
1644
- const tool = availableTools.get(toolId);
1645
- if (tool) {
1646
- filteredTools.set(toolId, tool);
1647
- }
1648
- }
1649
- // Add inline tools
1650
- stepTools.forEach((tool) => {
1651
- if (tool.id) {
1652
- filteredTools.set(tool.id, tool);
1653
- }
1654
- });
1655
- availableTools.clear();
1656
- filteredTools.forEach((tool, id) => availableTools.set(id, tool));
1657
- }
1658
- }
1659
-
1660
- // Convert to the format expected by AI providers
1661
- return Array.from(availableTools.values()).map((tool) => ({
1662
- id: tool.id,
1663
- name: tool.name || tool.id,
1664
- description: tool.description,
1665
- parameters: tool.parameters,
1666
- }));
652
+ getCurrentSession(): SessionState | undefined {
653
+ return this.currentSession;
1667
654
  }
1668
655
 
1669
656
  /**
1670
657
  * Execute a prepare or finalize function/tool
1671
- * @private
658
+ * @internal Used by ResponseModal
1672
659
  */
1673
- private async executePrepareFinalize(
660
+ async executePrepareFinalize(
1674
661
  prepareOrFinalize:
1675
662
  | string
1676
- | Tool<TContext, unknown[], unknown, unknown>
1677
- | ((context: TContext, data?: Partial<unknown>) => void | Promise<void>)
663
+ | Tool<TContext, TData, unknown[], unknown>
664
+ | ((context: TContext, data?: Partial<TData>) => void | Promise<void>)
1678
665
  | undefined,
1679
666
  context: TContext,
1680
- data?: Partial<unknown>,
1681
- route?: Route<TContext, unknown>,
1682
- step?: Step<TContext, unknown>
667
+ data?: Partial<TData>,
668
+ route?: Route<TContext, TData>,
669
+ step?: Step<TContext, TData>
1683
670
  ): Promise<void> {
1684
671
  if (!prepareOrFinalize) return;
1685
672
 
@@ -1688,14 +675,11 @@ export class Agent<TContext = unknown> {
1688
675
  await prepareOrFinalize(context, data);
1689
676
  } else {
1690
677
  // It's a tool reference - find and execute the tool
1691
- let tool: Tool<TContext, unknown[], unknown, unknown> | undefined;
678
+ let tool: Tool<TContext, TData, unknown[], unknown> | undefined;
1692
679
 
1693
680
  if (typeof prepareOrFinalize === "string") {
1694
681
  // Tool ID - find it in available tools
1695
- const availableTools = new Map<
1696
- string,
1697
- Tool<TContext, unknown[], unknown, unknown>
1698
- >();
682
+ const availableTools = new Map<string, Tool<TContext, TData, unknown[], unknown>>();
1699
683
 
1700
684
  // Add agent-level tools
1701
685
  this.tools.forEach((t) => {
@@ -1727,11 +711,12 @@ export class Agent<TContext = unknown> {
1727
711
  }
1728
712
 
1729
713
  if (tool) {
1730
- const toolExecutor = new ToolExecutor<TContext, unknown>();
714
+ const toolExecutor = new ToolExecutor<TContext, TData>();
1731
715
  const result = await toolExecutor.executeTool({
1732
716
  tool,
1733
717
  context,
1734
718
  updateContext: this.updateContext.bind(this),
719
+ updateData: this.updateCollectedData.bind(this),
1735
720
  history: [], // Empty history for prepare/finalize
1736
721
  data,
1737
722
  });
@@ -1744,86 +729,15 @@ export class Agent<TContext = unknown> {
1744
729
  }
1745
730
  } else {
1746
731
  logger.warn(
1747
- `[Agent] Tool not found for prepare/finalize: ${
1748
- typeof prepareOrFinalize === "string"
1749
- ? prepareOrFinalize
1750
- : "inline tool"
732
+ `[Agent] Tool not found for prepare/finalize: ${typeof prepareOrFinalize === "string"
733
+ ? prepareOrFinalize
734
+ : "inline tool"
1751
735
  }`
1752
736
  );
1753
737
  }
1754
738
  }
1755
739
  }
1756
740
 
1757
- /**
1758
- * Get all guidelines
1759
- */
1760
- getGuidelines(): Guideline<TContext>[] {
1761
- return [...this.guidelines];
1762
- }
1763
-
1764
- /**
1765
- * Get the agent's knowledge base
1766
- */
1767
- getKnowledgeBase(): Record<string, unknown> {
1768
- return { ...this.knowledgeBase };
1769
- }
1770
-
1771
- /**
1772
- * Merge terms with route-specific taking precedence on conflicts
1773
- * @private
1774
- */
1775
- private mergeTerms(
1776
- agentTerms: Term<TContext>[],
1777
- routeTerms: Term<TContext>[]
1778
- ): Term<TContext>[] {
1779
- const merged = new Map<string, Term<TContext>>();
1780
-
1781
- // Add agent terms first
1782
- agentTerms.forEach((term) => {
1783
- const name =
1784
- typeof term.name === "string" ? term.name : term.name.toString();
1785
- merged.set(name, term);
1786
- });
1787
-
1788
- // Add route terms (these take precedence)
1789
- routeTerms.forEach((term) => {
1790
- const name =
1791
- typeof term.name === "string" ? term.name : term.name.toString();
1792
- merged.set(name, term);
1793
- });
1794
-
1795
- return Array.from(merged.values());
1796
- }
1797
-
1798
- /**
1799
- * Get the persistence manager (if configured)
1800
- */
1801
- getPersistenceManager(): PersistenceManager | undefined {
1802
- return this.persistenceManager;
1803
- }
1804
-
1805
- /**
1806
- * Check if persistence is enabled
1807
- */
1808
- hasPersistence(): boolean {
1809
- return this.persistenceManager !== undefined;
1810
- }
1811
-
1812
- /**
1813
- * Set the current session for convenience methods
1814
- * @param session - Session step to use for subsequent calls
1815
- */
1816
- setCurrentSession(session: SessionState): void {
1817
- this.currentSession = session;
1818
- }
1819
-
1820
- /**
1821
- * Get the current session (if set)
1822
- */
1823
- getCurrentSession(): SessionState | undefined {
1824
- return this.currentSession;
1825
- }
1826
-
1827
741
  /**
1828
742
  * Clear the current session
1829
743
  */
@@ -1832,21 +746,20 @@ export class Agent<TContext = unknown> {
1832
746
  }
1833
747
 
1834
748
  /**
1835
- * Get collected data from current session
749
+ * Get collected data from current session or agent-level collected data
1836
750
  * @param routeId - Optional route ID to get data for (uses current route if not provided)
1837
- * @returns The collected data from the current session
751
+ * @returns The collected data from the current session or agent-level data
1838
752
  */
1839
- getData<TData = unknown>(routeId?: string): Partial<TData> {
1840
- if (!this.currentSession) {
1841
- return {} as Partial<TData>;
1842
- }
1843
- if (routeId) {
1844
- return (
1845
- (this.currentSession.dataByRoute?.[routeId] as Partial<TData>) ||
1846
- ({} as Partial<TData>)
1847
- );
753
+ getData(): Partial<TData> {
754
+ // If we have a current session, use session data
755
+ if (this.currentSession) {
756
+ // With agent-level data, all routes share the same data structure
757
+ // No need for route-specific data access
758
+ return (this.currentSession.data) || {};
1848
759
  }
1849
- return (this.currentSession.data as Partial<TData>) || {};
760
+
761
+ // Otherwise, return agent-level collected data
762
+ return this.getCollectedData();
1850
763
  }
1851
764
 
1852
765
  /**
@@ -1868,10 +781,10 @@ export class Agent<TContext = unknown> {
1868
781
  */
1869
782
  async nextStepRoute(
1870
783
  routeIdOrTitle: string,
1871
- session?: SessionState,
1872
- condition?: Template<TContext, unknown>,
784
+ session?: SessionState<TData>,
785
+ condition?: Template<TContext, TData>,
1873
786
  history?: Event[]
1874
- ): Promise<SessionState> {
787
+ ): Promise<SessionState<TData>> {
1875
788
  const targetSession = session || this.currentSession;
1876
789
 
1877
790
  if (!targetSession) {
@@ -1900,12 +813,12 @@ export class Agent<TContext = unknown> {
1900
813
  };
1901
814
  const renderedCondition = await render(condition, templateContext);
1902
815
 
1903
- const updatedSession: SessionState = {
816
+ const updatedSession: SessionState<TData> = {
1904
817
  ...targetSession,
1905
818
  pendingTransition: {
1906
819
  targetRouteId: targetRoute.id,
1907
820
  condition: renderedCondition,
1908
- reason: "manual",
821
+ reason: "route_complete",
1909
822
  },
1910
823
  };
1911
824
 
@@ -1915,7 +828,7 @@ export class Agent<TContext = unknown> {
1915
828
  }
1916
829
 
1917
830
  logger.debug(
1918
- `[Agent] Set pending manual transition to route: ${targetRoute.title}`
831
+ `[Agent] Set pending transition to route: ${targetRoute.title}`
1919
832
  );
1920
833
 
1921
834
  return updatedSession;
@@ -1925,43 +838,27 @@ export class Agent<TContext = unknown> {
1925
838
  * Simplified respond method using SessionManager
1926
839
  * Automatically manages conversation history through the session
1927
840
  */
1928
- async chat<TData = Record<string, unknown>>(
841
+ async chat(
1929
842
  message?: string,
1930
- options?: {
1931
- history?: History; // Optional: override session history for this response
1932
- contextOverride?: Partial<TContext>;
1933
- signal?: AbortSignal;
1934
- }
843
+ options?: GenerateOptions<TContext>
1935
844
  ): Promise<AgentResponse<TData>> {
1936
- // Determine which history to use
1937
- let history: History;
1938
- if (options?.history) {
1939
- // Use provided history for this response only
1940
- history = options.history;
1941
- } else {
1942
- // Add user message to session history if provided
1943
- if (message) {
1944
- await this.session.addMessage("user", message);
1945
- }
1946
- history = this.session.getHistory();
1947
- }
1948
-
1949
- // Get or create session
1950
- const session = await this.session.getOrCreate();
845
+ // Delegate to ResponseModal.generate()
846
+ return this.responseModal.generate(message, options);
847
+ }
1951
848
 
1952
- // Use existing respond method with session-managed history
1953
- const result = await this.respond({
1954
- history,
1955
- 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,
1956
860
  contextOverride: options?.contextOverride,
1957
861
  signal: options?.signal,
1958
862
  });
1959
-
1960
- // Add agent response to session history (only if not using override history)
1961
- if (!options?.history) {
1962
- await this.session.addMessage("assistant", result.message);
1963
- }
1964
-
1965
- return result as AgentResponse<TData>;
1966
863
  }
1967
- }
864
+ }