@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
@@ -1,29 +1,47 @@
1
1
  /**
2
2
  * Core Agent implementation
3
3
  */
4
- import { EventKind, MessageRole } from "../types/history";
5
- import { enterRoute, enterStep, mergeCollected, logger, LoggerLevel, render, getLastMessageFromHistory, normalizeHistory, cloneDeep, } from "../utils";
4
+ import { mergeCollected, logger, LoggerLevel, render, } from "../utils";
6
5
  import { Route } from "./Route";
7
- import { Step } from "./Step";
8
6
  import { PersistenceManager } from "./PersistenceManager";
9
7
  import { SessionManager } from "./SessionManager";
10
8
  import { RoutingEngine } from "./RoutingEngine";
11
- import { ResponseEngine } from "./ResponseEngine";
12
9
  import { ToolExecutor } from "./ToolExecutor";
13
- import { ResponsePipeline } from "./ResponsePipeline";
14
- import { END_ROUTE_ID } from "../constants";
10
+ import { ResponseModal } from "./ResponseModal";
15
11
  /**
16
- * Main Agent class with generic context support
12
+ * Error thrown when data validation fails
17
13
  */
14
+ class DataValidationError extends Error {
15
+ constructor(errors, message) {
16
+ super(message || "Data validation failed");
17
+ this.errors = errors;
18
+ this.name = "DataValidationError";
19
+ }
20
+ }
21
+ /**
22
+ * Error thrown when route configuration is invalid
23
+ */
24
+ class RouteConfigurationError extends Error {
25
+ constructor(routeTitle, invalidFields, message) {
26
+ super(message || `Route configuration error in '${routeTitle}'`);
27
+ this.routeTitle = routeTitle;
28
+ this.invalidFields = invalidFields;
29
+ this.name = "RouteConfigurationError";
30
+ }
31
+ }
32
+ /**
33
+ * Main Agent class with generic context and data support
34
+ */
35
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
18
36
  export class Agent {
19
37
  constructor(options) {
20
38
  this.options = options;
21
39
  this.terms = [];
22
40
  this.guidelines = [];
23
41
  this.tools = [];
24
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
25
42
  this.routes = [];
26
43
  this.knowledgeBase = {};
44
+ this.collectedData = {};
27
45
  // Set log level based on debug option
28
46
  if (options.debug) {
29
47
  logger.setLevel(LoggerLevel.DEBUG);
@@ -32,26 +50,60 @@ export class Agent {
32
50
  if (options.context !== undefined && options.contextProvider) {
33
51
  throw new Error("Cannot provide both 'context' and 'contextProvider'. Choose one.");
34
52
  }
53
+ // Initialize and validate agent-level schema if provided
54
+ if (options.schema) {
55
+ this.schema = options.schema;
56
+ this.validateSchema(this.schema);
57
+ logger.debug("[Agent] Agent-level schema initialized and validated");
58
+ }
35
59
  // Initialize context if provided
36
60
  this.context = options.context;
61
+ // Initialize collected data with initial data if provided
62
+ if (options.initialData) {
63
+ if (this.schema) {
64
+ const validation = this.validateData(options.initialData);
65
+ if (!validation.valid) {
66
+ throw new Error(`Initial data validation failed: ${validation.errors.map(e => e.message).join(', ')}`);
67
+ }
68
+ }
69
+ this.collectedData = { ...options.initialData };
70
+ logger.debug("[Agent] Initial data set:", this.collectedData);
71
+ }
37
72
  // Initialize current session if provided
38
73
  this.currentSession = options.session;
39
- // Initialize routing and response engines
74
+ // Initialize routing engine
40
75
  this.routingEngine = new RoutingEngine({
41
76
  maxCandidates: 5,
42
77
  allowRouteSwitch: true,
43
78
  switchThreshold: 70,
44
79
  });
45
- this.responseEngine = new ResponseEngine();
46
- this.responsePipeline = new ResponsePipeline(options, this.routes, this.tools, this.routingEngine, this.updateContext.bind(this), this.updateData.bind(this));
80
+ // Initialize ResponseModal for handling all response generation
81
+ this.responseModal = new ResponseModal(this);
47
82
  // Initialize persistence if configured
48
83
  if (options.persistence) {
49
- this.persistenceManager = new PersistenceManager(options.persistence);
50
- // Initialize the adapter if it has an initialize method
51
- if (options.persistence.adapter.initialize) {
52
- options.persistence.adapter.initialize().catch((error) => {
53
- logger.error("[Agent] Persistence adapter initialization failed:", error);
54
- });
84
+ try {
85
+ // Validate persistence configuration
86
+ if (!options.persistence.adapter) {
87
+ throw new Error("Persistence adapter is required when persistence is configured");
88
+ }
89
+ if (!options.persistence.adapter.sessionRepository) {
90
+ throw new Error("Persistence adapter must provide a sessionRepository");
91
+ }
92
+ if (!options.persistence.adapter.messageRepository) {
93
+ throw new Error("Persistence adapter must provide a messageRepository");
94
+ }
95
+ this.persistenceManager = new PersistenceManager(options.persistence);
96
+ // Initialize the adapter if it has an initialize method
97
+ if (options.persistence.adapter.initialize) {
98
+ options.persistence.adapter.initialize().catch((error) => {
99
+ logger.error("[Agent] Persistence adapter initialization failed:", error instanceof Error ? error.message : String(error));
100
+ });
101
+ }
102
+ }
103
+ catch (error) {
104
+ const errorMessage = error instanceof Error ? error.message : String(error);
105
+ logger.error("[Agent] Failed to initialize persistence:", errorMessage);
106
+ throw new Error(`Failed to initialize persistence: ${errorMessage}`);
55
107
  }
56
108
  }
57
109
  // Initialize from options - use create methods for consistency
@@ -89,6 +141,116 @@ export class Agent {
89
141
  });
90
142
  }
91
143
  }
144
+ /**
145
+ * Validate the agent-level schema structure
146
+ * @private
147
+ */
148
+ validateSchema(schema) {
149
+ if (!schema || typeof schema !== 'object') {
150
+ throw new Error("Agent schema must be a valid JSON Schema object. " +
151
+ "Provide a schema with 'type': 'object' and 'properties' to define the data structure.");
152
+ }
153
+ if (schema.type !== 'object') {
154
+ throw new Error(`Agent schema must be of type 'object', but received '${String(schema.type)}'. ` +
155
+ "Agent-level schemas must define object structures for data collection.");
156
+ }
157
+ if (!schema.properties || typeof schema.properties !== 'object') {
158
+ throw new Error("Agent schema must have a 'properties' field defining the data fields. " +
159
+ "Example: { type: 'object', properties: { name: { type: 'string' }, email: { type: 'string' } } }");
160
+ }
161
+ logger.debug("[Agent] Schema validation passed");
162
+ }
163
+ /**
164
+ * Validate data against the agent-level schema
165
+ */
166
+ validateData(data) {
167
+ if (!this.schema) {
168
+ // No schema defined, consider all data valid
169
+ return { valid: true, errors: [], warnings: [] };
170
+ }
171
+ const errors = [];
172
+ const warnings = [];
173
+ // Basic validation - check if provided fields exist in schema
174
+ if (this.schema.properties) {
175
+ for (const [key, value] of Object.entries(data)) {
176
+ if (!(key in this.schema.properties)) {
177
+ errors.push({
178
+ field: key,
179
+ value,
180
+ message: `Field '${key}' is not defined in agent schema`,
181
+ schemaPath: `properties.${key}`
182
+ });
183
+ }
184
+ }
185
+ }
186
+ // Check required fields if specified
187
+ if (this.schema.required && Array.isArray(this.schema.required)) {
188
+ for (const requiredField of this.schema.required) {
189
+ if (!(requiredField in data) || data[requiredField] === undefined) {
190
+ warnings.push({
191
+ field: requiredField,
192
+ value: undefined,
193
+ message: `Required field '${requiredField}' is missing`,
194
+ schemaPath: `required`
195
+ });
196
+ }
197
+ }
198
+ }
199
+ return {
200
+ valid: errors.length === 0,
201
+ errors,
202
+ warnings
203
+ };
204
+ }
205
+ /**
206
+ * Check if a field is valid according to the agent schema
207
+ * @param field - The field key to validate
208
+ * @returns true if field exists in schema or no schema is defined, false otherwise
209
+ */
210
+ isValidSchemaField(field) {
211
+ if (!this.schema || !this.schema.properties) {
212
+ // No schema defined, consider all fields valid
213
+ return true;
214
+ }
215
+ return field in this.schema.properties;
216
+ }
217
+ /**
218
+ * Get the current collected data
219
+ */
220
+ getCollectedData() {
221
+ return { ...this.collectedData };
222
+ }
223
+ /**
224
+ * Update collected data with validation
225
+ */
226
+ async updateCollectedData(updates) {
227
+ // Validate the updates
228
+ const validation = this.validateData(updates);
229
+ if (!validation.valid) {
230
+ const errorMessages = validation.errors.map(e => e.message).join(', ');
231
+ throw new DataValidationError(validation.errors, `Data validation failed: ${errorMessages}`);
232
+ }
233
+ // Log warnings if any
234
+ if (validation.warnings.length > 0) {
235
+ const warningMessages = validation.warnings.map(w => w.message).join(', ');
236
+ logger.warn(`[Agent] Data validation warnings: ${warningMessages}`);
237
+ }
238
+ // Merge updates with current data
239
+ const previousData = { ...this.collectedData };
240
+ this.collectedData = {
241
+ ...this.collectedData,
242
+ ...updates
243
+ };
244
+ // Trigger agent-level lifecycle hook if configured
245
+ if (this.options.hooks?.onDataUpdate) {
246
+ this.collectedData = await this.options.hooks.onDataUpdate(this.collectedData, previousData);
247
+ }
248
+ // Update current session if it exists to keep it in sync
249
+ if (this.currentSession) {
250
+ this.currentSession = mergeCollected(this.currentSession, this.collectedData);
251
+ }
252
+ logger.debug("[Agent] Collected data updated:", updates);
253
+ }
92
254
  /**
93
255
  * Get agent name
94
256
  */
@@ -114,10 +276,25 @@ export class Agent {
114
276
  return this.options.identity;
115
277
  }
116
278
  /**
117
- * Create a new route (journey)
118
- * @template TData - Type of data collected throughout the route
279
+ * Create a new route (journey) using agent-level data type
119
280
  */
120
281
  createRoute(options) {
282
+ // Validate that requiredFields exist in agent schema
283
+ if (options.requiredFields && this.schema?.properties) {
284
+ const invalidRequiredFields = options.requiredFields.filter(field => !(String(field) in this.schema.properties));
285
+ if (invalidRequiredFields.length > 0) {
286
+ throw new RouteConfigurationError(options.title, invalidRequiredFields.map(f => String(f)), `Invalid required fields in route '${options.title}': ${invalidRequiredFields.join(', ')}. ` +
287
+ `Must be valid keys from agent schema. Available fields: ${Object.keys(this.schema.properties).join(', ')}.`);
288
+ }
289
+ }
290
+ // Validate that optionalFields exist in agent schema
291
+ if (options.optionalFields && this.schema?.properties) {
292
+ const invalidOptionalFields = options.optionalFields.filter(field => !(String(field) in this.schema.properties));
293
+ if (invalidOptionalFields.length > 0) {
294
+ throw new RouteConfigurationError(options.title, invalidOptionalFields.map(f => String(f)), `Invalid optional fields in route '${options.title}': ${invalidOptionalFields.join(', ')}. ` +
295
+ `Must be valid keys from agent schema. Available fields: ${Object.keys(this.schema.properties).join(', ')}.`);
296
+ }
297
+ }
121
298
  const route = new Route(options);
122
299
  this.routes.push(route);
123
300
  return route;
@@ -202,6 +379,8 @@ export class Agent {
202
379
  if (this.options.hooks?.onDataUpdate) {
203
380
  newCollected = (await this.options.hooks.onDataUpdate(newCollected, previousCollected));
204
381
  }
382
+ // Update agent's collected data to stay in sync
383
+ this.collectedData = { ...newCollected };
205
384
  // Return updated session
206
385
  return mergeCollected(session, newCollected);
207
386
  }
@@ -216,894 +395,25 @@ export class Agent {
216
395
  // Otherwise return the stored context
217
396
  return this.context;
218
397
  }
398
+ /**
399
+ * Get current schema
400
+ */
401
+ getSchema() {
402
+ return this.schema;
403
+ }
219
404
  /**
220
405
  * Generate a response based on history and context as a stream
221
406
  */
222
407
  async *respondStream(params) {
223
- const { history: simpleHistory, signal } = params;
224
- const history = normalizeHistory(simpleHistory);
225
- // Prepare context and session using the response pipeline
226
- this.responsePipeline.setContext(this.context);
227
- this.responsePipeline.setCurrentSession(this.currentSession);
228
- let session;
229
- const responseContext = await this.responsePipeline.prepareResponseContext({
230
- contextOverride: params.contextOverride,
231
- session: params.session ? cloneDeep(params.session) : undefined,
232
- });
233
- const { effectiveContext } = responseContext;
234
- session = responseContext.session;
235
- // Update our stored context if it was modified by beforeRespond hook
236
- this.context = this.responsePipeline.getStoredContext();
237
- // PHASE 1: PREPARE - Execute prepare function if current step has one
238
- if (session.currentRoute && session.currentStep) {
239
- const currentRoute = this.routes.find((r) => r.id === session.currentRoute?.id);
240
- if (currentRoute) {
241
- const currentStep = currentRoute.getStep(session.currentStep.id);
242
- if (currentStep?.prepare) {
243
- logger.debug(`[Agent] Executing prepare for step: ${currentStep.id}`);
244
- await this.executePrepareFinalize(currentStep.prepare, effectiveContext, session.data, currentRoute, currentStep);
245
- }
246
- }
247
- }
248
- // PHASE 2: ROUTING + STEP SELECTION - Use response pipeline
249
- const routingResult = await this.responsePipeline.handleRoutingAndStepSelection({
250
- session,
251
- history,
252
- context: effectiveContext,
253
- signal,
254
- });
255
- const selectedRoute = routingResult.selectedRoute;
256
- const selectedStep = routingResult.selectedStep;
257
- const responseDirectives = routingResult.responseDirectives;
258
- const isRouteComplete = routingResult.isRouteComplete;
259
- session = routingResult.session;
260
- // PHASE 3: DETERMINE NEXT STEP - Use pipeline method
261
- const stepResult = this.responsePipeline.determineNextStep({
262
- selectedRoute,
263
- selectedStep,
264
- session,
265
- isRouteComplete,
266
- });
267
- const nextStep = stepResult.nextStep;
268
- session = stepResult.session;
269
- if (selectedRoute && !isRouteComplete) {
270
- // PHASE 4: RESPONSE GENERATION - Stream message using selected route and step
271
- // Get last user message
272
- const lastUserMessage = getLastMessageFromHistory(history);
273
- // Build response schema for this route (with collect fields from step)
274
- const responseSchema = this.responseEngine.responseSchemaForRoute(selectedRoute, nextStep);
275
- // Check if selected route and next step are defined
276
- if (!selectedRoute || !nextStep) {
277
- logger.error("[Agent] Selected route or next step is not defined", {
278
- selectedRoute,
279
- nextStep,
280
- });
281
- throw new Error("Selected route or next step is not defined");
282
- }
283
- // Build response prompt
284
- const responsePrompt = await this.responseEngine.buildResponsePrompt({
285
- route: selectedRoute,
286
- currentStep: nextStep,
287
- rules: selectedRoute.getRules(),
288
- prohibitions: selectedRoute.getProhibitions(),
289
- directives: responseDirectives,
290
- history,
291
- lastMessage: lastUserMessage,
292
- agentOptions: this.options,
293
- // Combine agent and route properties according to the specified logic
294
- combinedGuidelines: [
295
- ...this.getGuidelines(),
296
- ...selectedRoute.getGuidelines(),
297
- ],
298
- combinedTerms: this.mergeTerms(this.getTerms(), selectedRoute.getTerms()),
299
- context: effectiveContext,
300
- session,
301
- });
302
- // Collect available tools for AI
303
- const availableTools = this.collectAvailableTools(selectedRoute, nextStep);
304
- // Generate message stream using AI provider
305
- const stream = this.options.provider.generateMessageStream({
306
- prompt: responsePrompt,
307
- history,
308
- context: effectiveContext,
309
- tools: availableTools,
310
- signal,
311
- parameters: {
312
- jsonSchema: responseSchema,
313
- schemaName: "response_stream_output",
314
- },
315
- });
316
- // Stream chunks to caller
317
- for await (const chunk of stream) {
318
- let toolCalls = undefined;
319
- // Extract tool calls from AI response on final chunk
320
- if (chunk.done && chunk.structured?.toolCalls) {
321
- toolCalls = chunk.structured.toolCalls;
322
- // Execute dynamic tool calls
323
- if (toolCalls.length > 0) {
324
- logger.debug(`[Agent] Executing ${toolCalls.length} dynamic tool calls`);
325
- for (const toolCall of toolCalls) {
326
- const tool = this.findAvailableTool(toolCall.toolName, selectedRoute);
327
- if (!tool) {
328
- logger.warn(`[Agent] Tool not found: ${toolCall.toolName}`);
329
- continue;
330
- }
331
- const toolExecutor = new ToolExecutor();
332
- const result = await toolExecutor.executeTool({
333
- tool: tool,
334
- context: effectiveContext,
335
- updateContext: this.updateContext.bind(this),
336
- history,
337
- data: session.data,
338
- toolArguments: toolCall.arguments,
339
- });
340
- // Update context with tool results
341
- if (result.contextUpdate) {
342
- await this.updateContext(result.contextUpdate);
343
- }
344
- // Update collected data with tool results
345
- if (result.dataUpdate) {
346
- session = await this.updateData(session, result.dataUpdate);
347
- logger.debug(`[Agent] Tool updated collected data:`, result.dataUpdate);
348
- }
349
- logger.debug(`[Agent] Executed dynamic tool: ${result.toolName} (success: ${result.success})`);
350
- }
351
- }
352
- }
353
- // TOOL LOOP: Allow AI to make follow-up tool calls after initial tool execution (streaming)
354
- const MAX_TOOL_LOOPS = 5;
355
- let toolLoopCount = 0;
356
- let hasToolCalls = toolCalls && toolCalls.length > 0;
357
- while (hasToolCalls && toolLoopCount < MAX_TOOL_LOOPS) {
358
- toolLoopCount++;
359
- logger.debug(`[Agent] Starting streaming tool loop ${toolLoopCount}/${MAX_TOOL_LOOPS}`);
360
- // Add tool execution results to history so AI knows what happened
361
- const toolResultsEvents = [];
362
- for (const toolCall of toolCalls || []) {
363
- const tool = this.findAvailableTool(toolCall.toolName, selectedRoute);
364
- if (tool) {
365
- toolResultsEvents.push({
366
- kind: EventKind.TOOL,
367
- source: MessageRole.AGENT,
368
- timestamp: new Date().toISOString(),
369
- data: {
370
- tool_calls: [
371
- {
372
- tool_id: toolCall.toolName,
373
- arguments: toolCall.arguments,
374
- result: {
375
- data: "Tool executed successfully",
376
- },
377
- },
378
- ],
379
- },
380
- });
381
- }
382
- }
383
- // Create updated history with tool results
384
- const updatedHistory = [...history, ...toolResultsEvents];
385
- // Make follow-up streaming AI call to see if more tools are needed
386
- const followUpStream = this.options.provider.generateMessageStream({
387
- prompt: responsePrompt,
388
- history: updatedHistory,
389
- context: effectiveContext,
390
- tools: availableTools,
391
- parameters: {
392
- jsonSchema: responseSchema,
393
- schemaName: "tool_followup",
394
- },
395
- signal,
396
- });
397
- let followUpToolCalls;
398
- for await (const followUpChunk of followUpStream) {
399
- // Extract tool calls from follow-up stream
400
- if (followUpChunk.done && followUpChunk.structured?.toolCalls) {
401
- followUpToolCalls = followUpChunk.structured.toolCalls;
402
- }
403
- }
404
- hasToolCalls = followUpToolCalls && followUpToolCalls.length > 0;
405
- if (hasToolCalls) {
406
- logger.debug(`[Agent] Follow-up streaming call produced ${followUpToolCalls.length} additional tool calls`);
407
- // Execute the follow-up tool calls
408
- for (const toolCall of followUpToolCalls) {
409
- const tool = this.findAvailableTool(toolCall.toolName, selectedRoute);
410
- if (!tool) {
411
- logger.warn(`[Agent] Tool not found in streaming follow-up: ${toolCall.toolName}`);
412
- continue;
413
- }
414
- const toolExecutor = new ToolExecutor();
415
- const result = await toolExecutor.executeTool({
416
- tool: tool,
417
- context: effectiveContext,
418
- updateContext: this.updateContext.bind(this),
419
- history: updatedHistory,
420
- data: session.data,
421
- toolArguments: toolCall.arguments,
422
- });
423
- // Update context with follow-up tool results
424
- if (result.contextUpdate) {
425
- await this.updateContext(result.contextUpdate);
426
- }
427
- if (result.dataUpdate) {
428
- session = await this.updateData(session, result.dataUpdate);
429
- logger.debug(`[Agent] Streaming follow-up tool updated collected data:`, result.dataUpdate);
430
- }
431
- logger.debug(`[Agent] Executed streaming follow-up tool: ${result.toolName} (success: ${result.success})`);
432
- }
433
- // Update toolCalls for next iteration
434
- toolCalls = followUpToolCalls;
435
- }
436
- else {
437
- logger.debug(`[Agent] Streaming tool loop completed after ${toolLoopCount} iterations`);
438
- // Update toolCalls for final response
439
- toolCalls = followUpToolCalls || [];
440
- break;
441
- }
442
- }
443
- if (toolLoopCount >= MAX_TOOL_LOOPS) {
444
- logger.warn(`[Agent] Streaming tool loop limit reached (${MAX_TOOL_LOOPS}), stopping`);
445
- }
446
- // Extract collected data on final chunk
447
- if (chunk.done && chunk.structured && nextStep.collect) {
448
- const collectedData = {};
449
- // The structured response includes both base fields and collected extraction fields
450
- const structuredData = chunk.structured;
451
- for (const field of nextStep.collect) {
452
- if (field in structuredData) {
453
- collectedData[field] = structuredData[field];
454
- }
455
- }
456
- // Merge collected data into session
457
- if (Object.keys(collectedData).length > 0) {
458
- session = await this.updateData(session, collectedData);
459
- logger.debug(`[Agent] Collected data:`, collectedData);
460
- }
461
- }
462
- // Extract any additional data from structured response on final chunk
463
- if (chunk.done &&
464
- chunk.structured &&
465
- typeof chunk.structured === "object" &&
466
- "contextUpdate" in chunk.structured) {
467
- await this.updateContext(chunk.structured
468
- .contextUpdate);
469
- }
470
- // Auto-save session step on final chunk
471
- if (chunk.done &&
472
- this.persistenceManager &&
473
- session.id &&
474
- this.options.persistence?.autoSave !== false) {
475
- await this.persistenceManager.saveSessionState(session.id, session);
476
- logger.debug(`[Agent] Auto-saved session step to persistence: ${session.id}`);
477
- }
478
- // Execute finalize function on final chunk
479
- if (chunk.done && session.currentRoute && session.currentStep) {
480
- const currentRoute = this.routes.find((r) => r.id === session.currentRoute?.id);
481
- if (currentRoute) {
482
- const currentStep = currentRoute.getStep(session.currentStep.id);
483
- if (currentStep?.finalize) {
484
- logger.debug(`[Agent] Executing finalize for step: ${currentStep.id}`);
485
- await this.executePrepareFinalize(currentStep.finalize, effectiveContext, session.data, currentRoute, currentStep);
486
- }
487
- }
488
- }
489
- // Update current session if we have one
490
- if (chunk.done && this.currentSession) {
491
- this.currentSession = session;
492
- }
493
- yield {
494
- delta: chunk.delta,
495
- accumulated: chunk.accumulated,
496
- done: chunk.done,
497
- session, // Return updated session
498
- toolCalls,
499
- isRouteComplete,
500
- metadata: chunk.metadata,
501
- structured: chunk.structured,
502
- };
503
- }
504
- }
505
- else if (isRouteComplete && selectedRoute) {
506
- // Route is complete - generate completion message then check for onComplete transition
507
- const lastUserMessage = getLastMessageFromHistory(history);
508
- // Get endStep spec from route
509
- const endStepSpec = selectedRoute.endStepSpec;
510
- // Create a temporary step for completion message generation using endStep configuration
511
- const completionStep = new Step(selectedRoute.id, {
512
- description: endStepSpec.description,
513
- id: endStepSpec.id || END_ROUTE_ID,
514
- collect: endStepSpec.collect,
515
- requires: endStepSpec.requires,
516
- prompt: endStepSpec.prompt ||
517
- "Summarize what was accomplished and confirm completion based on the conversation history and collected data",
518
- });
519
- // Build response schema for completion
520
- const responseSchema = this.responseEngine.responseSchemaForRoute(selectedRoute, completionStep);
521
- const templateContext = {
522
- context: effectiveContext,
523
- session,
524
- history,
525
- };
526
- // Build completion response prompt
527
- const completionPrompt = await this.responseEngine.buildResponsePrompt({
528
- route: selectedRoute,
529
- currentStep: completionStep,
530
- rules: selectedRoute.getRules(),
531
- prohibitions: selectedRoute.getProhibitions(),
532
- directives: undefined, // No directives for completion
533
- history,
534
- lastMessage: lastUserMessage,
535
- agentOptions: this.options,
536
- // Combine agent and route properties according to the specified logic
537
- combinedGuidelines: [
538
- ...this.getGuidelines(),
539
- ...selectedRoute.getGuidelines(),
540
- ],
541
- combinedTerms: this.mergeTerms(this.getTerms(), selectedRoute.getTerms()),
542
- context: effectiveContext,
543
- session,
544
- });
545
- // Stream completion message using AI provider
546
- const stream = this.options.provider.generateMessageStream({
547
- prompt: completionPrompt,
548
- history,
549
- context: effectiveContext,
550
- signal,
551
- parameters: {
552
- jsonSchema: responseSchema,
553
- schemaName: "completion_message_stream",
554
- },
555
- });
556
- logger.debug(`[Agent] Streaming completion message for route: ${selectedRoute.title}`);
557
- // Check for onComplete transition
558
- const transitionConfig = await selectedRoute.evaluateOnComplete({ data: session.data }, effectiveContext);
559
- if (transitionConfig) {
560
- // Find target route by ID or title
561
- const targetRoute = this.routes.find((r) => r.id === transitionConfig.nextStep ||
562
- r.title === transitionConfig.nextStep);
563
- if (targetRoute) {
564
- const renderedCondition = await render(transitionConfig.condition, templateContext);
565
- // Set pending transition in session
566
- session = {
567
- ...session,
568
- pendingTransition: {
569
- targetRouteId: targetRoute.id,
570
- condition: renderedCondition,
571
- reason: "route_complete",
572
- },
573
- };
574
- logger.debug(`[Agent] Route ${selectedRoute.title} completed with pending transition to: ${targetRoute.title}`);
575
- }
576
- else {
577
- logger.warn(`[Agent] Route ${selectedRoute.title} completed but target route not found: ${transitionConfig.nextStep}`);
578
- }
579
- }
580
- // Set step to END_ROUTE marker
581
- session = enterStep(session, END_ROUTE_ID, "Route completed");
582
- logger.debug(`[Agent] Route ${selectedRoute.title} completed. Entered END_ROUTE step.`);
583
- // Stream completion chunks
584
- for await (const chunk of stream) {
585
- // Update current session if we have one
586
- if (chunk.done && this.currentSession) {
587
- this.currentSession = session;
588
- }
589
- yield {
590
- delta: chunk.delta,
591
- accumulated: chunk.accumulated,
592
- done: chunk.done,
593
- session,
594
- toolCalls: undefined,
595
- isRouteComplete: true,
596
- metadata: chunk.metadata,
597
- structured: chunk.structured,
598
- };
599
- }
600
- }
601
- else {
602
- // Fallback: No routes defined, stream a simple response
603
- const fallbackPrompt = await this.responseEngine.buildFallbackPrompt({
604
- history,
605
- agentOptions: this.options,
606
- terms: this.terms,
607
- guidelines: this.guidelines,
608
- context: effectiveContext,
609
- session,
610
- });
611
- const stream = this.options.provider.generateMessageStream({
612
- prompt: fallbackPrompt,
613
- history,
614
- context: effectiveContext,
615
- signal,
616
- parameters: {
617
- jsonSchema: {
618
- type: "object",
619
- properties: {
620
- message: { type: "string" },
621
- },
622
- required: ["message"],
623
- additionalProperties: false,
624
- },
625
- schemaName: "fallback_stream_response",
626
- },
627
- });
628
- for await (const chunk of stream) {
629
- // Update current session if we have one
630
- if (chunk.done && this.currentSession) {
631
- this.currentSession = session;
632
- }
633
- yield {
634
- delta: chunk.delta,
635
- accumulated: chunk.accumulated,
636
- done: chunk.done,
637
- session, // Return updated session
638
- toolCalls: undefined,
639
- isRouteComplete: false,
640
- metadata: chunk.metadata,
641
- structured: chunk.structured,
642
- };
643
- }
644
- }
408
+ // Delegate to ResponseModal
409
+ yield* this.responseModal.respondStream(params);
645
410
  }
646
411
  /**
647
412
  * Generate a response based on history and context
648
413
  */
649
414
  async respond(params) {
650
- const { history: simpleHistory, contextOverride, signal } = params;
651
- const history = normalizeHistory(simpleHistory);
652
- // Get current context (may fetch from provider)
653
- let currentContext = await this.getContext();
654
- // Call beforeRespond hook if configured
655
- if (this.options.hooks?.beforeRespond && currentContext !== undefined) {
656
- currentContext = await this.options.hooks.beforeRespond(currentContext);
657
- // Update stored context with the result from beforeRespond
658
- this.context = currentContext;
659
- }
660
- // Merge context with override
661
- const effectiveContext = {
662
- ...currentContext,
663
- ...contextOverride,
664
- };
665
- // Initialize or get session (use current session if available)
666
- let session = cloneDeep(params.session) ||
667
- cloneDeep(this.currentSession) ||
668
- (await this.session.getOrCreate());
669
- // PHASE 1: PREPARE - Execute prepare function if current step has one
670
- if (session.currentRoute && session.currentStep) {
671
- const currentRoute = this.routes.find((r) => r.id === session.currentRoute?.id);
672
- if (currentRoute) {
673
- const currentStep = currentRoute.getStep(session.currentStep.id);
674
- if (currentStep?.prepare) {
675
- logger.debug(`[Agent] Executing prepare for step: ${currentStep.id}`);
676
- await this.executePrepareFinalize(currentStep.prepare, effectiveContext, session.data, currentRoute, currentStep);
677
- }
678
- }
679
- }
680
- // PHASE 2: ROUTING + STEP SELECTION - Determine which route and step to use (combined)
681
- let selectedRoute;
682
- let responseDirectives;
683
- let selectedStep;
684
- let isRouteComplete = false;
685
- // Check for pending transition from previous route completion
686
- if (session.pendingTransition) {
687
- const targetRoute = this.routes.find((r) => r.id === session.pendingTransition?.targetRouteId);
688
- if (targetRoute) {
689
- logger.debug(`[Agent] Auto-transitioning from pending transition to route: ${targetRoute.title}`);
690
- // Clear pending transition and enter new route
691
- session = {
692
- ...session,
693
- pendingTransition: undefined,
694
- };
695
- session = enterRoute(session, targetRoute.id, targetRoute.title);
696
- // Merge initial data if available
697
- if (targetRoute.initialData) {
698
- session = mergeCollected(session, targetRoute.initialData);
699
- }
700
- selectedRoute = targetRoute;
701
- }
702
- else {
703
- logger.warn(`[Agent] Pending transition target route not found: ${session.pendingTransition.targetRouteId}`);
704
- // Clear invalid transition
705
- session = {
706
- ...session,
707
- pendingTransition: undefined,
708
- };
709
- }
710
- }
711
- // If no pending transition or transition handled, do normal routing
712
- if (this.routes.length > 0 && !selectedRoute) {
713
- const orchestration = await this.routingEngine.decideRouteAndStep({
714
- routes: this.routes,
715
- session,
716
- history,
717
- agentOptions: this.options,
718
- provider: this.options.provider,
719
- context: effectiveContext,
720
- signal,
721
- });
722
- selectedRoute = orchestration.selectedRoute;
723
- selectedStep = orchestration.selectedStep;
724
- responseDirectives = orchestration.responseDirectives;
725
- session = orchestration.session;
726
- isRouteComplete = orchestration.isRouteComplete || false;
727
- // Log if route is complete
728
- if (isRouteComplete) {
729
- logger.debug(`[Agent] Route complete: all required data collected, END_ROUTE reached`);
730
- }
731
- }
732
- // PHASE 3: DETERMINE NEXT STEP - Use step from combined decision or get initial step
733
- let message;
734
- let toolCalls = undefined;
735
- let responsePrompt;
736
- let availableTools = [];
737
- let responseSchema;
738
- let nextStep;
739
- // Get last user message (needed for both route and completion handling)
740
- const lastUserMessage = getLastMessageFromHistory(history);
741
- if (selectedRoute && !isRouteComplete) {
742
- // If we have a selected step from the combined routing decision, use it
743
- if (selectedStep) {
744
- nextStep = selectedStep;
745
- }
746
- else {
747
- // New route or no step selected - get initial step or first valid step
748
- const candidates = this.routingEngine.getCandidateSteps(selectedRoute, undefined, session.data || {});
749
- if (candidates.length > 0) {
750
- nextStep = candidates[0].step;
751
- logger.debug(`[Agent] Using first valid step: ${nextStep.id} for new route`);
752
- }
753
- else {
754
- // Fallback to initial step even if it should be skipped
755
- nextStep = selectedRoute.initialStep;
756
- logger.warn(`[Agent] No valid steps found, using initial step: ${nextStep.id}`);
757
- }
758
- }
759
- // Update session with next step
760
- session = enterStep(session, nextStep.id, nextStep.description);
761
- logger.debug(`[Agent] Entered step: ${nextStep.id}`);
762
- // PHASE 4: RESPONSE GENERATION - Generate message using selected route and step
763
- // Get last user message
764
- const lastUserMessage = getLastMessageFromHistory(history);
765
- // Build response schema for this route (with collect fields from step)
766
- responseSchema = this.responseEngine.responseSchemaForRoute(selectedRoute, nextStep);
767
- // Build response prompt
768
- responsePrompt = await this.responseEngine.buildResponsePrompt({
769
- route: selectedRoute,
770
- currentStep: nextStep,
771
- rules: selectedRoute.getRules(),
772
- prohibitions: selectedRoute.getProhibitions(),
773
- directives: responseDirectives,
774
- history,
775
- lastMessage: lastUserMessage,
776
- agentOptions: this.options,
777
- // Combine agent and route properties according to the specified logic
778
- combinedGuidelines: [
779
- ...this.getGuidelines(),
780
- ...selectedRoute.getGuidelines(),
781
- ],
782
- combinedTerms: this.mergeTerms(this.getTerms(), selectedRoute.getTerms()),
783
- context: effectiveContext,
784
- session,
785
- });
786
- // Collect available tools for AI
787
- availableTools = this.collectAvailableTools(selectedRoute, nextStep);
788
- }
789
- else {
790
- // No route selected - generate basic response without route context
791
- logger.debug(`[Agent] No route selected, generating basic response`);
792
- // Build basic response prompt without route context
793
- responsePrompt = await this.responseEngine.buildFallbackPrompt({
794
- history,
795
- agentOptions: this.options,
796
- terms: this.getTerms(),
797
- guidelines: this.getGuidelines(),
798
- context: effectiveContext,
799
- session,
800
- });
801
- // Use agent-level tools only
802
- availableTools = [...this.tools];
803
- responseSchema = undefined;
804
- }
805
- // Generate message using AI provider (common for both route and no-route cases)
806
- const result = await this.options.provider.generateMessage({
807
- prompt: responsePrompt,
808
- history,
809
- context: effectiveContext,
810
- tools: availableTools,
811
- signal,
812
- parameters: responseSchema
813
- ? {
814
- jsonSchema: responseSchema,
815
- schemaName: "response_output",
816
- }
817
- : undefined,
818
- });
819
- message = result.structured?.message || result.message;
820
- // Process dynamic tool calls from AI response (common for both route and no-route cases)
821
- if (result.structured?.toolCalls) {
822
- toolCalls = result.structured.toolCalls;
823
- // Execute dynamic tool calls
824
- if (toolCalls.length > 0) {
825
- logger.debug(`[Agent] Executing ${toolCalls.length} dynamic tool calls`);
826
- for (const toolCall of toolCalls) {
827
- const tool = this.findAvailableTool(toolCall.toolName, selectedRoute);
828
- if (!tool) {
829
- logger.warn(`[Agent] Tool not found: ${toolCall.toolName}`);
830
- continue;
831
- }
832
- const toolExecutor = new ToolExecutor();
833
- const toolResult = await toolExecutor.executeTool({
834
- tool: tool,
835
- context: effectiveContext,
836
- updateContext: this.updateContext.bind(this),
837
- history,
838
- data: session.data,
839
- toolArguments: toolCall.arguments,
840
- });
841
- // Update context with tool results
842
- if (toolResult.contextUpdate) {
843
- await this.updateContext(toolResult.contextUpdate);
844
- }
845
- // Update collected data with tool results
846
- if (toolResult.dataUpdate) {
847
- session = await this.updateData(session, toolResult.dataUpdate);
848
- logger.debug(`[Agent] Tool updated collected data:`, toolResult.dataUpdate);
849
- }
850
- logger.debug(`[Agent] Executed dynamic tool: ${toolResult.toolName} (success: ${toolResult.success})`);
851
- }
852
- }
853
- }
854
- // TOOL LOOP: Allow AI to make follow-up tool calls after initial tool execution
855
- const MAX_TOOL_LOOPS = 5;
856
- let toolLoopCount = 0;
857
- let hasToolCalls = toolCalls && toolCalls.length > 0;
858
- while (hasToolCalls && toolLoopCount < MAX_TOOL_LOOPS) {
859
- toolLoopCount++;
860
- logger.debug(`[Agent] Starting tool loop ${toolLoopCount}/${MAX_TOOL_LOOPS}`);
861
- // Add tool execution results to history so AI knows what happened
862
- const toolResultsEvents = [];
863
- for (const toolCall of toolCalls || []) {
864
- const tool = this.findAvailableTool(toolCall.toolName, selectedRoute);
865
- if (tool) {
866
- toolResultsEvents.push({
867
- kind: EventKind.TOOL,
868
- source: MessageRole.AGENT,
869
- timestamp: new Date().toISOString(),
870
- data: {
871
- tool_calls: [
872
- {
873
- tool_id: toolCall.toolName,
874
- arguments: toolCall.arguments,
875
- result: {
876
- data: "Tool executed successfully",
877
- },
878
- },
879
- ],
880
- },
881
- });
882
- }
883
- }
884
- // Create updated history with tool results
885
- const updatedHistory = [...history, ...toolResultsEvents];
886
- // Make follow-up AI call to see if more tools are needed
887
- const followUpResult = await this.options.provider.generateMessage({
888
- prompt: responsePrompt,
889
- history: updatedHistory,
890
- context: effectiveContext,
891
- tools: availableTools,
892
- parameters: {
893
- jsonSchema: responseSchema,
894
- schemaName: "tool_followup",
895
- },
896
- signal,
897
- });
898
- // Check if follow-up call has more tool calls
899
- const followUpToolCalls = followUpResult.structured?.toolCalls;
900
- hasToolCalls = followUpToolCalls && followUpToolCalls.length > 0;
901
- if (hasToolCalls) {
902
- logger.debug(`[Agent] Follow-up call produced ${followUpToolCalls.length} additional tool calls`);
903
- // Execute the follow-up tool calls
904
- for (const toolCall of followUpToolCalls) {
905
- const tool = this.findAvailableTool(toolCall.toolName, selectedRoute);
906
- if (!tool) {
907
- logger.warn(`[Agent] Tool not found in follow-up: ${toolCall.toolName}`);
908
- continue;
909
- }
910
- const toolExecutor = new ToolExecutor();
911
- const toolResult = await toolExecutor.executeTool({
912
- tool: tool,
913
- context: effectiveContext,
914
- updateContext: this.updateContext.bind(this),
915
- history: updatedHistory,
916
- data: session.data,
917
- toolArguments: toolCall.arguments,
918
- });
919
- // Update context with follow-up tool results
920
- if (toolResult.contextUpdate) {
921
- await this.updateContext(toolResult.contextUpdate);
922
- }
923
- if (toolResult.dataUpdate) {
924
- session = await this.updateData(session, toolResult.dataUpdate);
925
- logger.debug(`[Agent] Follow-up tool updated collected data:`, toolResult.dataUpdate);
926
- }
927
- logger.debug(`[Agent] Executed follow-up tool: ${toolResult.toolName} (success: ${toolResult.success})`);
928
- }
929
- // Update toolCalls for next iteration or final response
930
- toolCalls = followUpToolCalls;
931
- }
932
- else {
933
- logger.debug(`[Agent] Tool loop completed after ${toolLoopCount} iterations`);
934
- // Update final message and toolCalls from follow-up result if no more tools
935
- message = followUpResult.structured?.message || followUpResult.message;
936
- toolCalls = followUpToolCalls || [];
937
- break;
938
- }
939
- }
940
- if (toolLoopCount >= MAX_TOOL_LOOPS) {
941
- logger.warn(`[Agent] Tool loop limit reached (${MAX_TOOL_LOOPS}), stopping`);
942
- }
943
- // Extract collected data from final response (only for route-based interactions)
944
- if (selectedRoute && result.structured && nextStep?.collect) {
945
- const collectedData = {};
946
- // The structured response includes both base fields and collected extraction fields
947
- const structuredData = result.structured;
948
- for (const field of nextStep.collect) {
949
- if (field in structuredData) {
950
- collectedData[field] = structuredData[field];
951
- }
952
- }
953
- // Merge collected data into session
954
- if (Object.keys(collectedData).length > 0) {
955
- session = await this.updateData(session, collectedData);
956
- logger.debug(`[Agent] Collected data:`, collectedData);
957
- }
958
- }
959
- // Extract any additional data from structured response
960
- if (result.structured &&
961
- typeof result.structured === "object" &&
962
- "contextUpdate" in result.structured) {
963
- await this.updateContext(result.structured
964
- .contextUpdate);
965
- }
966
- // Handle route completion if route is complete
967
- if (isRouteComplete) {
968
- // Route is complete - generate completion message then check for onComplete transition
969
- // Get endStep spec from route
970
- const endStepSpec = selectedRoute.endStepSpec;
971
- // Create a temporary step for completion message generation using endStep configuration
972
- const completionStep = new Step(selectedRoute.id, {
973
- description: endStepSpec.description,
974
- id: endStepSpec.id || END_ROUTE_ID,
975
- collect: endStepSpec.collect,
976
- requires: endStepSpec.requires,
977
- prompt: endStepSpec.prompt ||
978
- "Summarize what was accomplished and confirm completion based on the conversation history and collected data",
979
- });
980
- // Build response schema for completion
981
- const responseSchema = this.responseEngine.responseSchemaForRoute(selectedRoute, completionStep);
982
- const templateContext = {
983
- context: effectiveContext,
984
- session,
985
- history,
986
- };
987
- if (!selectedRoute) {
988
- throw new Error("Selected route is not defined");
989
- }
990
- // Build completion response prompt
991
- const completionPrompt = await this.responseEngine.buildResponsePrompt({
992
- route: selectedRoute,
993
- currentStep: completionStep,
994
- rules: selectedRoute.getRules(),
995
- prohibitions: selectedRoute.getProhibitions(),
996
- directives: undefined, // No directives for completion
997
- history,
998
- lastMessage: lastUserMessage,
999
- agentOptions: this.options,
1000
- // Combine agent and route properties according to the specified logic
1001
- combinedGuidelines: [
1002
- ...this.getGuidelines(),
1003
- ...selectedRoute.getGuidelines(),
1004
- ],
1005
- combinedTerms: this.mergeTerms(this.getTerms(), selectedRoute.getTerms()),
1006
- context: effectiveContext,
1007
- session,
1008
- });
1009
- // Generate completion message using AI provider
1010
- const completionResult = await this.options.provider.generateMessage({
1011
- prompt: completionPrompt,
1012
- history,
1013
- context: effectiveContext,
1014
- signal,
1015
- parameters: {
1016
- jsonSchema: responseSchema,
1017
- schemaName: "completion_message",
1018
- },
1019
- });
1020
- message =
1021
- completionResult.structured?.message || completionResult.message;
1022
- logger.debug(`[Agent] Generated completion message for route: ${selectedRoute.title}`);
1023
- // Check for onComplete transition
1024
- const transitionConfig = await selectedRoute.evaluateOnComplete({ data: session.data }, effectiveContext);
1025
- if (transitionConfig) {
1026
- // Find target route by ID or title
1027
- const targetRoute = this.routes.find((r) => r.id === transitionConfig.nextStep ||
1028
- r.title === transitionConfig.nextStep);
1029
- if (targetRoute) {
1030
- const renderedCondition = await render(transitionConfig.condition, templateContext);
1031
- // Set pending transition in session
1032
- session = {
1033
- ...session,
1034
- pendingTransition: {
1035
- targetRouteId: targetRoute.id,
1036
- condition: renderedCondition,
1037
- reason: "route_complete",
1038
- },
1039
- };
1040
- logger.debug(`[Agent] Route ${selectedRoute.title} completed with pending transition to: ${targetRoute.title}`);
1041
- }
1042
- else {
1043
- logger.warn(`[Agent] Route ${selectedRoute.title} completed but target route not found: ${transitionConfig.nextStep}`);
1044
- }
1045
- }
1046
- // Set step to END_ROUTE marker
1047
- session = enterStep(session, END_ROUTE_ID, "Route completed");
1048
- logger.debug(`[Agent] Route ${selectedRoute.title} completed. Entered END_ROUTE step.`);
1049
- }
1050
- else {
1051
- // Fallback: No routes defined, generate a simple response
1052
- const fallbackPrompt = await this.responseEngine.buildFallbackPrompt({
1053
- history,
1054
- agentOptions: this.options,
1055
- terms: this.terms,
1056
- guidelines: this.guidelines,
1057
- context: effectiveContext,
1058
- session,
1059
- });
1060
- const result = await this.options.provider.generateMessage({
1061
- prompt: fallbackPrompt,
1062
- history,
1063
- context: effectiveContext,
1064
- signal,
1065
- parameters: {
1066
- jsonSchema: {
1067
- type: "object",
1068
- properties: {
1069
- message: { type: "string" },
1070
- },
1071
- required: ["message"],
1072
- additionalProperties: false,
1073
- },
1074
- schemaName: "fallback_response",
1075
- },
1076
- });
1077
- message = result.structured?.message || result.message;
1078
- }
1079
- // Auto-save session step to persistence if configured
1080
- if (this.persistenceManager &&
1081
- session.id &&
1082
- this.options.persistence?.autoSave !== false) {
1083
- await this.persistenceManager.saveSessionState(session.id, session);
1084
- logger.debug(`[Agent] Auto-saved session step to persistence: ${session.id}`);
1085
- }
1086
- // Execute finalize function
1087
- if (session.currentRoute && session.currentStep) {
1088
- const currentRoute = this.routes.find((r) => r.id === session.currentRoute?.id);
1089
- if (currentRoute) {
1090
- const currentStep = currentRoute.getStep(session.currentStep.id);
1091
- if (currentStep?.finalize) {
1092
- logger.debug(`[Agent] Executing finalize for step: ${currentStep.id}`);
1093
- await this.executePrepareFinalize(currentStep.finalize, effectiveContext, session.data, currentRoute, currentStep);
1094
- }
1095
- }
1096
- }
1097
- // Update current session if we have one
1098
- if (this.currentSession) {
1099
- this.currentSession = session;
1100
- }
1101
- return {
1102
- message,
1103
- session, // Return updated session with route/step info
1104
- toolCalls,
1105
- isRouteComplete, // Indicates if the route has reached END_ROUTE with all data collected
1106
- };
415
+ // Delegate to ResponseModal
416
+ return this.responseModal.respond(params);
1107
417
  }
1108
418
  /**
1109
419
  * Get all routes
@@ -1111,6 +421,27 @@ export class Agent {
1111
421
  getRoutes() {
1112
422
  return [...this.routes];
1113
423
  }
424
+ /**
425
+ * Get agent options
426
+ * @internal Used by ResponseModal
427
+ */
428
+ getAgentOptions() {
429
+ return this.options;
430
+ }
431
+ /**
432
+ * Get routing engine
433
+ * @internal Used by ResponseModal
434
+ */
435
+ getRoutingEngine() {
436
+ return this.routingEngine;
437
+ }
438
+ /**
439
+ * Get the updateData method bound to this agent
440
+ * @internal Used by ResponseModal
441
+ */
442
+ getUpdateDataMethod() {
443
+ return this.updateData.bind(this);
444
+ }
1114
445
  /**
1115
446
  * Get all terms
1116
447
  */
@@ -1124,85 +455,45 @@ export class Agent {
1124
455
  return [...this.tools];
1125
456
  }
1126
457
  /**
1127
- * Find an available tool by name for the given route
1128
- * Route-level tools take precedence over agent-level tools
1129
- * @private
458
+ * Get all guidelines
1130
459
  */
1131
- findAvailableTool(toolName, route) {
1132
- // Check route-level tools first (if route provided)
1133
- if (route) {
1134
- const routeTool = route
1135
- .getTools()
1136
- .find((tool) => tool.id === toolName || tool.name === toolName);
1137
- if (routeTool)
1138
- return routeTool;
1139
- }
1140
- // Fall back to agent-level tools
1141
- return this.tools.find((tool) => tool.id === toolName || tool.name === toolName);
460
+ getGuidelines() {
461
+ return [...this.guidelines];
1142
462
  }
1143
463
  /**
1144
- * Collect all available tools for the given route and step context
1145
- * @private
464
+ * Get the agent's knowledge base
1146
465
  */
1147
- collectAvailableTools(route, step) {
1148
- const availableTools = new Map();
1149
- // Add agent-level tools
1150
- this.tools.forEach((tool) => {
1151
- availableTools.set(tool.id, tool);
1152
- });
1153
- // Add route-level tools (these take precedence)
1154
- if (route) {
1155
- route.getTools().forEach((tool) => {
1156
- availableTools.set(tool.id, tool);
1157
- });
1158
- }
1159
- // Filter by step-level allowed tools if specified
1160
- if (step?.tools) {
1161
- const allowedToolIds = new Set();
1162
- const stepTools = [];
1163
- for (const toolRef of step.tools) {
1164
- if (typeof toolRef === "string") {
1165
- // Reference to registered tool
1166
- allowedToolIds.add(toolRef);
1167
- }
1168
- else {
1169
- // Inline tool definition
1170
- if (toolRef.id) {
1171
- allowedToolIds.add(toolRef.id);
1172
- stepTools.push(toolRef);
1173
- }
1174
- }
1175
- }
1176
- // If step specifies tools, only include those
1177
- if (allowedToolIds.size > 0) {
1178
- const filteredTools = new Map();
1179
- for (const toolId of allowedToolIds) {
1180
- const tool = availableTools.get(toolId);
1181
- if (tool) {
1182
- filteredTools.set(toolId, tool);
1183
- }
1184
- }
1185
- // Add inline tools
1186
- stepTools.forEach((tool) => {
1187
- if (tool.id) {
1188
- filteredTools.set(tool.id, tool);
1189
- }
1190
- });
1191
- availableTools.clear();
1192
- filteredTools.forEach((tool, id) => availableTools.set(id, tool));
1193
- }
1194
- }
1195
- // Convert to the format expected by AI providers
1196
- return Array.from(availableTools.values()).map((tool) => ({
1197
- id: tool.id,
1198
- name: tool.name || tool.id,
1199
- description: tool.description,
1200
- parameters: tool.parameters,
1201
- }));
466
+ getKnowledgeBase() {
467
+ return { ...this.knowledgeBase };
468
+ }
469
+ /**
470
+ * Get the persistence manager (if configured)
471
+ */
472
+ getPersistenceManager() {
473
+ return this.persistenceManager;
474
+ }
475
+ /**
476
+ * Check if persistence is enabled
477
+ */
478
+ hasPersistence() {
479
+ return this.persistenceManager !== undefined;
480
+ }
481
+ /**
482
+ * Set the current session for convenience methods
483
+ * @param session - Session step to use for subsequent calls
484
+ */
485
+ setCurrentSession(session) {
486
+ this.currentSession = session;
487
+ }
488
+ /**
489
+ * Get the current session (if set)
490
+ */
491
+ getCurrentSession() {
492
+ return this.currentSession;
1202
493
  }
1203
494
  /**
1204
495
  * Execute a prepare or finalize function/tool
1205
- * @private
496
+ * @internal Used by ResponseModal
1206
497
  */
1207
498
  async executePrepareFinalize(prepareOrFinalize, context, data, route, step) {
1208
499
  if (!prepareOrFinalize)
@@ -1250,6 +541,7 @@ export class Agent {
1250
541
  tool,
1251
542
  context,
1252
543
  updateContext: this.updateContext.bind(this),
544
+ updateData: this.updateCollectedData.bind(this),
1253
545
  history: [], // Empty history for prepare/finalize
1254
546
  data,
1255
547
  });
@@ -1265,61 +557,6 @@ export class Agent {
1265
557
  }
1266
558
  }
1267
559
  }
1268
- /**
1269
- * Get all guidelines
1270
- */
1271
- getGuidelines() {
1272
- return [...this.guidelines];
1273
- }
1274
- /**
1275
- * Get the agent's knowledge base
1276
- */
1277
- getKnowledgeBase() {
1278
- return { ...this.knowledgeBase };
1279
- }
1280
- /**
1281
- * Merge terms with route-specific taking precedence on conflicts
1282
- * @private
1283
- */
1284
- mergeTerms(agentTerms, routeTerms) {
1285
- const merged = new Map();
1286
- // Add agent terms first
1287
- agentTerms.forEach((term) => {
1288
- const name = typeof term.name === "string" ? term.name : term.name.toString();
1289
- merged.set(name, term);
1290
- });
1291
- // Add route terms (these take precedence)
1292
- routeTerms.forEach((term) => {
1293
- const name = typeof term.name === "string" ? term.name : term.name.toString();
1294
- merged.set(name, term);
1295
- });
1296
- return Array.from(merged.values());
1297
- }
1298
- /**
1299
- * Get the persistence manager (if configured)
1300
- */
1301
- getPersistenceManager() {
1302
- return this.persistenceManager;
1303
- }
1304
- /**
1305
- * Check if persistence is enabled
1306
- */
1307
- hasPersistence() {
1308
- return this.persistenceManager !== undefined;
1309
- }
1310
- /**
1311
- * Set the current session for convenience methods
1312
- * @param session - Session step to use for subsequent calls
1313
- */
1314
- setCurrentSession(session) {
1315
- this.currentSession = session;
1316
- }
1317
- /**
1318
- * Get the current session (if set)
1319
- */
1320
- getCurrentSession() {
1321
- return this.currentSession;
1322
- }
1323
560
  /**
1324
561
  * Clear the current session
1325
562
  */
@@ -1327,19 +564,19 @@ export class Agent {
1327
564
  this.currentSession = undefined;
1328
565
  }
1329
566
  /**
1330
- * Get collected data from current session
567
+ * Get collected data from current session or agent-level collected data
1331
568
  * @param routeId - Optional route ID to get data for (uses current route if not provided)
1332
- * @returns The collected data from the current session
569
+ * @returns The collected data from the current session or agent-level data
1333
570
  */
1334
- getData(routeId) {
1335
- if (!this.currentSession) {
1336
- return {};
1337
- }
1338
- if (routeId) {
1339
- return (this.currentSession.dataByRoute?.[routeId] ||
1340
- {});
571
+ getData() {
572
+ // If we have a current session, use session data
573
+ if (this.currentSession) {
574
+ // With agent-level data, all routes share the same data structure
575
+ // No need for route-specific data access
576
+ return (this.currentSession.data) || {};
1341
577
  }
1342
- return this.currentSession.data || {};
578
+ // Otherwise, return agent-level collected data
579
+ return this.getCollectedData();
1343
580
  }
1344
581
  /**
1345
582
  * Manually transition to a different route
@@ -1382,14 +619,14 @@ export class Agent {
1382
619
  pendingTransition: {
1383
620
  targetRouteId: targetRoute.id,
1384
621
  condition: renderedCondition,
1385
- reason: "manual",
622
+ reason: "route_complete",
1386
623
  },
1387
624
  };
1388
625
  // Update current session if using it
1389
626
  if (!session && this.currentSession) {
1390
627
  this.currentSession = updatedSession;
1391
628
  }
1392
- logger.debug(`[Agent] Set pending manual transition to route: ${targetRoute.title}`);
629
+ logger.debug(`[Agent] Set pending transition to route: ${targetRoute.title}`);
1393
630
  return updatedSession;
1394
631
  }
1395
632
  /**
@@ -1397,33 +634,20 @@ export class Agent {
1397
634
  * Automatically manages conversation history through the session
1398
635
  */
1399
636
  async chat(message, options) {
1400
- // Determine which history to use
1401
- let history;
1402
- if (options?.history) {
1403
- // Use provided history for this response only
1404
- history = options.history;
1405
- }
1406
- else {
1407
- // Add user message to session history if provided
1408
- if (message) {
1409
- await this.session.addMessage("user", message);
1410
- }
1411
- history = this.session.getHistory();
1412
- }
1413
- // Get or create session
1414
- const session = await this.session.getOrCreate();
1415
- // Use existing respond method with session-managed history
1416
- const result = await this.respond({
1417
- history,
1418
- session,
637
+ // Delegate to ResponseModal.generate()
638
+ return this.responseModal.generate(message, options);
639
+ }
640
+ /**
641
+ * Modern streaming API - simple interface like chat() but returns a stream
642
+ * Automatically manages conversation history through the session
643
+ */
644
+ async *stream(message, options) {
645
+ // Delegate to ResponseModal with the same options structure as chat()
646
+ yield* this.responseModal.stream(message, {
647
+ history: options?.history,
1419
648
  contextOverride: options?.contextOverride,
1420
649
  signal: options?.signal,
1421
650
  });
1422
- // Add agent response to session history (only if not using override history)
1423
- if (!options?.history) {
1424
- await this.session.addMessage("assistant", result.message);
1425
- }
1426
- return result;
1427
651
  }
1428
652
  }
1429
653
  //# sourceMappingURL=Agent.js.map