@falai/agent 0.9.0-alpha-2 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/dist/cjs/src/core/Agent.d.ts +31 -41
  2. package/dist/cjs/src/core/Agent.d.ts.map +1 -1
  3. package/dist/cjs/src/core/Agent.js +72 -1075
  4. package/dist/cjs/src/core/Agent.js.map +1 -1
  5. package/dist/cjs/src/core/ResponseModal.d.ts +205 -0
  6. package/dist/cjs/src/core/ResponseModal.d.ts.map +1 -0
  7. package/dist/cjs/src/core/ResponseModal.js +1328 -0
  8. package/dist/cjs/src/core/ResponseModal.js.map +1 -0
  9. package/dist/cjs/src/core/ResponsePipeline.js +1 -1
  10. package/dist/cjs/src/core/ResponsePipeline.js.map +1 -1
  11. package/dist/cjs/src/index.d.ts +3 -1
  12. package/dist/cjs/src/index.d.ts.map +1 -1
  13. package/dist/cjs/src/index.js +7 -1
  14. package/dist/cjs/src/index.js.map +1 -1
  15. package/dist/cjs/src/types/agent.d.ts +1 -0
  16. package/dist/cjs/src/types/agent.d.ts.map +1 -1
  17. package/dist/cjs/src/types/ai.d.ts +1 -1
  18. package/dist/cjs/src/types/ai.d.ts.map +1 -1
  19. package/dist/cjs/src/utils/clone.d.ts.map +1 -1
  20. package/dist/cjs/src/utils/clone.js +0 -4
  21. package/dist/cjs/src/utils/clone.js.map +1 -1
  22. package/dist/cjs/src/utils/history.d.ts +30 -1
  23. package/dist/cjs/src/utils/history.d.ts.map +1 -1
  24. package/dist/cjs/src/utils/history.js +169 -23
  25. package/dist/cjs/src/utils/history.js.map +1 -1
  26. package/dist/cjs/src/utils/index.d.ts +1 -1
  27. package/dist/cjs/src/utils/index.d.ts.map +1 -1
  28. package/dist/cjs/src/utils/index.js +5 -1
  29. package/dist/cjs/src/utils/index.js.map +1 -1
  30. package/dist/src/core/Agent.d.ts +31 -41
  31. package/dist/src/core/Agent.d.ts.map +1 -1
  32. package/dist/src/core/Agent.js +73 -1076
  33. package/dist/src/core/Agent.js.map +1 -1
  34. package/dist/src/core/ResponseModal.d.ts +205 -0
  35. package/dist/src/core/ResponseModal.d.ts.map +1 -0
  36. package/dist/src/core/ResponseModal.js +1323 -0
  37. package/dist/src/core/ResponseModal.js.map +1 -0
  38. package/dist/src/core/ResponsePipeline.js +1 -1
  39. package/dist/src/core/ResponsePipeline.js.map +1 -1
  40. package/dist/src/index.d.ts +3 -1
  41. package/dist/src/index.d.ts.map +1 -1
  42. package/dist/src/index.js +2 -1
  43. package/dist/src/index.js.map +1 -1
  44. package/dist/src/types/agent.d.ts +1 -0
  45. package/dist/src/types/agent.d.ts.map +1 -1
  46. package/dist/src/types/ai.d.ts +1 -1
  47. package/dist/src/types/ai.d.ts.map +1 -1
  48. package/dist/src/utils/clone.d.ts.map +1 -1
  49. package/dist/src/utils/clone.js +0 -4
  50. package/dist/src/utils/clone.js.map +1 -1
  51. package/dist/src/utils/history.d.ts +30 -1
  52. package/dist/src/utils/history.d.ts.map +1 -1
  53. package/dist/src/utils/history.js +165 -23
  54. package/dist/src/utils/history.js.map +1 -1
  55. package/dist/src/utils/index.d.ts +1 -1
  56. package/dist/src/utils/index.d.ts.map +1 -1
  57. package/dist/src/utils/index.js +1 -1
  58. package/dist/src/utils/index.js.map +1 -1
  59. package/docs/README.md +2 -1
  60. package/docs/api/README.md +160 -0
  61. package/docs/api/overview.md +66 -1
  62. package/docs/guides/migration/README.md +72 -0
  63. package/docs/guides/migration/response-modal-refactor.md +518 -0
  64. package/examples/advanced-patterns/streaming-responses.ts +169 -96
  65. package/examples/core-concepts/modern-streaming-api.ts +309 -0
  66. package/package.json +1 -1
  67. package/src/core/Agent.ts +95 -1488
  68. package/src/core/ResponseModal.ts +1722 -0
  69. package/src/core/ResponsePipeline.ts +1 -1
  70. package/src/index.ts +11 -0
  71. package/src/types/agent.ts +1 -0
  72. package/src/types/ai.ts +1 -1
  73. package/src/utils/clone.ts +6 -8
  74. package/src/utils/history.ts +190 -27
  75. package/src/utils/index.ts +4 -0
