@falai/agent 0.9.0-alpha-2 → 0.9.2

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