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