@@ -0,0 +1,1323 @@
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
+ import { EventKind, MessageRole } from "../types";
6
+ import { Step } from "./Step";
7
+ import { ResponseEngine } from "./ResponseEngine";
8
+ import { ResponsePipeline } from "./ResponsePipeline";
9
+ import { cloneDeep, mergeCollected, enterStep, getLastMessageFromHistory, render, logger, historyToEvents } from "../utils";
10
+ import { ToolExecutor } from "./ToolExecutor";
11
+ import { END_ROUTE_ID } from "../constants";
12
+ /**
13
+ * Error class for response generation failures
14
+ */
15
+ export class ResponseGenerationError extends Error {
16
+ constructor(message, details) {
17
+ super(message);
18
+ this.details = details;
19
+ this.name = 'ResponseGenerationError';
20
+ // Preserve stack trace from original error if available
21
+ if (details?.originalError instanceof Error && details.originalError.stack) {
22
+ this.stack = `${this.stack}\nCaused by: ${details.originalError.stack}`;
23
+ }
24
+ }
25
+ /**
26
+ * Create a ResponseGenerationError from an unknown error
27
+ */
28
+ static fromError(error, phase, params, context) {
29
+ const message = error instanceof Error ? error.message : String(error);
30
+ return new ResponseGenerationError(`Response generation failed in ${phase}: ${message}`, { originalError: error, params, phase, context });
31
+ }
32
+ /**
33
+ * Check if an error is a ResponseGenerationError
34
+ */
35
+ static isResponseGenerationError(error) {
36
+ return error instanceof ResponseGenerationError;
37
+ }
38
+ }
39
+ /**
40
+ * ResponseModal class that encapsulates all response generation logic
41
+ * Uses unified approach for both streaming and non-streaming responses
42
+ */
43
+ export class ResponseModal {
44
+ constructor(agent, options) {
45
+ this.agent = agent;
46
+ this.options = options;
47
+ // Initialize response engine
48
+ this.responseEngine = new ResponseEngine();
49
+ // Initialize response pipeline with agent dependencies
50
+ this.responsePipeline = new ResponsePipeline(this.agent.getAgentOptions(), this.agent.getRoutes(), this.agent.getTools(), this.agent.getRoutingEngine(), this.agent.updateContext.bind(this.agent), this.agent.getUpdateDataMethod(), this.agent.updateCollectedData.bind(this.agent));
51
+ }
52
+ /**
53
+ * Generate a non-streaming response using unified logic
54
+ */
55
+ async respond(params) {
56
+ try {
57
+ // Use unified response preparation and routing
58
+ const responseContext = await this.prepareUnifiedResponseContext(params);
59
+ // Generate response using unified logic
60
+ const result = await this.generateUnifiedResponse(responseContext);
61
+ // Finalize session
62
+ await this.finalizeSession(result.session, responseContext.effectiveContext);
63
+ return result;
64
+ }
65
+ catch (error) {
66
+ throw new ResponseGenerationError(`Failed to generate response: ${error instanceof Error ? error.message : String(error)}`, { originalError: error, params, phase: 'response_generation' });
67
+ }
68
+ }
69
+ /**
70
+ * Generate a streaming response using unified logic
71
+ */
72
+ async *respondStream(params) {
73
+ try {
74
+ // Use unified response preparation and routing
75
+ const responseContext = await this.prepareUnifiedResponseContext(params);
76
+ // Generate streaming response using unified logic
77
+ yield* this.generateUnifiedStreamingResponse(responseContext);
78
+ }
79
+ catch (error) {
80
+ // Stream error to caller
81
+ yield {
82
+ delta: "",
83
+ accumulated: "",
84
+ done: true,
85
+ session: params.session || await this.agent.session.getOrCreate(),
86
+ error: new ResponseGenerationError(`Streaming response failed: ${error instanceof Error ? error.message : String(error)}`, { originalError: error, params, phase: 'streaming' }),
87
+ };
88
+ }
89
+ }
90
+ /**
91
+ * Modern streaming API - simple interface like chat()
92
+ */
93
+ async *stream(message, options) {
94
+ // Determine which history to use
95
+ let history;
96
+ if (options?.history) {
97
+ // Use provided history for this response only
98
+ history = options.history;
99
+ }
100
+ else {
101
+ // Add user message to session history if provided
102
+ if (message) {
103
+ await this.agent.session.addMessage("user", message);
104
+ }
105
+ history = this.agent.session.getHistory();
106
+ }
107
+ // Get or create session
108
+ let session = await this.agent.session.getOrCreate();
109
+ // Merge agent's collected data into session (agent data takes precedence)
110
+ const collectedData = this.agent.getCollectedData();
111
+ if (Object.keys(collectedData).length > 0) {
112
+ session = mergeCollected(session, collectedData);
113
+ // Update the session manager with the merged data
114
+ await this.agent.session.setData(collectedData);
115
+ logger.debug("[ResponseModal] Merged agent collected data into stream session:", collectedData);
116
+ }
117
+ // Stream response using existing respondStream method
118
+ let finalMessage = "";
119
+ for await (const chunk of this.respondStream({
120
+ history,
121
+ session,
122
+ contextOverride: options?.contextOverride,
123
+ signal: options?.signal,
124
+ })) {
125
+ // Accumulate the final message for session history
126
+ if (chunk.done) {
127
+ finalMessage = chunk.accumulated;
128
+ }
129
+ yield chunk;
130
+ }
131
+ // Add agent response to session history (only if not using override history)
132
+ if (!options?.history && finalMessage) {
133
+ await this.agent.session.addMessage("assistant", finalMessage);
134
+ }
135
+ }
136
+ /**
137
+ * Modern non-streaming API - equivalent to chat() but more explicit
138
+ */
139
+ async generate(message, options) {
140
+ // Determine which history to use
141
+ let history;
142
+ if (options?.history) {
143
+ // Use provided history for this response only
144
+ history = options.history;
145
+ }
146
+ else {
147
+ // Add user message to session history if provided
148
+ if (message) {
149
+ await this.agent.session.addMessage("user", message);
150
+ }
151
+ history = this.agent.session.getHistory();
152
+ }
153
+ // Get or create session
154
+ let session = await this.agent.session.getOrCreate();
155
+ // Merge agent's collected data into session (agent data takes precedence)
156
+ const collectedData = this.agent.getCollectedData();
157
+ if (Object.keys(collectedData).length > 0) {
158
+ session = mergeCollected(session, collectedData);
159
+ // Update the session manager with the merged data
160
+ await this.agent.session.setData(collectedData);
161
+ logger.debug("[ResponseModal] Merged agent collected data into generate session:", collectedData);
162
+ }
163
+ // Generate response using existing respond method
164
+ const result = await this.respond({
165
+ history,
166
+ session,
167
+ contextOverride: options?.contextOverride,
168
+ signal: options?.signal,
169
+ });
170
+ // Add agent response to session history (only if not using override history)
171
+ if (!options?.history) {
172
+ await this.agent.session.addMessage("assistant", result.message);
173
+ }
174
+ // Ensure the result includes the current session
175
+ return {
176
+ ...result,
177
+ session: result.session || this.agent.session.current,
178
+ };
179
+ }
180
+ /**
181
+ * Get the response engine instance
182
+ * @internal
183
+ */
184
+ getResponseEngine() {
185
+ return this.responseEngine;
186
+ }
187
+ /**
188
+ * Get the response pipeline instance
189
+ * @internal
190
+ */
191
+ getResponsePipeline() {
192
+ return this.responsePipeline;
193
+ }
194
+ // UNIFIED RESPONSE LOGIC - Consolidates common logic between streaming and non-streaming
195
+ // ============================================================================
196
+ /**
197
+ * Unified response preparation - handles context setup, session management, and routing
198
+ * This consolidates common logic between streaming and non-streaming responses
199
+ * @private
200
+ */
201
+ async prepareUnifiedResponseContext(params) {
202
+ try {
203
+ const { history: simpleHistory, contextOverride, signal } = params;
204
+ // Validate input parameters
205
+ if (!simpleHistory) {
206
+ throw new ResponseGenerationError('History is required for response generation', { params, phase: 'validation' });
207
+ }
208
+ // Convert HistoryItem[] to Event[] for internal processing
209
+ const historyEvents = historyToEvents(simpleHistory);
210
+ // Keep original HistoryItem[] format for external APIs
211
+ const history = simpleHistory;
212
+ // Use ResponsePipeline for optimized context and session preparation
213
+ // This leverages existing optimizations and avoids code duplication
214
+ let responseContext;
215
+ try {
216
+ // Set current context and session in pipeline for consistency
217
+ this.responsePipeline.setContext(await this.agent.getContext());
218
+ this.responsePipeline.setCurrentSession(this.agent.getCurrentSession());
219
+ responseContext = await this.responsePipeline.prepareResponseContext({
220
+ contextOverride,
221
+ session: params.session ? cloneDeep(params.session) : undefined,
222
+ });
223
+ }
224
+ catch (error) {
225
+ throw ResponseGenerationError.fromError(error, 'pipeline_context_preparation', params);
226
+ }
227
+ const { effectiveContext } = responseContext;
228
+ let session = responseContext.session;
229
+ // Update our stored context if it was modified by beforeRespond hook
230
+ const storedContext = this.responsePipeline.getStoredContext();
231
+ if (storedContext !== undefined) {
232
+ try {
233
+ await this.agent.updateContext(storedContext);
234
+ }
235
+ catch (error) {
236
+ throw ResponseGenerationError.fromError(error, 'context_update_from_pipeline', params, { storedContext });
237
+ }
238
+ }
239
+ // Merge agent's collected data into session (agent data takes precedence)
240
+ const collectedData = this.agent.getCollectedData();
241
+ if (Object.keys(collectedData).length > 0) {
242
+ try {
243
+ session = mergeCollected(session, collectedData);
244
+ logger.debug("[ResponseModal] Merged agent collected data into session:", collectedData);
245
+ }
246
+ catch (error) {
247
+ throw ResponseGenerationError.fromError(error, 'data_merging', params, { collectedData });
248
+ }
249
+ }
250
+ // PHASE 1: PREPARE - Execute prepare function if current step has one
251
+ try {
252
+ await this.executeStepPrepare(session, effectiveContext);
253
+ }
254
+ catch (error) {
255
+ throw ResponseGenerationError.fromError(error, 'step_preparation', params, { session, effectiveContext });
256
+ }
257
+ // PHASE 2: ROUTING + STEP SELECTION - Determine which route and step to use
258
+ let routingResult;
259
+ try {
260
+ routingResult = await this.handleUnifiedRoutingAndStepSelection({
261
+ session,
262
+ history: historyEvents,
263
+ context: effectiveContext,
264
+ signal,
265
+ });
266
+ }
267
+ catch (error) {
268
+ throw ResponseGenerationError.fromError(error, 'routing_and_step_selection', params, { session, effectiveContext });
269
+ }
270
+ return {
271
+ effectiveContext,
272
+ session: routingResult.session,
273
+ history,
274
+ selectedRoute: routingResult.selectedRoute,
275
+ selectedStep: routingResult.selectedStep,
276
+ responseDirectives: routingResult.responseDirectives,
277
+ isRouteComplete: routingResult.isRouteComplete,
278
+ };
279
+ }
280
+ catch (error) {
281
+ // Re-throw ResponseGenerationError as-is, wrap others
282
+ if (ResponseGenerationError.isResponseGenerationError(error)) {
283
+ throw error;
284
+ }
285
+ throw ResponseGenerationError.fromError(error, 'preparation', params);
286
+ }
287
+ }
288
+ /**
289
+ * Unified routing and step selection logic using ResponsePipeline for optimization
290
+ * @private
291
+ */
292
+ async handleUnifiedRoutingAndStepSelection(params) {
293
+ try {
294
+ // Use the ResponsePipeline for optimized routing and step selection
295
+ // This avoids duplicate logic and leverages existing optimizations
296
+ // ResponsePipeline expects Event[] for history
297
+ const routingResult = await this.responsePipeline.handleRoutingAndStepSelection({
298
+ session: params.session,
299
+ history: params.history, // Already Event[]
300
+ context: params.context,
301
+ signal: params.signal,
302
+ });
303
+ // Determine next step using pipeline method for consistency
304
+ const stepResult = this.responsePipeline.determineNextStep({
305
+ selectedRoute: routingResult.selectedRoute,
306
+ selectedStep: routingResult.selectedStep,
307
+ session: routingResult.session,
308
+ isRouteComplete: routingResult.isRouteComplete,
309
+ });
310
+ return {
311
+ selectedRoute: routingResult.selectedRoute,
312
+ selectedStep: stepResult.nextStep, // Use the determined next step
313
+ responseDirectives: routingResult.responseDirectives,
314
+ session: stepResult.session,
315
+ isRouteComplete: routingResult.isRouteComplete,
316
+ };
317
+ }
318
+ catch (error) {
319
+ throw ResponseGenerationError.fromError(error, 'routing_optimization', params);
320
+ }
321
+ }
322
+ /**
323
+ * Unified response generation for non-streaming responses
324
+ * @private
325
+ */
326
+ async generateUnifiedResponse(responseContext) {
327
+ const { effectiveContext, session: initialSession, history, selectedRoute, selectedStep, responseDirectives, isRouteComplete } = responseContext;
328
+ let session = initialSession;
329
+ // Get last user message (needed for both route and completion handling)
330
+ // Convert HistoryItem[] to Event[] for internal processing
331
+ const historyEvents = historyToEvents(history);
332
+ const lastMessageText = getLastMessageFromHistory(historyEvents);
333
+ let message;
334
+ let toolCalls = undefined;
335
+ if (selectedRoute && !isRouteComplete) {
336
+ // Handle normal route processing
337
+ const result = await this.processRouteResponse({
338
+ selectedRoute,
339
+ selectedStep,
340
+ responseDirectives,
341
+ session,
342
+ history,
343
+ context: effectiveContext,
344
+ lastMessageText,
345
+ historyEvents,
346
+ signal: responseContext.history ? undefined : undefined, // TODO: Fix signal passing
347
+ });
348
+ message = result.message;
349
+ toolCalls = result.toolCalls;
350
+ session = result.session;
351
+ }
352
+ else if (isRouteComplete && selectedRoute) {
353
+ // Handle route completion
354
+ message = await this.handleRouteCompletion({
355
+ selectedRoute,
356
+ session,
357
+ context: effectiveContext,
358
+ lastMessageText,
359
+ historyEvents,
360
+ });
361
+ // Set step to END_ROUTE marker
362
+ session = enterStep(session, END_ROUTE_ID, "Route completed");
363
+ logger.debug(`[ResponseModal] Route ${selectedRoute.title} completed. Entered END_ROUTE step.`);
364
+ }
365
+ else {
366
+ // Fallback: No routes defined, generate a simple response
367
+ message = await this.generateFallbackResponse({
368
+ history: historyEvents, // Use Event[] for fallback response
369
+ context: effectiveContext,
370
+ session,
371
+ });
372
+ }
373
+ return {
374
+ message,
375
+ session,
376
+ toolCalls,
377
+ isRouteComplete,
378
+ };
379
+ }
380
+ /**
381
+ * Unified streaming response generation
382
+ * @private
383
+ */
384
+ async *generateUnifiedStreamingResponse(responseContext) {
385
+ const { effectiveContext, session: initialSession, history, selectedRoute, selectedStep, responseDirectives, isRouteComplete } = responseContext;
386
+ const session = initialSession;
387
+ // Get last user message (needed for both route and completion handling)
388
+ // Convert HistoryItem[] to Event[] for internal processing
389
+ const historyEvents = historyToEvents(history);
390
+ const lastMessageText = getLastMessageFromHistory(historyEvents);
391
+ if (selectedRoute && !isRouteComplete) {
392
+ // Handle normal route processing with streaming
393
+ yield* this.processRouteStreamingResponse({
394
+ selectedRoute,
395
+ selectedStep,
396
+ responseDirectives,
397
+ session,
398
+ history,
399
+ context: effectiveContext,
400
+ lastMessageText,
401
+ historyEvents,
402
+ });
403
+ }
404
+ else if (isRouteComplete && selectedRoute) {
405
+ // Handle route completion streaming
406
+ yield* this.streamRouteCompletion({
407
+ selectedRoute,
408
+ session,
409
+ context: effectiveContext,
410
+ lastMessageText,
411
+ historyEvents,
412
+ });
413
+ }
414
+ else {
415
+ // Fallback: No routes defined, stream a simple response
416
+ yield* this.streamFallbackResponse({
417
+ history: historyEvents, // Use Event[] for fallback response
418
+ context: effectiveContext,
419
+ session,
420
+ });
421
+ }
422
+ }
423
+ /**
424
+ * Execute prepare function for current step if available
425
+ * @private
426
+ */
427
+ async executeStepPrepare(session, context) {
428
+ if (session.currentRoute && session.currentStep) {
429
+ const currentRoute = this.agent.getRoutes().find((r) => r.id === session.currentRoute?.id);
430
+ if (currentRoute) {
431
+ const currentStep = currentRoute.getStep(session.currentStep.id);
432
+ if (currentStep?.prepare) {
433
+ logger.debug(`[ResponseModal] Executing prepare for step: ${currentStep.id}`);
434
+ await this.executePrepareFinalize(currentStep.prepare, context, session.data, currentRoute, currentStep);
435
+ }
436
+ }
437
+ }
438
+ }
439
+ /**
440
+ * Execute finalize function for current step if available
441
+ * @private
442
+ */
443
+ async executeStepFinalize(session, context) {
444
+ if (session.currentRoute && session.currentStep) {
445
+ const currentRoute = this.agent.getRoutes().find((r) => r.id === session.currentRoute?.id);
446
+ if (currentRoute) {
447
+ const currentStep = currentRoute.getStep(session.currentStep.id);
448
+ if (currentStep?.finalize) {
449
+ logger.debug(`[ResponseModal] Executing finalize for step: ${currentStep.id}`);
450
+ await this.executePrepareFinalize(currentStep.finalize, context, session.data, currentRoute, currentStep);
451
+ }
452
+ }
453
+ }
454
+ }
455
+ /**
456
+ * Process route response with unified tool execution and data collection
457
+ * @private
458
+ */
459
+ async processRouteResponse(params) {
460
+ const { selectedRoute, selectedStep, responseDirectives, history, context, lastMessageText, historyEvents, signal } = params;
461
+ let session = params.session;
462
+ // Determine next step
463
+ let nextStep;
464
+ if (selectedStep) {
465
+ nextStep = selectedStep;
466
+ }
467
+ else {
468
+ // New route or no step selected - get initial step or first valid step
469
+ const routingEngine = this.agent.getRoutingEngine();
470
+ const candidates = routingEngine.getCandidateSteps(selectedRoute, undefined, session.data || {});
471
+ if (candidates.length > 0) {
472
+ nextStep = candidates[0].step;
473
+ logger.debug(`[ResponseModal] Using first valid step: ${nextStep.id} for new route`);
474
+ }
475
+ else {
476
+ // Fallback to initial step even if it should be skipped
477
+ nextStep = selectedRoute.initialStep;
478
+ logger.warn(`[ResponseModal] No valid steps found, using initial step: ${nextStep.id}`);
479
+ }
480
+ }
481
+ // Update session with next step
482
+ session = enterStep(session, nextStep.id, nextStep.description);
483
+ logger.debug(`[ResponseModal] Entered step: ${nextStep.id}`);
484
+ // Build response schema for this route (with collect fields from step)
485
+ const responseSchema = this.responseEngine.responseSchemaForRoute(selectedRoute, nextStep, this.agent.getSchema());
486
+ // Build response prompt
487
+ const responsePrompt = await this.responseEngine.buildResponsePrompt({
488
+ route: selectedRoute,
489
+ currentStep: nextStep,
490
+ rules: selectedRoute.getRules(),
491
+ prohibitions: selectedRoute.getProhibitions(),
492
+ directives: responseDirectives,
493
+ history: historyEvents, // Use Event[] for buildResponsePrompt
494
+ lastMessage: lastMessageText, // Use string for buildResponsePrompt
495
+ agentOptions: this.agent.getAgentOptions(),
496
+ combinedGuidelines: [...this.agent.getGuidelines(), ...selectedRoute.getGuidelines()],
497
+ combinedTerms: this.mergeTerms(this.agent.getTerms(), selectedRoute.getTerms()),
498
+ context,
499
+ session,
500
+ agentSchema: this.agent.getSchema(),
501
+ });
502
+ // Collect available tools for AI
503
+ const availableTools = this.collectAvailableTools(selectedRoute, nextStep);
504
+ // Generate message using AI provider
505
+ const agentOptions = this.agent.getAgentOptions();
506
+ const result = await agentOptions.provider.generateMessage({
507
+ prompt: responsePrompt,
508
+ history: historyEvents, // Use Event[] for AI provider
509
+ context,
510
+ tools: availableTools,
511
+ signal,
512
+ parameters: responseSchema ? { jsonSchema: responseSchema, schemaName: "response_output" } : undefined,
513
+ });
514
+ let message = result.structured?.message || result.message;
515
+ let toolCalls = result.structured?.toolCalls;
516
+ // Execute tools with unified loop handling
517
+ const toolResult = await this.executeUnifiedToolLoop({
518
+ toolCalls,
519
+ context,
520
+ session,
521
+ history,
522
+ selectedRoute,
523
+ responsePrompt,
524
+ availableTools,
525
+ responseSchema,
526
+ signal,
527
+ });
528
+ session = toolResult.session;
529
+ toolCalls = toolResult.finalToolCalls;
530
+ if (toolResult.finalMessage) {
531
+ message = toolResult.finalMessage;
532
+ }
533
+ // Collect data from response
534
+ session = await this.collectDataFromResponse({ result, selectedRoute, nextStep, session });
535
+ return { message, toolCalls, session };
536
+ }
537
+ /**
538
+ * Process route streaming response with unified tool execution and data collection
539
+ * @private
540
+ */
541
+ async *processRouteStreamingResponse(params) {
542
+ const { selectedRoute, selectedStep, responseDirectives, history, context, lastMessageText, historyEvents, signal } = params;
543
+ let session = params.session;
544
+ // Determine next step (same logic as non-streaming)
545
+ let nextStep;
546
+ if (selectedStep) {
547
+ nextStep = selectedStep;
548
+ }
549
+ else {
550
+ const routingEngine = this.agent.getRoutingEngine();
551
+ const candidates = routingEngine.getCandidateSteps(selectedRoute, undefined, session.data || {});
552
+ if (candidates.length > 0) {
553
+ nextStep = candidates[0].step;
554
+ logger.debug(`[ResponseModal] Using first valid step: ${nextStep.id} for new route`);
555
+ }
556
+ else {
557
+ nextStep = selectedRoute.initialStep;
558
+ logger.warn(`[ResponseModal] No valid steps found, using initial step: ${nextStep.id}`);
559
+ }
560
+ }
561
+ // Update session with next step
562
+ session = enterStep(session, nextStep.id, nextStep.description);
563
+ logger.debug(`[ResponseModal] Entered step: ${nextStep.id}`);
564
+ // Build response schema and prompt (same as non-streaming)
565
+ const responseSchema = this.responseEngine.responseSchemaForRoute(selectedRoute, nextStep, this.agent.getSchema());
566
+ const responsePrompt = await this.responseEngine.buildResponsePrompt({
567
+ route: selectedRoute,
568
+ currentStep: nextStep,
569
+ rules: selectedRoute.getRules(),
570
+ prohibitions: selectedRoute.getProhibitions(),
571
+ directives: responseDirectives,
572
+ history: historyEvents, // Use Event[] for buildResponsePrompt
573
+ lastMessage: lastMessageText, // Use string for buildResponsePrompt
574
+ agentOptions: this.agent.getAgentOptions(),
575
+ combinedGuidelines: [...this.agent.getGuidelines(), ...selectedRoute.getGuidelines()],
576
+ combinedTerms: this.mergeTerms(this.agent.getTerms(), selectedRoute.getTerms()),
577
+ context,
578
+ session,
579
+ agentSchema: this.agent.getSchema(),
580
+ });
581
+ // Collect available tools for AI
582
+ const availableTools = this.collectAvailableTools(selectedRoute, nextStep);
583
+ // Generate message stream using AI provider
584
+ const agentOptions = this.agent.getAgentOptions();
585
+ const stream = agentOptions.provider.generateMessageStream({
586
+ prompt: responsePrompt,
587
+ history: historyEvents, // Use Event[] for AI provider
588
+ context,
589
+ tools: availableTools,
590
+ signal,
591
+ parameters: { jsonSchema: responseSchema, schemaName: "response_stream_output" },
592
+ });
593
+ // Stream chunks with unified tool handling
594
+ for await (const chunk of stream) {
595
+ let toolCalls = undefined;
596
+ // Extract tool calls from AI response on final chunk
597
+ if (chunk.done && chunk.structured?.toolCalls) {
598
+ toolCalls = chunk.structured.toolCalls;
599
+ // Execute tools with unified loop handling
600
+ const toolResult = await this.executeUnifiedToolLoop({
601
+ toolCalls,
602
+ context,
603
+ session,
604
+ history,
605
+ selectedRoute,
606
+ responsePrompt,
607
+ availableTools,
608
+ responseSchema,
609
+ signal,
610
+ });
611
+ session = toolResult.session;
612
+ toolCalls = toolResult.finalToolCalls;
613
+ }
614
+ // Extract collected data on final chunk
615
+ if (chunk.done && chunk.structured && nextStep.collect) {
616
+ session = await this.collectDataFromResponse({
617
+ result: { structured: chunk.structured },
618
+ selectedRoute,
619
+ nextStep,
620
+ session,
621
+ });
622
+ }
623
+ // Handle session finalization on final chunk
624
+ if (chunk.done) {
625
+ await this.finalizeSession(session, context);
626
+ }
627
+ yield {
628
+ delta: chunk.delta,
629
+ accumulated: chunk.accumulated,
630
+ done: chunk.done,
631
+ session,
632
+ toolCalls,
633
+ isRouteComplete: false,
634
+ metadata: chunk.metadata,
635
+ structured: chunk.structured,
636
+ };
637
+ }
638
+ }
639
+ /**
640
+ * Unified tool execution logic with loop handling
641
+ * Consolidates the complex tool execution logic from both streaming and non-streaming responses
642
+ * @private
643
+ */
644
+ async executeUnifiedToolLoop(params) {
645
+ try {
646
+ const { context, history, selectedRoute, responsePrompt, availableTools, responseSchema, signal } = params;
647
+ let { toolCalls, session } = params;
648
+ // Convert HistoryItem[] to Event[] for internal processing
649
+ const historyEvents = historyToEvents(history);
650
+ // Execute initial dynamic tool calls
651
+ if (toolCalls && toolCalls.length > 0) {
652
+ logger.debug(`[ResponseModal] Executing ${toolCalls.length} dynamic tool calls`);
653
+ for (const toolCall of toolCalls) {
654
+ const tool = this.findAvailableTool(toolCall.toolName, selectedRoute);
655
+ if (!tool) {
656
+ logger.warn(`[ResponseModal] Tool not found: ${toolCall.toolName}`);
657
+ continue;
658
+ }
659
+ try {
660
+ const toolExecutor = new ToolExecutor();
661
+ const toolResult = await toolExecutor.executeTool({
662
+ tool: tool,
663
+ context,
664
+ updateContext: this.agent.updateContext.bind(this.agent),
665
+ updateData: this.agent.updateCollectedData.bind(this.agent),
666
+ history: historyEvents, // Use Event[] for tool execution
667
+ data: session.data,
668
+ toolArguments: toolCall.arguments,
669
+ });
670
+ // Check if tool execution was successful
671
+ if (!toolResult.success) {
672
+ logger.error(`[ResponseModal] Tool execution failed: ${toolCall.toolName} - ${toolResult.error}`);
673
+ // Continue with other tools rather than failing completely
674
+ continue;
675
+ }
676
+ // Update context with tool results
677
+ if (toolResult.contextUpdate) {
678
+ try {
679
+ await this.agent.updateContext(toolResult.contextUpdate);
680
+ }
681
+ catch (error) {
682
+ logger.error(`[ResponseModal] Failed to update context from tool ${toolCall.toolName}:`, error);
683
+ // Continue execution but log the error
684
+ }
685
+ }
686
+ // Update collected data with tool results
687
+ if (toolResult.dataUpdate) {
688
+ try {
689
+ const updateDataMethod = this.agent.getUpdateDataMethod();
690
+ session = await updateDataMethod(session, toolResult.dataUpdate);
691
+ logger.debug(`[ResponseModal] Tool updated collected data:`, toolResult.dataUpdate);
692
+ }
693
+ catch (error) {
694
+ logger.error(`[ResponseModal] Failed to update data from tool ${toolCall.toolName}:`, error);
695
+ // Continue execution but log the error
696
+ }
697
+ }
698
+ logger.debug(`[ResponseModal] Executed dynamic tool: ${toolResult.toolName} (success: ${toolResult.success})`);
699
+ }
700
+ catch (error) {
701
+ logger.error(`[ResponseModal] Tool execution error for ${toolCall.toolName}:`, error);
702
+ // Continue with other tools rather than failing the entire response
703
+ continue;
704
+ }
705
+ }
706
+ }
707
+ // TOOL LOOP: Allow AI to make follow-up tool calls after initial tool execution
708
+ const MAX_TOOL_LOOPS = this.options?.maxToolLoops || 5;
709
+ let toolLoopCount = 0;
710
+ let hasToolCalls = toolCalls && toolCalls.length > 0;
711
+ let finalMessage;
712
+ while (hasToolCalls && toolLoopCount < MAX_TOOL_LOOPS) {
713
+ toolLoopCount++;
714
+ logger.debug(`[ResponseModal] Starting tool loop ${toolLoopCount}/${MAX_TOOL_LOOPS}`);
715
+ // Create tool result events with proper Event format structure
716
+ const toolResultEvents = [];
717
+ for (const toolCall of toolCalls || []) {
718
+ const tool = this.findAvailableTool(toolCall.toolName, selectedRoute);
719
+ if (tool) {
720
+ // Create proper Event format for tool results
721
+ const toolResultEvent = {
722
+ kind: EventKind.TOOL,
723
+ source: MessageRole.AGENT,
724
+ timestamp: new Date().toISOString(),
725
+ data: {
726
+ tool_calls: [
727
+ {
728
+ tool_id: toolCall.toolName,
729
+ arguments: toolCall.arguments,
730
+ result: {
731
+ data: "Tool executed successfully",
732
+ },
733
+ },
734
+ ],
735
+ },
736
+ };
737
+ toolResultEvents.push(toolResultEvent);
738
+ }
739
+ }
740
+ // Create updated history with tool results (combine Event arrays)
741
+ const updatedHistoryEvents = [...historyEvents, ...toolResultEvents];
742
+ // Make follow-up AI call to see if more tools are needed
743
+ const agentOptions = this.agent.getAgentOptions();
744
+ const followUpResult = await agentOptions.provider.generateMessage({
745
+ prompt: responsePrompt,
746
+ history: updatedHistoryEvents, // Use Event[] for AI provider
747
+ context,
748
+ tools: availableTools,
749
+ parameters: responseSchema ? {
750
+ jsonSchema: responseSchema,
751
+ schemaName: "tool_followup",
752
+ } : undefined,
753
+ signal,
754
+ });
755
+ // Check if follow-up call has more tool calls
756
+ const followUpToolCalls = followUpResult.structured?.toolCalls;
757
+ hasToolCalls = followUpToolCalls && followUpToolCalls.length > 0;
758
+ if (hasToolCalls) {
759
+ logger.debug(`[ResponseModal] Follow-up call produced ${followUpToolCalls.length} additional tool calls`);
760
+ // Execute the follow-up tool calls
761
+ for (const toolCall of followUpToolCalls) {
762
+ const tool = this.findAvailableTool(toolCall.toolName, selectedRoute);
763
+ if (!tool) {
764
+ logger.warn(`[ResponseModal] Tool not found in follow-up: ${toolCall.toolName}`);
765
+ continue;
766
+ }
767
+ try {
768
+ const toolExecutor = new ToolExecutor();
769
+ const toolResult = await toolExecutor.executeTool({
770
+ tool: tool,
771
+ context,
772
+ updateContext: this.agent.updateContext.bind(this.agent),
773
+ updateData: this.agent.updateCollectedData.bind(this.agent),
774
+ history: updatedHistoryEvents, // Use Event[] for tool execution
775
+ data: session.data,
776
+ toolArguments: toolCall.arguments,
777
+ });
778
+ // Check if tool execution was successful
779
+ if (!toolResult.success) {
780
+ logger.error(`[ResponseModal] Follow-up tool execution failed: ${toolCall.toolName} - ${toolResult.error}`);
781
+ continue;
782
+ }
783
+ // Update context with follow-up tool results
784
+ if (toolResult.contextUpdate) {
785
+ try {
786
+ await this.agent.updateContext(toolResult.contextUpdate);
787
+ }
788
+ catch (error) {
789
+ logger.error(`[ResponseModal] Failed to update context from follow-up tool ${toolCall.toolName}:`, error);
790
+ }
791
+ }
792
+ if (toolResult.dataUpdate) {
793
+ try {
794
+ const updateDataMethod = this.agent.getUpdateDataMethod();
795
+ session = await updateDataMethod(session, toolResult.dataUpdate);
796
+ logger.debug(`[ResponseModal] Follow-up tool updated collected data:`, toolResult.dataUpdate);
797
+ }
798
+ catch (error) {
799
+ logger.error(`[ResponseModal] Failed to update data from follow-up tool ${toolCall.toolName}:`, error);
800
+ }
801
+ }
802
+ logger.debug(`[ResponseModal] Executed follow-up tool: ${toolResult.toolName} (success: ${toolResult.success})`);
803
+ }
804
+ catch (error) {
805
+ logger.error(`[ResponseModal] Follow-up tool execution error for ${toolCall.toolName}:`, error);
806
+ continue;
807
+ }
808
+ }
809
+ // Update toolCalls for next iteration or final response
810
+ toolCalls = followUpToolCalls;
811
+ }
812
+ else {
813
+ logger.debug(`[ResponseModal] Tool loop completed after ${toolLoopCount} iterations`);
814
+ // Update final message and toolCalls from follow-up result if no more tools
815
+ finalMessage = followUpResult.structured?.message || followUpResult.message;
816
+ toolCalls = followUpToolCalls || [];
817
+ break;
818
+ }
819
+ }
820
+ if (toolLoopCount >= MAX_TOOL_LOOPS) {
821
+ logger.warn(`[ResponseModal] Tool loop limit reached (${MAX_TOOL_LOOPS}), stopping`);
822
+ }
823
+ return {
824
+ session,
825
+ finalToolCalls: toolCalls,
826
+ finalMessage,
827
+ };
828
+ }
829
+ catch (error) {
830
+ throw ResponseGenerationError.fromError(error, 'tool_execution', params, {
831
+ toolCallsCount: params.toolCalls?.length || 0,
832
+ availableToolsCount: params.availableTools.length
833
+ });
834
+ }
835
+ } /**
836
+ * Unified data collection from AI response
837
+ * @private
838
+ */
839
+ async collectDataFromResponse(params) {
840
+ try {
841
+ const { result, selectedRoute, nextStep, session } = params;
842
+ let updatedSession = session;
843
+ // Extract collected data from final response (only for route-based interactions)
844
+ if (selectedRoute && result.structured && nextStep?.collect) {
845
+ try {
846
+ const collectedData = {};
847
+ // AgentStructuredResponse extends Record<string, unknown>, so we can safely access properties
848
+ const structuredData = result.structured;
849
+ for (const field of nextStep.collect) {
850
+ const fieldKey = String(field);
851
+ if (fieldKey in structuredData) {
852
+ collectedData[fieldKey] = structuredData[fieldKey];
853
+ }
854
+ }
855
+ // Merge collected data into session using agent-level data validation
856
+ if (Object.keys(collectedData).length > 0) {
857
+ try {
858
+ // Update agent-level collected data with validation
859
+ await this.agent.updateCollectedData(collectedData);
860
+ // Update session with validated data
861
+ const updateDataMethod = this.agent.getUpdateDataMethod();
862
+ updatedSession = await updateDataMethod(updatedSession, collectedData);
863
+ logger.debug(`[ResponseModal] Collected data:`, collectedData);
864
+ }
865
+ catch (error) {
866
+ logger.error(`[ResponseModal] Failed to update collected data:`, error);
867
+ // Continue without updating data rather than failing completely
868
+ }
869
+ }
870
+ }
871
+ catch (error) {
872
+ logger.error(`[ResponseModal] Error during data collection:`, error);
873
+ // Continue without collecting data rather than failing completely
874
+ }
875
+ }
876
+ // Extract any additional data from structured response
877
+ // Since AgentStructuredResponse extends Record<string, unknown>, we can safely check for additional properties
878
+ if (result.structured && "contextUpdate" in result.structured) {
879
+ try {
880
+ const contextUpdate = result.structured.contextUpdate;
881
+ if (contextUpdate) {
882
+ await this.agent.updateContext(contextUpdate);
883
+ }
884
+ }
885
+ catch (error) {
886
+ logger.error(`[ResponseModal] Failed to update context from structured response:`, error);
887
+ // Continue without updating context rather than failing completely
888
+ }
889
+ }
890
+ return updatedSession;
891
+ }
892
+ catch (error) {
893
+ logger.error(`[ResponseModal] Error in collectDataFromResponse:`, error);
894
+ // Return original session if data collection fails completely
895
+ return params.session;
896
+ }
897
+ }
898
+ /**
899
+ * Handle route completion logic
900
+ * @private
901
+ */
902
+ async handleRouteCompletion(params) {
903
+ const { selectedRoute, session, context, lastMessageText, historyEvents, signal } = params;
904
+ // Get endStep spec from route
905
+ const endStepSpec = selectedRoute.endStepSpec;
906
+ // Create a temporary step for completion message generation using endStep configuration
907
+ const completionStep = new Step(selectedRoute.id, {
908
+ description: endStepSpec.description,
909
+ id: endStepSpec.id || END_ROUTE_ID,
910
+ collect: endStepSpec.collect,
911
+ requires: endStepSpec.requires,
912
+ prompt: endStepSpec.prompt || "Summarize what was accomplished and confirm completion based on the conversation history and collected data",
913
+ });
914
+ // Build response schema for completion
915
+ const responseSchema = this.responseEngine.responseSchemaForRoute(selectedRoute, completionStep, this.agent.getSchema());
916
+ const templateContext = { context, session, history: historyEvents }; // Use Event[] for template context
917
+ // Build completion response prompt
918
+ const completionPrompt = await this.responseEngine.buildResponsePrompt({
919
+ route: selectedRoute,
920
+ currentStep: completionStep,
921
+ rules: selectedRoute.getRules(),
922
+ prohibitions: selectedRoute.getProhibitions(),
923
+ directives: undefined, // No directives for completion
924
+ history: historyEvents, // Use Event[] for buildResponsePrompt
925
+ lastMessage: lastMessageText, // Use string for buildResponsePrompt
926
+ agentOptions: this.agent.getAgentOptions(),
927
+ combinedGuidelines: [...this.agent.getGuidelines(), ...selectedRoute.getGuidelines()],
928
+ combinedTerms: this.mergeTerms(this.agent.getTerms(), selectedRoute.getTerms()),
929
+ context,
930
+ session,
931
+ agentSchema: this.agent.getSchema(),
932
+ });
933
+ // Generate completion message using AI provider
934
+ const agentOptions = this.agent.getAgentOptions();
935
+ const completionResult = await agentOptions.provider.generateMessage({
936
+ prompt: completionPrompt,
937
+ history: historyEvents, // Use Event[] for AI provider
938
+ context,
939
+ signal,
940
+ parameters: { jsonSchema: responseSchema, schemaName: "completion_message" },
941
+ });
942
+ const message = completionResult.structured?.message || completionResult.message;
943
+ logger.debug(`[ResponseModal] Generated completion message for route: ${selectedRoute.title}`);
944
+ // Check for onComplete transition
945
+ const transitionConfig = await selectedRoute.evaluateOnComplete({ data: session.data }, context);
946
+ if (transitionConfig) {
947
+ // Find target route by ID or title
948
+ const targetRoute = this.agent.getRoutes().find((r) => r.id === transitionConfig.nextStep || r.title === transitionConfig.nextStep);
949
+ if (targetRoute) {
950
+ const renderedCondition = await render(transitionConfig.condition, templateContext);
951
+ // Set pending transition in session
952
+ session.pendingTransition = {
953
+ targetRouteId: targetRoute.id,
954
+ condition: renderedCondition,
955
+ reason: "route_complete",
956
+ };
957
+ logger.debug(`[ResponseModal] Route ${selectedRoute.title} completed with pending transition to: ${targetRoute.title}`);
958
+ }
959
+ else {
960
+ logger.warn(`[ResponseModal] Route ${selectedRoute.title} completed but target route not found: ${transitionConfig.nextStep}`);
961
+ }
962
+ }
963
+ return message;
964
+ }
965
+ /**
966
+ * Stream route completion response
967
+ * @private
968
+ */
969
+ async *streamRouteCompletion(params) {
970
+ const { selectedRoute, context, lastMessageText, historyEvents, signal } = params;
971
+ let session = params.session;
972
+ // Get endStep spec from route
973
+ const endStepSpec = selectedRoute.endStepSpec;
974
+ // Create a temporary step for completion message generation using endStep configuration
975
+ const completionStep = new Step(selectedRoute.id, {
976
+ description: endStepSpec.description,
977
+ id: endStepSpec.id || END_ROUTE_ID,
978
+ collect: endStepSpec.collect,
979
+ requires: endStepSpec.requires,
980
+ prompt: endStepSpec.prompt || "Summarize what was accomplished and confirm completion based on the conversation history and collected data",
981
+ });
982
+ // Build response schema for completion
983
+ const responseSchema = this.responseEngine.responseSchemaForRoute(selectedRoute, completionStep, this.agent.getSchema());
984
+ const templateContext = { context, session, history: historyEvents }; // Use Event[] for template context
985
+ // Build completion response prompt
986
+ const completionPrompt = await this.responseEngine.buildResponsePrompt({
987
+ route: selectedRoute,
988
+ currentStep: completionStep,
989
+ rules: selectedRoute.getRules(),
990
+ prohibitions: selectedRoute.getProhibitions(),
991
+ directives: undefined, // No directives for completion
992
+ history: historyEvents, // Use Event[] for buildResponsePrompt
993
+ lastMessage: lastMessageText, // Use string for buildResponsePrompt
994
+ agentOptions: this.agent.getAgentOptions(),
995
+ combinedGuidelines: [...this.agent.getGuidelines(), ...selectedRoute.getGuidelines()],
996
+ combinedTerms: this.mergeTerms(this.agent.getTerms(), selectedRoute.getTerms()),
997
+ context,
998
+ session,
999
+ agentSchema: this.agent.getSchema(),
1000
+ });
1001
+ // Stream completion message using AI provider
1002
+ const agentOptions = this.agent.getAgentOptions();
1003
+ const stream = agentOptions.provider.generateMessageStream({
1004
+ prompt: completionPrompt,
1005
+ history: historyEvents, // Use Event[] for AI provider
1006
+ context,
1007
+ signal,
1008
+ parameters: { jsonSchema: responseSchema, schemaName: "completion_message_stream" },
1009
+ });
1010
+ logger.debug(`[ResponseModal] Streaming completion message for route: ${selectedRoute.title}`);
1011
+ // Check for onComplete transition
1012
+ const transitionConfig = await selectedRoute.evaluateOnComplete({ data: session.data }, context);
1013
+ if (transitionConfig) {
1014
+ // Find target route by ID or title
1015
+ const targetRoute = this.agent.getRoutes().find((r) => r.id === transitionConfig.nextStep || r.title === transitionConfig.nextStep);
1016
+ if (targetRoute) {
1017
+ const renderedCondition = await render(transitionConfig.condition, templateContext);
1018
+ // Set pending transition in session
1019
+ session = {
1020
+ ...session,
1021
+ pendingTransition: {
1022
+ targetRouteId: targetRoute.id,
1023
+ condition: renderedCondition,
1024
+ reason: "route_complete",
1025
+ },
1026
+ };
1027
+ logger.debug(`[ResponseModal] Route ${selectedRoute.title} completed with pending transition to: ${targetRoute.title}`);
1028
+ }
1029
+ else {
1030
+ logger.warn(`[ResponseModal] Route ${selectedRoute.title} completed but target route not found: ${transitionConfig.nextStep}`);
1031
+ }
1032
+ }
1033
+ // Set step to END_ROUTE marker
1034
+ session = enterStep(session, END_ROUTE_ID, "Route completed");
1035
+ logger.debug(`[ResponseModal] Route ${selectedRoute.title} completed. Entered END_ROUTE step.`);
1036
+ // Stream completion chunks
1037
+ for await (const chunk of stream) {
1038
+ // Update current session if we have one
1039
+ if (chunk.done) {
1040
+ await this.finalizeSession(session, context);
1041
+ }
1042
+ yield {
1043
+ delta: chunk.delta,
1044
+ accumulated: chunk.accumulated,
1045
+ done: chunk.done,
1046
+ session,
1047
+ toolCalls: undefined,
1048
+ isRouteComplete: true,
1049
+ metadata: chunk.metadata,
1050
+ structured: chunk.structured,
1051
+ };
1052
+ }
1053
+ }
1054
+ /**
1055
+ * Generate fallback response when no routes are available
1056
+ * @private
1057
+ */
1058
+ async generateFallbackResponse(params) {
1059
+ const { history, context, session, signal } = params;
1060
+ logger.debug(`[ResponseModal] No route selected, generating basic response`);
1061
+ // Build basic response prompt without route context
1062
+ const fallbackPrompt = await this.responseEngine.buildFallbackPrompt({
1063
+ history,
1064
+ agentOptions: this.agent.getAgentOptions(),
1065
+ terms: this.agent.getTerms(),
1066
+ guidelines: this.agent.getGuidelines(),
1067
+ context,
1068
+ session,
1069
+ });
1070
+ const agentOptions = this.agent.getAgentOptions();
1071
+ const result = await agentOptions.provider.generateMessage({
1072
+ prompt: fallbackPrompt,
1073
+ history,
1074
+ context,
1075
+ signal,
1076
+ parameters: {
1077
+ jsonSchema: {
1078
+ type: "object",
1079
+ properties: { message: { type: "string" } },
1080
+ required: ["message"],
1081
+ additionalProperties: false,
1082
+ },
1083
+ schemaName: "fallback_response",
1084
+ },
1085
+ });
1086
+ return result.structured?.message || result.message;
1087
+ }
1088
+ /**
1089
+ * Stream fallback response when no routes are available
1090
+ * @private
1091
+ */
1092
+ async *streamFallbackResponse(params) {
1093
+ const { history, context, session, signal } = params;
1094
+ const fallbackPrompt = await this.responseEngine.buildFallbackPrompt({
1095
+ history,
1096
+ agentOptions: this.agent.getAgentOptions(),
1097
+ terms: this.agent.getTerms(),
1098
+ guidelines: this.agent.getGuidelines(),
1099
+ context,
1100
+ session,
1101
+ });
1102
+ const agentOptions = this.agent.getAgentOptions();
1103
+ const stream = agentOptions.provider.generateMessageStream({
1104
+ prompt: fallbackPrompt,
1105
+ history,
1106
+ context,
1107
+ signal,
1108
+ parameters: {
1109
+ jsonSchema: {
1110
+ type: "object",
1111
+ properties: { message: { type: "string" } },
1112
+ required: ["message"],
1113
+ additionalProperties: false,
1114
+ },
1115
+ schemaName: "fallback_stream_response",
1116
+ },
1117
+ });
1118
+ for await (const chunk of stream) {
1119
+ // Update current session if we have one
1120
+ if (chunk.done) {
1121
+ await this.finalizeSession(session, context);
1122
+ }
1123
+ yield {
1124
+ delta: chunk.delta,
1125
+ accumulated: chunk.accumulated,
1126
+ done: chunk.done,
1127
+ session,
1128
+ toolCalls: undefined,
1129
+ isRouteComplete: false,
1130
+ metadata: chunk.metadata,
1131
+ structured: chunk.structured,
1132
+ };
1133
+ }
1134
+ }
1135
+ /**
1136
+ * Handle session persistence and finalization
1137
+ * @private
1138
+ */
1139
+ async finalizeSession(session, context) {
1140
+ // Auto-save session step to persistence if configured
1141
+ const persistenceManager = this.agent.getPersistenceManager();
1142
+ const agentOptions = this.agent.getAgentOptions();
1143
+ if (persistenceManager &&
1144
+ session.id &&
1145
+ (this.options?.enableAutoSave !== false && agentOptions.persistence?.autoSave !== false)) {
1146
+ await persistenceManager.saveSessionState(session.id, session);
1147
+ logger.debug(`[ResponseModal] Auto-saved session step to persistence: ${session.id}`);
1148
+ }
1149
+ // Execute finalize function
1150
+ await this.executeStepFinalize(session, context);
1151
+ // Update current session if we have one
1152
+ const currentSession = this.agent.getCurrentSession();
1153
+ if (currentSession) {
1154
+ this.agent.setCurrentSession(session);
1155
+ }
1156
+ }
1157
+ // ============================================================================
1158
+ // UTILITY METHODS - Helper methods for tool management and other utilities
1159
+ // ============================================================================
1160
+ /**
1161
+ * Find an available tool by name for the given route
1162
+ * Route-level tools take precedence over agent-level tools
1163
+ * @private
1164
+ */
1165
+ findAvailableTool(toolName, route) {
1166
+ // Check route-level tools first (if route provided)
1167
+ if (route) {
1168
+ const routeTool = route
1169
+ .getTools()
1170
+ .find((tool) => tool.id === toolName || tool.name === toolName);
1171
+ if (routeTool)
1172
+ return routeTool;
1173
+ }
1174
+ // Fall back to agent-level tools
1175
+ const agentTools = this.agent.getTools();
1176
+ return agentTools.find((tool) => tool.id === toolName || tool.name === toolName);
1177
+ }
1178
+ /**
1179
+ * Collect all available tools for the given route and step context
1180
+ * @private
1181
+ */
1182
+ collectAvailableTools(route, step) {
1183
+ const availableTools = new Map();
1184
+ // Add agent-level tools
1185
+ this.agent.getTools().forEach((tool) => {
1186
+ availableTools.set(tool.id, tool);
1187
+ });
1188
+ // Add route-level tools (these take precedence)
1189
+ if (route) {
1190
+ route.getTools().forEach((tool) => {
1191
+ availableTools.set(tool.id, tool);
1192
+ });
1193
+ }
1194
+ // Filter by step-level allowed tools if specified
1195
+ if (step?.tools) {
1196
+ const allowedToolIds = new Set();
1197
+ const stepTools = [];
1198
+ for (const toolRef of step.tools) {
1199
+ if (typeof toolRef === "string") {
1200
+ // Reference to registered tool
1201
+ allowedToolIds.add(toolRef);
1202
+ }
1203
+ else {
1204
+ // Inline tool definition
1205
+ if (toolRef.id) {
1206
+ allowedToolIds.add(toolRef.id);
1207
+ stepTools.push(toolRef);
1208
+ }
1209
+ }
1210
+ }
1211
+ // If step specifies tools, only include those
1212
+ if (allowedToolIds.size > 0) {
1213
+ const filteredTools = new Map();
1214
+ for (const toolId of Array.from(allowedToolIds)) {
1215
+ const tool = availableTools.get(toolId);
1216
+ if (tool) {
1217
+ filteredTools.set(toolId, tool);
1218
+ }
1219
+ }
1220
+ // Add inline tools
1221
+ stepTools.forEach((tool) => {
1222
+ if (tool.id) {
1223
+ filteredTools.set(tool.id, tool);
1224
+ }
1225
+ });
1226
+ availableTools.clear();
1227
+ filteredTools.forEach((tool, id) => availableTools.set(id, tool));
1228
+ }
1229
+ }
1230
+ // Convert to the format expected by AI providers
1231
+ return Array.from(availableTools.values()).map((tool) => ({
1232
+ id: tool.id,
1233
+ name: tool.name || tool.id,
1234
+ description: tool.description,
1235
+ parameters: tool.parameters,
1236
+ }));
1237
+ }
1238
+ /**
1239
+ * Execute a prepare or finalize function/tool
1240
+ * @private
1241
+ */
1242
+ async executePrepareFinalize(prepareOrFinalize, context, data, route, step) {
1243
+ if (!prepareOrFinalize)
1244
+ return;
1245
+ if (typeof prepareOrFinalize === "function") {
1246
+ // It's a function - call it directly
1247
+ await prepareOrFinalize(context, data);
1248
+ }
1249
+ else {
1250
+ // It's a tool reference - find and execute the tool
1251
+ let tool;
1252
+ if (typeof prepareOrFinalize === "string") {
1253
+ // Tool ID - find it in available tools
1254
+ const availableTools = new Map();
1255
+ // Add agent-level tools
1256
+ this.agent.getTools().forEach((t) => {
1257
+ availableTools.set(t.id, t);
1258
+ });
1259
+ // Add route-level tools
1260
+ if (route) {
1261
+ route.getTools().forEach((t) => {
1262
+ availableTools.set(t.id, t);
1263
+ });
1264
+ }
1265
+ // Add step-level tools
1266
+ if (step?.tools) {
1267
+ for (const toolRef of step.tools) {
1268
+ if (typeof toolRef === "string") {
1269
+ // Keep as is
1270
+ }
1271
+ else if (typeof toolRef === 'object' && 'id' in toolRef && toolRef.id) {
1272
+ availableTools.set(toolRef.id, toolRef);
1273
+ }
1274
+ }
1275
+ }
1276
+ tool = availableTools.get(prepareOrFinalize);
1277
+ }
1278
+ else {
1279
+ // Tool object - use directly
1280
+ tool = prepareOrFinalize;
1281
+ }
1282
+ if (tool) {
1283
+ const toolExecutor = new ToolExecutor();
1284
+ const result = await toolExecutor.executeTool({
1285
+ tool,
1286
+ context,
1287
+ updateContext: this.agent.updateContext.bind(this.agent),
1288
+ updateData: this.agent.updateCollectedData.bind(this.agent),
1289
+ history: [], // Empty history for prepare/finalize
1290
+ data,
1291
+ });
1292
+ if (!result.success) {
1293
+ logger.error(`[ResponseModal] Tool execution failed in prepare/finalize: ${result.error}`);
1294
+ throw new Error(`Tool execution failed: ${result.error}`);
1295
+ }
1296
+ }
1297
+ else {
1298
+ logger.warn(`[ResponseModal] Tool not found for prepare/finalize: ${typeof prepareOrFinalize === "string"
1299
+ ? prepareOrFinalize
1300
+ : "inline tool"}`);
1301
+ }
1302
+ }
1303
+ }
1304
+ /**
1305
+ * Merge terms with route-specific taking precedence on conflicts
1306
+ * @private
1307
+ */
1308
+ mergeTerms(agentTerms, routeTerms) {
1309
+ const merged = new Map();
1310
+ // Add agent terms first
1311
+ agentTerms.forEach((term) => {
1312
+ const name = typeof term.name === "string" ? term.name : term.name.toString();
1313
+ merged.set(name, term);
1314
+ });
1315
+ // Add route terms (these take precedence)
1316
+ routeTerms.forEach((term) => {
1317
+ const name = typeof term.name === "string" ? term.name : term.name.toString();
1318
+ merged.set(name, term);
1319
+ });
1320
+ return Array.from(merged.values());
1321
+ }
1322
+ }
1323
+ //# sourceMappingURL=ResponseModal.js.map