@falai/agent 0.4.1 → 0.5.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 (285) hide show
  1. package/README.md +21 -74
  2. package/dist/cjs/core/Agent.d.ts +22 -29
  3. package/dist/cjs/core/Agent.d.ts.map +1 -1
  4. package/dist/cjs/core/Agent.js +464 -291
  5. package/dist/cjs/core/Agent.js.map +1 -1
  6. package/dist/cjs/core/Events.d.ts +10 -1
  7. package/dist/cjs/core/Events.d.ts.map +1 -1
  8. package/dist/cjs/core/Events.js +3 -2
  9. package/dist/cjs/core/Events.js.map +1 -1
  10. package/dist/cjs/core/PersistenceManager.d.ts +19 -0
  11. package/dist/cjs/core/PersistenceManager.d.ts.map +1 -1
  12. package/dist/cjs/core/PersistenceManager.js +57 -0
  13. package/dist/cjs/core/PersistenceManager.js.map +1 -1
  14. package/dist/cjs/core/PromptComposer.d.ts +24 -0
  15. package/dist/cjs/core/PromptComposer.d.ts.map +1 -0
  16. package/dist/cjs/core/PromptComposer.js +127 -0
  17. package/dist/cjs/core/PromptComposer.js.map +1 -0
  18. package/dist/cjs/core/ResponseEngine.d.ts +19 -0
  19. package/dist/cjs/core/ResponseEngine.d.ts.map +1 -0
  20. package/dist/cjs/core/ResponseEngine.js +51 -0
  21. package/dist/cjs/core/ResponseEngine.js.map +1 -0
  22. package/dist/cjs/core/Route.d.ts +18 -12
  23. package/dist/cjs/core/Route.d.ts.map +1 -1
  24. package/dist/cjs/core/Route.js +15 -9
  25. package/dist/cjs/core/Route.js.map +1 -1
  26. package/dist/cjs/core/RoutingEngine.d.ts +38 -0
  27. package/dist/cjs/core/RoutingEngine.d.ts.map +1 -0
  28. package/dist/cjs/core/RoutingEngine.js +110 -0
  29. package/dist/cjs/core/RoutingEngine.js.map +1 -0
  30. package/dist/cjs/core/State.d.ts +15 -4
  31. package/dist/cjs/core/State.d.ts.map +1 -1
  32. package/dist/cjs/core/State.js +21 -2
  33. package/dist/cjs/core/State.js.map +1 -1
  34. package/dist/cjs/core/ToolExecutor.d.ts +29 -0
  35. package/dist/cjs/core/ToolExecutor.d.ts.map +1 -0
  36. package/dist/cjs/core/ToolExecutor.js +73 -0
  37. package/dist/cjs/core/ToolExecutor.js.map +1 -0
  38. package/dist/cjs/core/Transition.d.ts +5 -5
  39. package/dist/cjs/core/Transition.d.ts.map +1 -1
  40. package/dist/cjs/core/Transition.js.map +1 -1
  41. package/dist/cjs/index.d.ts +6 -8
  42. package/dist/cjs/index.d.ts.map +1 -1
  43. package/dist/cjs/index.js +8 -10
  44. package/dist/cjs/index.js.map +1 -1
  45. package/dist/cjs/providers/AnthropicProvider.d.ts.map +1 -1
  46. package/dist/cjs/providers/AnthropicProvider.js +10 -13
  47. package/dist/cjs/providers/AnthropicProvider.js.map +1 -1
  48. package/dist/cjs/providers/GeminiProvider.d.ts.map +1 -1
  49. package/dist/cjs/providers/GeminiProvider.js +12 -8
  50. package/dist/cjs/providers/GeminiProvider.js.map +1 -1
  51. package/dist/cjs/providers/OpenAIProvider.d.ts.map +1 -1
  52. package/dist/cjs/providers/OpenAIProvider.js +10 -53
  53. package/dist/cjs/providers/OpenAIProvider.js.map +1 -1
  54. package/dist/cjs/providers/OpenRouterProvider.d.ts.map +1 -1
  55. package/dist/cjs/providers/OpenRouterProvider.js +10 -53
  56. package/dist/cjs/providers/OpenRouterProvider.js.map +1 -1
  57. package/dist/cjs/types/agent.d.ts +13 -12
  58. package/dist/cjs/types/agent.d.ts.map +1 -1
  59. package/dist/cjs/types/ai.d.ts +8 -2
  60. package/dist/cjs/types/ai.d.ts.map +1 -1
  61. package/dist/cjs/types/history.d.ts +8 -0
  62. package/dist/cjs/types/history.d.ts.map +1 -1
  63. package/dist/cjs/types/index.d.ts +0 -3
  64. package/dist/cjs/types/index.d.ts.map +1 -1
  65. package/dist/cjs/types/index.js +1 -3
  66. package/dist/cjs/types/index.js.map +1 -1
  67. package/dist/cjs/types/route.d.ts +39 -4
  68. package/dist/cjs/types/route.d.ts.map +1 -1
  69. package/dist/cjs/types/routing.d.ts +16 -0
  70. package/dist/cjs/types/routing.d.ts.map +1 -0
  71. package/dist/cjs/types/routing.js +3 -0
  72. package/dist/cjs/types/routing.js.map +1 -0
  73. package/dist/cjs/types/schema.d.ts +22 -0
  74. package/dist/cjs/types/schema.d.ts.map +1 -0
  75. package/dist/cjs/types/schema.js +3 -0
  76. package/dist/cjs/types/schema.js.map +1 -0
  77. package/dist/cjs/types/session.d.ts +72 -0
  78. package/dist/cjs/types/session.d.ts.map +1 -0
  79. package/dist/cjs/types/session.js +140 -0
  80. package/dist/cjs/types/session.js.map +1 -0
  81. package/dist/cjs/types/tool.d.ts +11 -5
  82. package/dist/cjs/types/tool.d.ts.map +1 -1
  83. package/dist/cjs/utils/id.d.ts +0 -5
  84. package/dist/cjs/utils/id.d.ts.map +1 -1
  85. package/dist/cjs/utils/id.js +0 -10
  86. package/dist/cjs/utils/id.js.map +1 -1
  87. package/dist/cjs/utils/schema.d.ts +17 -0
  88. package/dist/cjs/utils/schema.d.ts.map +1 -0
  89. package/dist/cjs/utils/schema.js +32 -0
  90. package/dist/cjs/utils/schema.js.map +1 -0
  91. package/dist/core/Agent.d.ts +22 -29
  92. package/dist/core/Agent.d.ts.map +1 -1
  93. package/dist/core/Agent.js +464 -291
  94. package/dist/core/Agent.js.map +1 -1
  95. package/dist/core/Events.d.ts +10 -1
  96. package/dist/core/Events.d.ts.map +1 -1
  97. package/dist/core/Events.js +3 -2
  98. package/dist/core/Events.js.map +1 -1
  99. package/dist/core/PersistenceManager.d.ts +19 -0
  100. package/dist/core/PersistenceManager.d.ts.map +1 -1
  101. package/dist/core/PersistenceManager.js +57 -0
  102. package/dist/core/PersistenceManager.js.map +1 -1
  103. package/dist/core/PromptComposer.d.ts +24 -0
  104. package/dist/core/PromptComposer.d.ts.map +1 -0
  105. package/dist/core/PromptComposer.js +123 -0
  106. package/dist/core/PromptComposer.js.map +1 -0
  107. package/dist/core/ResponseEngine.d.ts +19 -0
  108. package/dist/core/ResponseEngine.d.ts.map +1 -0
  109. package/dist/core/ResponseEngine.js +47 -0
  110. package/dist/core/ResponseEngine.js.map +1 -0
  111. package/dist/core/Route.d.ts +18 -12
  112. package/dist/core/Route.d.ts.map +1 -1
  113. package/dist/core/Route.js +15 -9
  114. package/dist/core/Route.js.map +1 -1
  115. package/dist/core/RoutingEngine.d.ts +38 -0
  116. package/dist/core/RoutingEngine.d.ts.map +1 -0
  117. package/dist/core/RoutingEngine.js +106 -0
  118. package/dist/core/RoutingEngine.js.map +1 -0
  119. package/dist/core/State.d.ts +15 -4
  120. package/dist/core/State.d.ts.map +1 -1
  121. package/dist/core/State.js +21 -2
  122. package/dist/core/State.js.map +1 -1
  123. package/dist/core/ToolExecutor.d.ts +29 -0
  124. package/dist/core/ToolExecutor.d.ts.map +1 -0
  125. package/dist/core/ToolExecutor.js +69 -0
  126. package/dist/core/ToolExecutor.js.map +1 -0
  127. package/dist/core/Transition.d.ts +5 -5
  128. package/dist/core/Transition.d.ts.map +1 -1
  129. package/dist/core/Transition.js.map +1 -1
  130. package/dist/index.d.ts +6 -8
  131. package/dist/index.d.ts.map +1 -1
  132. package/dist/index.js +3 -5
  133. package/dist/index.js.map +1 -1
  134. package/dist/providers/AnthropicProvider.d.ts.map +1 -1
  135. package/dist/providers/AnthropicProvider.js +10 -13
  136. package/dist/providers/AnthropicProvider.js.map +1 -1
  137. package/dist/providers/GeminiProvider.d.ts.map +1 -1
  138. package/dist/providers/GeminiProvider.js +12 -8
  139. package/dist/providers/GeminiProvider.js.map +1 -1
  140. package/dist/providers/OpenAIProvider.d.ts.map +1 -1
  141. package/dist/providers/OpenAIProvider.js +10 -53
  142. package/dist/providers/OpenAIProvider.js.map +1 -1
  143. package/dist/providers/OpenRouterProvider.d.ts.map +1 -1
  144. package/dist/providers/OpenRouterProvider.js +10 -53
  145. package/dist/providers/OpenRouterProvider.js.map +1 -1
  146. package/dist/types/agent.d.ts +13 -12
  147. package/dist/types/agent.d.ts.map +1 -1
  148. package/dist/types/ai.d.ts +8 -2
  149. package/dist/types/ai.d.ts.map +1 -1
  150. package/dist/types/history.d.ts +8 -0
  151. package/dist/types/history.d.ts.map +1 -1
  152. package/dist/types/index.d.ts +0 -3
  153. package/dist/types/index.d.ts.map +1 -1
  154. package/dist/types/index.js +0 -1
  155. package/dist/types/index.js.map +1 -1
  156. package/dist/types/route.d.ts +39 -4
  157. package/dist/types/route.d.ts.map +1 -1
  158. package/dist/types/routing.d.ts +16 -0
  159. package/dist/types/routing.d.ts.map +1 -0
  160. package/dist/types/routing.js +2 -0
  161. package/dist/types/routing.js.map +1 -0
  162. package/dist/types/schema.d.ts +22 -0
  163. package/dist/types/schema.d.ts.map +1 -0
  164. package/dist/types/schema.js +2 -0
  165. package/dist/types/schema.js.map +1 -0
  166. package/dist/types/session.d.ts +72 -0
  167. package/dist/types/session.d.ts.map +1 -0
  168. package/dist/types/session.js +132 -0
  169. package/dist/types/session.js.map +1 -0
  170. package/dist/types/tool.d.ts +11 -5
  171. package/dist/types/tool.d.ts.map +1 -1
  172. package/dist/utils/id.d.ts +0 -5
  173. package/dist/utils/id.d.ts.map +1 -1
  174. package/dist/utils/id.js +0 -9
  175. package/dist/utils/id.js.map +1 -1
  176. package/dist/utils/schema.d.ts +17 -0
  177. package/dist/utils/schema.d.ts.map +1 -0
  178. package/dist/utils/schema.js +27 -0
  179. package/dist/utils/schema.js.map +1 -0
  180. package/docs/ADAPTERS.md +83 -3
  181. package/docs/API_REFERENCE.md +95 -104
  182. package/docs/ARCHITECTURE.md +284 -286
  183. package/docs/CONSTRUCTOR_OPTIONS.md +192 -135
  184. package/docs/CONTEXT_MANAGEMENT.md +311 -28
  185. package/docs/CONTRIBUTING.md +1 -1
  186. package/docs/DOMAINS.md +61 -0
  187. package/docs/GETTING_STARTED.md +177 -88
  188. package/docs/PERSISTENCE.md +170 -23
  189. package/docs/README.md +7 -10
  190. package/examples/business-onboarding.ts +21 -9
  191. package/examples/company-qna-agent.ts +508 -0
  192. package/examples/declarative-agent.ts +143 -26
  193. package/examples/domain-scoping.ts +31 -10
  194. package/examples/extracted-data-modification.ts +415 -0
  195. package/examples/healthcare-agent.ts +194 -90
  196. package/examples/openai-agent.ts +67 -25
  197. package/examples/opensearch-persistence.ts +455 -151
  198. package/examples/persistent-onboarding.ts +162 -96
  199. package/examples/prisma-persistence.ts +371 -125
  200. package/examples/redis-persistence.ts +393 -23
  201. package/examples/rules-prohibitions.ts +32 -11
  202. package/examples/streaming-agent.ts +61 -13
  203. package/examples/travel-agent.ts +266 -133
  204. package/package.json +1 -1
  205. package/src/core/Agent.ts +674 -356
  206. package/src/core/Events.ts +12 -2
  207. package/src/core/PersistenceManager.ts +83 -0
  208. package/src/core/PromptComposer.ts +143 -0
  209. package/src/core/ResponseEngine.ts +82 -0
  210. package/src/core/Route.ts +32 -17
  211. package/src/core/RoutingEngine.ts +165 -0
  212. package/src/core/State.ts +55 -15
  213. package/src/core/ToolExecutor.ts +117 -0
  214. package/src/core/Transition.ts +5 -5
  215. package/src/index.ts +12 -21
  216. package/src/providers/AnthropicProvider.ts +10 -13
  217. package/src/providers/GeminiProvider.ts +12 -8
  218. package/src/providers/OpenAIProvider.ts +10 -56
  219. package/src/providers/OpenRouterProvider.ts +10 -56
  220. package/src/types/agent.ts +16 -13
  221. package/src/types/ai.ts +6 -2
  222. package/src/types/history.ts +8 -0
  223. package/src/types/index.ts +0 -11
  224. package/src/types/route.ts +41 -5
  225. package/src/types/routing.ts +18 -0
  226. package/src/types/schema.ts +23 -0
  227. package/src/types/session.ts +207 -0
  228. package/src/types/tool.ts +29 -7
  229. package/src/utils/id.ts +0 -10
  230. package/src/utils/schema.ts +32 -0
  231. package/dist/cjs/core/ConditionEvaluator.d.ts +0 -72
  232. package/dist/cjs/core/ConditionEvaluator.d.ts.map +0 -1
  233. package/dist/cjs/core/ConditionEvaluator.js +0 -272
  234. package/dist/cjs/core/ConditionEvaluator.js.map +0 -1
  235. package/dist/cjs/core/Observation.d.ts +0 -24
  236. package/dist/cjs/core/Observation.d.ts.map +0 -1
  237. package/dist/cjs/core/Observation.js +0 -39
  238. package/dist/cjs/core/Observation.js.map +0 -1
  239. package/dist/cjs/core/PreparationEngine.d.ts +0 -116
  240. package/dist/cjs/core/PreparationEngine.d.ts.map +0 -1
  241. package/dist/cjs/core/PreparationEngine.js +0 -353
  242. package/dist/cjs/core/PreparationEngine.js.map +0 -1
  243. package/dist/cjs/core/PromptBuilder.d.ts +0 -136
  244. package/dist/cjs/core/PromptBuilder.d.ts.map +0 -1
  245. package/dist/cjs/core/PromptBuilder.js +0 -421
  246. package/dist/cjs/core/PromptBuilder.js.map +0 -1
  247. package/dist/cjs/types/observation.d.ts +0 -27
  248. package/dist/cjs/types/observation.d.ts.map +0 -1
  249. package/dist/cjs/types/observation.js +0 -6
  250. package/dist/cjs/types/observation.js.map +0 -1
  251. package/dist/cjs/types/prompt.d.ts +0 -46
  252. package/dist/cjs/types/prompt.d.ts.map +0 -1
  253. package/dist/cjs/types/prompt.js +0 -19
  254. package/dist/cjs/types/prompt.js.map +0 -1
  255. package/dist/core/ConditionEvaluator.d.ts +0 -72
  256. package/dist/core/ConditionEvaluator.d.ts.map +0 -1
  257. package/dist/core/ConditionEvaluator.js +0 -268
  258. package/dist/core/ConditionEvaluator.js.map +0 -1
  259. package/dist/core/Observation.d.ts +0 -24
  260. package/dist/core/Observation.d.ts.map +0 -1
  261. package/dist/core/Observation.js +0 -35
  262. package/dist/core/Observation.js.map +0 -1
  263. package/dist/core/PreparationEngine.d.ts +0 -116
  264. package/dist/core/PreparationEngine.d.ts.map +0 -1
  265. package/dist/core/PreparationEngine.js +0 -349
  266. package/dist/core/PreparationEngine.js.map +0 -1
  267. package/dist/core/PromptBuilder.d.ts +0 -136
  268. package/dist/core/PromptBuilder.d.ts.map +0 -1
  269. package/dist/core/PromptBuilder.js +0 -417
  270. package/dist/core/PromptBuilder.js.map +0 -1
  271. package/dist/types/observation.d.ts +0 -27
  272. package/dist/types/observation.d.ts.map +0 -1
  273. package/dist/types/observation.js +0 -5
  274. package/dist/types/observation.js.map +0 -1
  275. package/dist/types/prompt.d.ts +0 -46
  276. package/dist/types/prompt.d.ts.map +0 -1
  277. package/dist/types/prompt.js +0 -16
  278. package/dist/types/prompt.js.map +0 -1
  279. package/docs/STRUCTURE.md +0 -58
  280. package/src/core/ConditionEvaluator.ts +0 -381
  281. package/src/core/Observation.ts +0 -47
  282. package/src/core/PreparationEngine.ts +0 -561
  283. package/src/core/PromptBuilder.ts +0 -617
  284. package/src/types/observation.ts +0 -29
  285. package/src/types/prompt.ts +0 -49
package/src/core/Agent.ts CHANGED
@@ -2,22 +2,41 @@
2
2
  * Core Agent implementation
3
3
  */
4
4
 
5
- import type {
6
- AgentOptions,
7
- Term,
8
- Guideline,
9
- GuidelineMatch,
10
- Capability,
11
- } from "../types/agent";
12
- import type { Event, StateRef } from "../types/index";
5
+ import type { AgentOptions, Term, Guideline, Capability } from "../types/agent";
6
+ import type { Event, StateRef, MessageEventData } from "../types/index";
13
7
  import type { RouteOptions } from "../types/route";
8
+ import type { RoutingDecisionOutput } from "./RoutingEngine";
9
+ import type { SessionState } from "../types/session";
10
+ import type { AgentStructuredResponse } from "../types/ai";
11
+ import {
12
+ createSession,
13
+ enterRoute,
14
+ enterState,
15
+ mergeExtracted,
16
+ } from "../types/session";
17
+ import { EventKind } from "../types/history";
18
+ import { PromptComposer } from "./PromptComposer";
14
19
 
15
20
  import { Route } from "./Route";
21
+ import { State } from "./State";
16
22
  import { DomainRegistry } from "./DomainRegistry";
17
- import { PromptBuilder } from "./PromptBuilder";
18
- import { Observation } from "./Observation";
19
23
  import { PersistenceManager } from "./PersistenceManager";
20
- import { PreparationEngine } from "./PreparationEngine";
24
+ import { RoutingEngine } from "./RoutingEngine";
25
+ import { ResponseEngine } from "./ResponseEngine";
26
+ import { ToolExecutor } from "./ToolExecutor";
27
+
28
+ /**
29
+ * Helper to extract last message from history
30
+ */
31
+ function getLastMessageFromHistory(history: Event[]): string {
32
+ for (let i = history.length - 1; i >= 0; i--) {
33
+ const event = history[i];
34
+ if (event.kind === EventKind.MESSAGE) {
35
+ return (event.data as MessageEventData).message;
36
+ }
37
+ }
38
+ return "";
39
+ }
21
40
 
22
41
  /**
23
42
  * Main Agent class with generic context support
@@ -26,12 +45,13 @@ export class Agent<TContext = unknown> {
26
45
  private terms: Term[] = [];
27
46
  private guidelines: Guideline[] = [];
28
47
  private capabilities: Capability[] = [];
29
- private routes: Route<TContext>[] = [];
30
- private observations: Observation[] = [];
48
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
49
+ private routes: Route<TContext, any>[] = [];
31
50
  private domainRegistry = new DomainRegistry();
32
51
  private context: TContext | undefined;
33
52
  private persistenceManager: PersistenceManager | undefined;
34
- private preparationEngine: PreparationEngine<TContext>;
53
+ private routingEngine: RoutingEngine<TContext>;
54
+ private responseEngine: ResponseEngine<TContext>;
35
55
 
36
56
  /**
37
57
  * Dynamic domain property - populated via addDomain
@@ -39,11 +59,6 @@ export class Agent<TContext = unknown> {
39
59
  public readonly domain: Record<string, Record<string, unknown>> = {};
40
60
 
41
61
  constructor(private readonly options: AgentOptions<TContext>) {
42
- // Initialize with default values
43
- if (!this.options.maxEngineIterations) {
44
- this.options.maxEngineIterations = 1;
45
- }
46
-
47
62
  // Validate context configuration
48
63
  if (options.context !== undefined && options.contextProvider) {
49
64
  throw new Error(
@@ -54,11 +69,13 @@ export class Agent<TContext = unknown> {
54
69
  // Initialize context if provided
55
70
  this.context = options.context;
56
71
 
57
- // Initialize preparation engine with AI provider and optional parallelism controls
58
- this.preparationEngine = new PreparationEngine<TContext>(options.ai, {
59
- maxParallelLlmCalls: options.preparation?.maxParallelLlmCalls,
60
- maxParallelTools: options.preparation?.maxParallelTools,
72
+ // Initialize routing and response engines
73
+ this.routingEngine = new RoutingEngine<TContext>({
74
+ maxCandidates: 5,
75
+ allowRouteSwitch: true,
76
+ switchThreshold: 70,
61
77
  });
78
+ this.responseEngine = new ResponseEngine<TContext>();
62
79
 
63
80
  // Initialize persistence if configured
64
81
  if (options.persistence) {
@@ -94,27 +111,7 @@ export class Agent<TContext = unknown> {
94
111
 
95
112
  if (options.routes) {
96
113
  options.routes.forEach((routeOptions) => {
97
- this.createRoute(routeOptions);
98
- });
99
- }
100
-
101
- if (options.observations) {
102
- options.observations.forEach((obsOptions) => {
103
- const obs = this.createObservation(obsOptions.description);
104
-
105
- // If route refs were provided, resolve and disambiguate
106
- if (obsOptions.routeRefs && obsOptions.routeRefs.length > 0) {
107
- const resolvedRoutes = obsOptions.routeRefs
108
- .map((ref) => {
109
- // Try to find route by ID or title
110
- return this.routes.find((r) => r.id === ref || r.title === ref);
111
- })
112
- .filter((r): r is Route<TContext> => r !== undefined);
113
-
114
- if (resolvedRoutes.length > 0) {
115
- obs.disambiguate(resolvedRoutes);
116
- }
117
- }
114
+ this.createRoute<unknown>(routeOptions);
118
115
  });
119
116
  }
120
117
  }
@@ -142,9 +139,12 @@ export class Agent<TContext = unknown> {
142
139
 
143
140
  /**
144
141
  * Create a new route (journey)
142
+ * @template TExtracted - Type of data extracted throughout the route
145
143
  */
146
- createRoute(options: RouteOptions): Route<TContext> {
147
- const route = new Route<TContext>(options);
144
+ createRoute<TExtracted = unknown>(
145
+ options: RouteOptions<TExtracted>
146
+ ): Route<TContext, TExtracted> {
147
+ const route = new Route<TContext, TExtracted>(options);
148
148
  this.routes.push(route);
149
149
  return route;
150
150
  }
@@ -182,25 +182,34 @@ export class Agent<TContext = unknown> {
182
182
  return this;
183
183
  }
184
184
 
185
- /**
186
- * Create an observation for disambiguation
187
- */
188
- createObservation(description: string): Observation {
189
- const observation = new Observation({ description });
190
- this.observations.push(observation);
191
- return observation;
192
- }
193
-
194
185
  /**
195
186
  * Add a domain with its tools/methods
187
+ * Automatically tags all ToolRef objects with their domain name for security enforcement
196
188
  */
197
189
  addDomain<TName extends string, TDomain extends Record<string, unknown>>(
198
190
  name: TName,
199
191
  domainObject: TDomain
200
192
  ): void {
201
- this.domainRegistry.register(name, domainObject);
193
+ // Tag all tools in this domain with the domain name for security enforcement
194
+ const taggedDomain = { ...domainObject };
195
+ for (const key in taggedDomain) {
196
+ const value = taggedDomain[key];
197
+ // Check if value is a ToolRef (has handler, id, name properties)
198
+ if (
199
+ value &&
200
+ typeof value === "object" &&
201
+ "handler" in value &&
202
+ "id" in value &&
203
+ "name" in value
204
+ ) {
205
+ // Tag the tool with its domain name
206
+ (value as Record<string, unknown>).domainName = name;
207
+ }
208
+ }
209
+
210
+ this.domainRegistry.register(name, taggedDomain);
202
211
  // Attach to the domain property for easy access
203
- this.domain[name] = domainObject;
212
+ this.domain[name] = taggedDomain;
204
213
  }
205
214
 
206
215
  /**
@@ -222,6 +231,36 @@ export class Agent<TContext = unknown> {
222
231
  }
223
232
  }
224
233
 
234
+ /**
235
+ * Update extracted data in session with lifecycle hook support
236
+ * Triggers the onExtractedUpdate lifecycle hook if configured
237
+ * @internal
238
+ */
239
+ private async updateExtracted<TExtracted = unknown>(
240
+ session: SessionState<TExtracted>,
241
+ extractedUpdate: Partial<TExtracted>
242
+ ): Promise<SessionState<TExtracted>> {
243
+ const previousExtracted = { ...session.extracted };
244
+
245
+ // Merge new extracted data
246
+ let newExtracted = {
247
+ ...session.extracted,
248
+ ...extractedUpdate,
249
+ };
250
+
251
+ // Trigger lifecycle hook if configured
252
+ if (this.options.hooks?.onExtractedUpdate) {
253
+ const updatedExtracted = (await this.options.hooks.onExtractedUpdate(
254
+ newExtracted,
255
+ previousExtracted
256
+ )) as Partial<TExtracted>;
257
+ newExtracted = updatedExtracted;
258
+ }
259
+
260
+ // Return updated session
261
+ return mergeExtracted(session, newExtracted);
262
+ }
263
+
225
264
  /**
226
265
  * Get current context (fetches from provider if configured)
227
266
  * @internal
@@ -236,20 +275,71 @@ export class Agent<TContext = unknown> {
236
275
  return this.context;
237
276
  }
238
277
 
278
+ /**
279
+ * Determine the next state in a route based on extracted data
280
+ * @internal
281
+ */
282
+ private getNextState<TExtracted = unknown>(
283
+ route: Route<TContext, TExtracted>,
284
+ currentState: State<TContext, TExtracted> | undefined,
285
+ extracted: Partial<TExtracted>
286
+ ): State<TContext, TExtracted> {
287
+ // If no current state, start from initial state
288
+ if (!currentState) {
289
+ // Check if initial state should be skipped
290
+ if (route.initialState.shouldSkip(extracted)) {
291
+ return this.getNextState(route, route.initialState, extracted);
292
+ }
293
+ return route.initialState;
294
+ }
295
+
296
+ // Get transitions from current state
297
+ const transitions = currentState.getTransitions();
298
+
299
+ // If no transitions, stay in current state
300
+ if (transitions.length === 0) {
301
+ return currentState;
302
+ }
303
+
304
+ // Try to find the next state to transition to
305
+ for (const transition of transitions) {
306
+ const target = transition.getTarget();
307
+ if (!target) continue;
308
+
309
+ // Check if target state should be skipped
310
+ if (target.shouldSkip(extracted)) {
311
+ // Recursively find next non-skipped state
312
+ return this.getNextState(route, target, extracted);
313
+ }
314
+
315
+ // Check if target state has required data
316
+ if (!target.hasRequiredData(extracted)) {
317
+ // Cannot enter this state yet, stay in current state
318
+ continue;
319
+ }
320
+
321
+ // Found valid next state
322
+ return target;
323
+ }
324
+
325
+ // No valid transition found, stay in current state
326
+ return currentState;
327
+ }
328
+
239
329
  /**
240
330
  * Generate a response based on history and context as a stream
241
331
  */
242
332
  async *respondStream(params: {
243
333
  history: Event[];
244
334
  state?: StateRef;
335
+ session?: SessionState;
245
336
  contextOverride?: Partial<TContext>;
246
337
  signal?: AbortSignal;
247
338
  }): AsyncGenerator<{
248
339
  delta: string;
249
340
  accumulated: string;
250
341
  done: boolean;
251
- route?: { id: string; title: string } | null;
252
- state?: { id: string; description?: string } | null;
342
+ session?: SessionState;
253
343
  toolCalls?: Array<{ toolName: string; arguments: Record<string, unknown> }>;
254
344
  }> {
255
345
  const { history, contextOverride, signal } = params;
@@ -265,184 +355,307 @@ export class Agent<TContext = unknown> {
265
355
  }
266
356
 
267
357
  // Merge context with override
268
- let effectiveContext = {
358
+ const effectiveContext = {
269
359
  ...(currentContext as Record<string, unknown>),
270
360
  ...(contextOverride as Record<string, unknown>),
271
361
  } as TContext;
272
362
 
273
- // RUN PREPARATION ITERATIONS
274
- // This is where tools execute automatically based on:
275
- // 1. Matched guidelines with associated tools
276
- // 2. State machine transitions with toolState
277
- //
278
- // The AI will NEVER see these tools - they execute before message generation
279
- const preparationResult = await this.preparationEngine.prepare({
280
- history,
281
- currentState: params.state,
282
- context: effectiveContext,
283
- routes: this.routes,
284
- guidelines: this.guidelines,
285
- maxIterations: this.options.maxEngineIterations || 1,
286
- });
287
-
288
- // Update context with results from tool executions
289
- effectiveContext = preparationResult.finalContext;
363
+ // Initialize or get session
364
+ let session = params.session || createSession();
290
365
 
291
- // Persist updated context and trigger lifecycle hook
292
- const previousStoredContext = this.context;
293
- this.context = effectiveContext;
294
- if (
295
- this.options.hooks?.onContextUpdate &&
296
- previousStoredContext !== undefined
297
- ) {
298
- await this.options.hooks.onContextUpdate(
299
- this.context,
300
- previousStoredContext
366
+ // PHASE 1: TOOL EXECUTION - Execute tools if current state has toolState
367
+ if (session.currentRoute && session.currentState) {
368
+ const currentRoute = this.routes.find(
369
+ (r) => r.id === session.currentRoute?.id
301
370
  );
371
+ if (currentRoute) {
372
+ const currentState = currentRoute.getState(session.currentState.id);
373
+ if (currentState) {
374
+ const transitions = currentState.getTransitions();
375
+ const toolTransition = transitions.find((t) => t.spec.toolState);
376
+
377
+ if (toolTransition?.spec.toolState) {
378
+ const toolExecutor = new ToolExecutor<TContext, unknown>();
379
+ // Get allowed domains from current route for security enforcement
380
+ const allowedDomains = currentRoute.getDomains();
381
+ const result = await toolExecutor.executeTool(
382
+ toolTransition.spec.toolState,
383
+ effectiveContext,
384
+ this.updateContext.bind(this),
385
+ history,
386
+ session.extracted,
387
+ allowedDomains
388
+ );
389
+
390
+ // Update context with tool results
391
+ if (result.contextUpdate) {
392
+ await this.updateContext(
393
+ result.contextUpdate as Partial<TContext>
394
+ );
395
+ }
396
+
397
+ // Update extracted data with tool results
398
+ if (result.extractedUpdate) {
399
+ session = await this.updateExtracted(
400
+ session,
401
+ result.extractedUpdate
402
+ );
403
+ console.log(
404
+ `[Agent] Tool updated extracted data:`,
405
+ result.extractedUpdate
406
+ );
407
+ }
408
+
409
+ console.log(
410
+ `[Agent] Executed tool: ${result.toolName} (success: ${result.success})`
411
+ );
412
+ }
413
+ }
414
+ }
302
415
  }
303
416
 
304
- // Log tool executions for debugging
305
- if (preparationResult.toolExecutions.length > 0) {
306
- console.log(
307
- `[Agent] Preparation complete: ${preparationResult.toolExecutions.length} tools executed in ${preparationResult.iterations.length} iterations`
417
+ // PHASE 2: ROUTING - Determine which route to use
418
+ let selectedRoute: Route<TContext> | undefined;
419
+ let responseDirectives: string[] | undefined;
420
+
421
+ if (this.routes.length > 0) {
422
+ // Get last user message
423
+ const lastUserMessage = getLastMessageFromHistory(history);
424
+
425
+ // Build routing schema
426
+ const routingSchema = this.routingEngine.buildDynamicRoutingSchema(
427
+ this.routes
308
428
  );
309
- }
310
429
 
311
- // Build prompt (same as respond method)
312
- const promptBuilder = new PromptBuilder();
430
+ // Build routing prompt with session context
431
+ const routingPrompt = this.routingEngine.buildRoutingPrompt(
432
+ history,
433
+ this.routes,
434
+ lastUserMessage,
435
+ {
436
+ name: this.options.name,
437
+ goal: this.options.goal,
438
+ description: this.options.description,
439
+ personality: this.options.personality,
440
+ },
441
+ session // Pass session for context-aware routing
442
+ );
313
443
 
314
- // Add agent identity
315
- if (this.options.description) {
316
- promptBuilder.addAgentIdentity({
317
- name: this.options.name,
318
- description: this.options.description,
444
+ // Call AI to score routes (non-streaming for routing decision)
445
+ const routingResult = await this.options.ai.generateMessage<
446
+ TContext,
447
+ RoutingDecisionOutput
448
+ >({
449
+ prompt: routingPrompt,
450
+ history,
451
+ context: effectiveContext,
452
+ signal,
453
+ parameters: {
454
+ jsonSchema: routingSchema,
455
+ schemaName: "routing_output",
456
+ },
319
457
  });
320
- }
321
458
 
322
- // Add interaction history
323
- promptBuilder.addInteractionHistoryForMessageGeneration(history);
459
+ // Select best route from scores
460
+ if (routingResult.structured?.routes) {
461
+ const decision = this.routingEngine.decideRouteFromScores({
462
+ context: routingResult.structured.context,
463
+ routes: routingResult.structured.routes,
464
+ responseDirectives: routingResult.structured.responseDirectives,
465
+ });
466
+ selectedRoute = this.routes.find((r) => r.id === decision.routeId);
467
+ responseDirectives = routingResult.structured.responseDirectives;
324
468
 
325
- // Add glossary
326
- if (this.terms.length > 0) {
327
- promptBuilder.addGlossary(this.terms);
328
- }
469
+ if (selectedRoute) {
470
+ console.log(
471
+ `[Agent] Selected route: ${selectedRoute.title} (score: ${decision.maxScore})`
472
+ );
329
473
 
330
- // Add guidelines (convert to GuidelineMatch format, filter enabled only)
331
- const enabledGuidelines = this.guidelines.filter(
332
- (g) => g.enabled !== false
333
- );
334
- if (enabledGuidelines.length > 0) {
335
- const guidelineMatches: GuidelineMatch[] = enabledGuidelines.map((g) => ({
336
- guideline: g,
337
- }));
338
- promptBuilder.addGuidelinesForMessageGeneration(guidelineMatches);
474
+ // Update session with selected route (if changed)
475
+ if (
476
+ !session.currentRoute ||
477
+ session.currentRoute.id !== selectedRoute.id
478
+ ) {
479
+ session = enterRoute(
480
+ session,
481
+ selectedRoute.id,
482
+ selectedRoute.title
483
+ );
484
+
485
+ // Merge initial data if provided by the route
486
+ if (selectedRoute.initialData) {
487
+ session = mergeExtracted(session, selectedRoute.initialData);
488
+ console.log(
489
+ `[Agent] Merged initial data:`,
490
+ selectedRoute.initialData
491
+ );
492
+ }
493
+
494
+ console.log(`[Agent] Entered route: ${selectedRoute.title}`);
495
+ }
496
+ }
497
+ }
339
498
  }
340
499
 
341
- // Add capabilities
342
- if (this.capabilities.length > 0) {
343
- promptBuilder.addCapabilitiesForMessageGeneration(this.capabilities);
344
- }
500
+ // PHASE 3: RESPONSE - Stream message using selected route
501
+ if (selectedRoute) {
502
+ // Determine next state based on current extracted data
503
+ const currentStateRef = session.currentState;
504
+ const currentState = currentStateRef
505
+ ? selectedRoute.getState(currentStateRef.id)
506
+ : undefined;
507
+ const nextState = this.getNextState(
508
+ selectedRoute,
509
+ currentState,
510
+ session.extracted
511
+ );
345
512
 
346
- // Add observations
347
- if (this.observations.length > 0) {
348
- const observationsWithRoutes = this.observations
349
- .map((obs) => ({
350
- description: obs.description,
351
- routes: obs.getRoutes().map((routeRef) => {
352
- const route = this.routes.find((r) => r.id === routeRef.id);
353
- return { title: route?.title || routeRef.id };
354
- }),
355
- }))
356
- .filter((obs) => obs.routes.length > 0);
357
-
358
- if (observationsWithRoutes.length > 0) {
359
- promptBuilder.addObservations(observationsWithRoutes);
360
- }
361
- }
513
+ // Update session with next state
514
+ session = enterState(session, nextState.id, nextState.description);
515
+ console.log(`[Agent] Entered state: ${nextState.id}`);
362
516
 
363
- // Add active routes with their rules and prohibitions
364
- if (this.routes.length > 0) {
365
- promptBuilder.addActiveRoutes(
366
- this.routes.map((r) => ({
367
- title: r.title,
368
- description: r.description,
369
- conditions: r.conditions,
370
- domains: r.getDomains(),
371
- rules: r.getRules(),
372
- prohibitions: r.getProhibitions(),
373
- }))
517
+ // Get last user message
518
+ const lastUserMessage = getLastMessageFromHistory(history);
519
+
520
+ // Build response schema for this route (with gather fields from state)
521
+ const responseSchema = this.responseEngine.responseSchemaForRoute(
522
+ selectedRoute,
523
+ nextState
374
524
  );
375
- }
376
525
 
377
- // NOTE: Domains/tools are NOT added to the prompt.
378
- // Tools execute automatically based on state transitions and guideline matching,
379
- // NOT based on AI decisions. The AI should never see available tools.
380
-
381
- // Add JSON response schema instructions
382
- promptBuilder.addJsonResponseSchema();
383
-
384
- // Build final prompt
385
- const prompt = promptBuilder.build();
386
-
387
- // Generate message stream using AI provider with JSON mode enabled
388
- const stream = this.options.ai.generateMessageStream({
389
- prompt,
390
- history,
391
- context: effectiveContext,
392
- signal,
393
- parameters: {
394
- jsonMode: true,
395
- },
396
- });
526
+ // Build response prompt
527
+ const responsePrompt = this.responseEngine.buildResponsePrompt(
528
+ selectedRoute,
529
+ selectedRoute.getRules(),
530
+ selectedRoute.getProhibitions(),
531
+ responseDirectives,
532
+ history,
533
+ lastUserMessage,
534
+ {
535
+ name: this.options.name,
536
+ goal: this.options.goal,
537
+ description: this.options.description,
538
+ personality: this.options.personality,
539
+ }
540
+ );
397
541
 
398
- // Stream chunks to caller
399
- for await (const chunk of stream) {
400
- // Extract route and state from structured response on final chunk
401
- let route: { id: string; title: string } | null = null;
402
- let state: { id: string; description?: string } | null = null;
403
- let toolCalls:
404
- | Array<{ toolName: string; arguments: Record<string, unknown> }>
405
- | undefined;
406
-
407
- if (chunk.done && chunk.structured) {
408
- // Find route by title
409
- if (chunk.structured.route) {
410
- const foundRoute = this.routes.find(
411
- (r) => r.title === chunk.structured?.route
412
- );
413
- if (foundRoute) {
414
- route = {
415
- id: foundRoute.id,
416
- title: foundRoute.title,
417
- };
542
+ // Generate message stream using AI provider
543
+ const stream = this.options.ai.generateMessageStream({
544
+ prompt: responsePrompt,
545
+ history,
546
+ context: effectiveContext,
547
+ signal,
548
+ parameters: {
549
+ jsonSchema: responseSchema,
550
+ schemaName: "response_stream_output",
551
+ },
552
+ });
553
+
554
+ // Stream chunks to caller
555
+ for await (const chunk of stream) {
556
+ const toolCalls:
557
+ | Array<{ toolName: string; arguments: Record<string, unknown> }>
558
+ | undefined = undefined;
559
+
560
+ // Extract gathered data on final chunk
561
+ if (chunk.done && chunk.structured && nextState.gatherFields) {
562
+ const gatheredData: Record<string, unknown> = {};
563
+ // The structured response includes both base fields and gathered extraction fields
564
+ const structuredData = chunk.structured as AgentStructuredResponse &
565
+ Record<string, unknown>;
566
+
567
+ for (const field of nextState.gatherFields) {
568
+ if (field in structuredData) {
569
+ gatheredData[field] = structuredData[field];
570
+ }
571
+ }
572
+
573
+ // Merge gathered data into session
574
+ if (Object.keys(gatheredData).length > 0) {
575
+ session = await this.updateExtracted(session, gatheredData);
576
+ console.log(`[Agent] Extracted data:`, gatheredData);
418
577
  }
419
578
  }
420
579
 
421
- // Create state reference if provided
422
- if (chunk.structured.state) {
423
- state = {
424
- id: "dynamic_state",
425
- description: chunk.structured.state,
426
- };
580
+ // Extract any additional data from structured response on final chunk
581
+ if (
582
+ chunk.done &&
583
+ chunk.structured &&
584
+ typeof chunk.structured === "object" &&
585
+ "contextUpdate" in chunk.structured
586
+ ) {
587
+ await this.updateContext(
588
+ (chunk.structured as { contextUpdate?: Partial<TContext> })
589
+ .contextUpdate as Partial<TContext>
590
+ );
427
591
  }
428
592
 
429
- // Extract tool calls
593
+ // Auto-save session state on final chunk
430
594
  if (
431
- chunk.structured.toolCalls &&
432
- chunk.structured.toolCalls.length > 0
595
+ chunk.done &&
596
+ this.persistenceManager &&
597
+ session.metadata?.sessionId &&
598
+ this.options.persistence?.autoSave !== false
433
599
  ) {
434
- toolCalls = chunk.structured.toolCalls;
600
+ await this.persistenceManager.saveSessionState(
601
+ session.metadata.sessionId,
602
+ session
603
+ );
604
+ console.log(
605
+ `[Agent] Auto-saved session state to persistence: ${session.metadata.sessionId}`
606
+ );
435
607
  }
608
+
609
+ yield {
610
+ delta: chunk.delta,
611
+ accumulated: chunk.accumulated,
612
+ done: chunk.done,
613
+ session, // Return updated session
614
+ toolCalls,
615
+ };
436
616
  }
617
+ } else {
618
+ // Fallback: No routes defined, stream a simple response
619
+ const fallbackPrompt = new PromptComposer<TContext>()
620
+ .addAgentMeta({
621
+ name: this.options.name,
622
+ goal: this.options.goal,
623
+ description: this.options.description,
624
+ })
625
+ .addPersonality(this.options.personality)
626
+ .addInteractionHistory(history)
627
+ .addGlossary(this.terms)
628
+ .addGuidelines(this.guidelines)
629
+ .addCapabilities(this.capabilities)
630
+ .build();
631
+
632
+ const stream = this.options.ai.generateMessageStream({
633
+ prompt: fallbackPrompt,
634
+ history,
635
+ context: effectiveContext,
636
+ signal,
637
+ parameters: {
638
+ jsonSchema: {
639
+ type: "object",
640
+ properties: {
641
+ message: { type: "string" },
642
+ },
643
+ required: ["message"],
644
+ additionalProperties: false,
645
+ },
646
+ schemaName: "fallback_stream_response",
647
+ },
648
+ });
437
649
 
438
- yield {
439
- delta: chunk.delta,
440
- accumulated: chunk.accumulated,
441
- done: chunk.done,
442
- route: route || undefined,
443
- state: state || undefined,
444
- toolCalls,
445
- };
650
+ for await (const chunk of stream) {
651
+ yield {
652
+ delta: chunk.delta,
653
+ accumulated: chunk.accumulated,
654
+ done: chunk.done,
655
+ session, // Return updated session
656
+ toolCalls: undefined,
657
+ };
658
+ }
446
659
  }
447
660
  }
448
661
 
@@ -452,12 +665,12 @@ export class Agent<TContext = unknown> {
452
665
  async respond(params: {
453
666
  history: Event[];
454
667
  state?: StateRef;
668
+ session?: SessionState;
455
669
  contextOverride?: Partial<TContext>;
456
670
  signal?: AbortSignal;
457
671
  }): Promise<{
458
672
  message: string;
459
- route?: { id: string; title: string } | null;
460
- state?: { id: string; description?: string } | null;
673
+ session?: SessionState;
461
674
  toolCalls?: Array<{ toolName: string; arguments: Record<string, unknown> }>;
462
675
  }> {
463
676
  const { history, contextOverride, signal } = params;
@@ -473,182 +686,294 @@ export class Agent<TContext = unknown> {
473
686
  }
474
687
 
475
688
  // Merge context with override
476
- let effectiveContext = {
689
+ const effectiveContext = {
477
690
  ...(currentContext as Record<string, unknown>),
478
691
  ...(contextOverride as Record<string, unknown>),
479
692
  } as TContext;
480
693
 
481
- // RUN PREPARATION ITERATIONS
482
- // This is where tools execute automatically based on:
483
- // 1. Matched guidelines with associated tools
484
- // 2. State machine transitions with toolState
485
- //
486
- // The AI will NEVER see these tools - they execute before message generation
487
- const preparationResult = await this.preparationEngine.prepare({
488
- history,
489
- currentState: params.state,
490
- context: effectiveContext,
491
- routes: this.routes,
492
- guidelines: this.guidelines,
493
- maxIterations: this.options.maxEngineIterations || 1,
494
- });
694
+ // Initialize or get session
695
+ let session = params.session || createSession();
495
696
 
496
- // Update context with results from tool executions
497
- effectiveContext = preparationResult.finalContext;
498
-
499
- // Persist updated context and trigger lifecycle hook
500
- const previousStoredContext = this.context;
501
- this.context = effectiveContext;
502
- if (
503
- this.options.hooks?.onContextUpdate &&
504
- previousStoredContext !== undefined
505
- ) {
506
- await this.options.hooks.onContextUpdate(
507
- this.context,
508
- previousStoredContext
697
+ // PHASE 1: TOOL EXECUTION - Execute tools if current state has toolState
698
+ if (session.currentRoute && session.currentState) {
699
+ const currentRoute = this.routes.find(
700
+ (r) => r.id === session.currentRoute?.id
509
701
  );
702
+ if (currentRoute) {
703
+ const currentState = currentRoute.getState(session.currentState.id);
704
+ if (currentState) {
705
+ const transitions = currentState.getTransitions();
706
+ const toolTransition = transitions.find((t) => t.spec.toolState);
707
+
708
+ if (toolTransition?.spec.toolState) {
709
+ const toolExecutor = new ToolExecutor<TContext, unknown>();
710
+ // Get allowed domains from current route for security enforcement
711
+ const allowedDomains = currentRoute.getDomains();
712
+ const result = await toolExecutor.executeTool(
713
+ toolTransition.spec.toolState,
714
+ effectiveContext,
715
+ this.updateContext.bind(this),
716
+ history,
717
+ session.extracted,
718
+ allowedDomains
719
+ );
720
+
721
+ // Update context with tool results
722
+ if (result.contextUpdate) {
723
+ await this.updateContext(
724
+ result.contextUpdate as Partial<TContext>
725
+ );
726
+ }
727
+
728
+ // Update extracted data with tool results
729
+ if (result.extractedUpdate) {
730
+ session = await this.updateExtracted(
731
+ session,
732
+ result.extractedUpdate
733
+ );
734
+ console.log(
735
+ `[Agent] Tool updated extracted data:`,
736
+ result.extractedUpdate
737
+ );
738
+ }
739
+
740
+ console.log(
741
+ `[Agent] Executed tool: ${result.toolName} (success: ${result.success})`
742
+ );
743
+ }
744
+ }
745
+ }
510
746
  }
511
747
 
512
- // Log tool executions for debugging
513
- if (preparationResult.toolExecutions.length > 0) {
514
- console.log(
515
- `[Agent] Preparation complete: ${preparationResult.toolExecutions.length} tools executed in ${preparationResult.iterations.length} iterations`
748
+ // PHASE 2: ROUTING - Determine which route to use
749
+ let selectedRoute: Route<TContext> | undefined;
750
+ let responseDirectives: string[] | undefined;
751
+
752
+ if (this.routes.length > 0) {
753
+ // Get last user message
754
+ const lastUserMessage = getLastMessageFromHistory(history);
755
+
756
+ // Build routing schema
757
+ const routingSchema = this.routingEngine.buildDynamicRoutingSchema(
758
+ this.routes
516
759
  );
517
- }
518
760
 
519
- // Build prompt
520
- const promptBuilder = new PromptBuilder();
761
+ // Build routing prompt with session context
762
+ const routingPrompt = this.routingEngine.buildRoutingPrompt(
763
+ history,
764
+ this.routes,
765
+ lastUserMessage,
766
+ {
767
+ name: this.options.name,
768
+ goal: this.options.goal,
769
+ description: this.options.description,
770
+ personality: this.options.personality,
771
+ },
772
+ session // Pass session for context-aware routing
773
+ );
521
774
 
522
- // Add agent identity
523
- if (this.options.description) {
524
- promptBuilder.addAgentIdentity({
525
- name: this.options.name,
526
- description: this.options.description,
775
+ // Call AI to score routes
776
+ const routingResult = await this.options.ai.generateMessage<
777
+ TContext,
778
+ RoutingDecisionOutput
779
+ >({
780
+ prompt: routingPrompt,
781
+ history,
782
+ context: effectiveContext,
783
+ signal,
784
+ parameters: {
785
+ jsonSchema: routingSchema,
786
+ schemaName: "routing_output",
787
+ },
527
788
  });
528
- }
529
789
 
530
- // Add interaction history
531
- promptBuilder.addInteractionHistoryForMessageGeneration(history);
790
+ // Select best route from scores
791
+ if (routingResult.structured?.routes) {
792
+ const decision = this.routingEngine.decideRouteFromScores({
793
+ context: routingResult.structured.context,
794
+ routes: routingResult.structured.routes,
795
+ responseDirectives: routingResult.structured.responseDirectives,
796
+ });
797
+ selectedRoute = this.routes.find((r) => r.id === decision.routeId);
798
+ responseDirectives = routingResult.structured.responseDirectives;
532
799
 
533
- // Add glossary
534
- if (this.terms.length > 0) {
535
- promptBuilder.addGlossary(this.terms);
536
- }
800
+ if (selectedRoute) {
801
+ console.log(
802
+ `[Agent] Selected route: ${selectedRoute.title} (score: ${decision.maxScore})`
803
+ );
537
804
 
538
- // Add guidelines (convert to GuidelineMatch format, filter enabled only)
539
- const enabledGuidelines = this.guidelines.filter(
540
- (g) => g.enabled !== false
541
- );
542
- if (enabledGuidelines.length > 0) {
543
- const guidelineMatches: GuidelineMatch[] = enabledGuidelines.map((g) => ({
544
- guideline: g,
545
- }));
546
- promptBuilder.addGuidelinesForMessageGeneration(guidelineMatches);
805
+ // Update session with selected route (if changed)
806
+ if (
807
+ !session.currentRoute ||
808
+ session.currentRoute.id !== selectedRoute.id
809
+ ) {
810
+ session = enterRoute(
811
+ session,
812
+ selectedRoute.id,
813
+ selectedRoute.title
814
+ );
815
+
816
+ // Merge initial data if provided by the route
817
+ if (selectedRoute.initialData) {
818
+ session = mergeExtracted(session, selectedRoute.initialData);
819
+ console.log(
820
+ `[Agent] Merged initial data:`,
821
+ selectedRoute.initialData
822
+ );
823
+ }
824
+
825
+ console.log(`[Agent] Entered route: ${selectedRoute.title}`);
826
+ }
827
+ }
828
+ }
547
829
  }
548
830
 
549
- // Add capabilities
550
- if (this.capabilities.length > 0) {
551
- promptBuilder.addCapabilitiesForMessageGeneration(this.capabilities);
552
- }
831
+ // PHASE 3: RESPONSE - Generate message using selected route
832
+ let message: string;
833
+ const toolCalls:
834
+ | Array<{ toolName: string; arguments: Record<string, unknown> }>
835
+ | undefined = undefined;
836
+
837
+ if (selectedRoute) {
838
+ // Determine next state based on current extracted data
839
+ const currentStateRef = session.currentState;
840
+ const currentState = currentStateRef
841
+ ? selectedRoute.getState(currentStateRef.id)
842
+ : undefined;
843
+ const nextState = this.getNextState(
844
+ selectedRoute,
845
+ currentState,
846
+ session.extracted
847
+ );
553
848
 
554
- // Add observations
555
- if (this.observations.length > 0) {
556
- const observationsWithRoutes = this.observations
557
- .map((obs) => ({
558
- description: obs.description,
559
- routes: obs.getRoutes().map((routeRef) => {
560
- const route = this.routes.find((r) => r.id === routeRef.id);
561
- return { title: route?.title || routeRef.id };
562
- }),
563
- }))
564
- .filter((obs) => obs.routes.length > 0);
565
-
566
- if (observationsWithRoutes.length > 0) {
567
- promptBuilder.addObservations(observationsWithRoutes);
568
- }
569
- }
849
+ // Update session with next state
850
+ session = enterState(session, nextState.id, nextState.description);
851
+ console.log(`[Agent] Entered state: ${nextState.id}`);
570
852
 
571
- // Add active routes with their rules and prohibitions
572
- if (this.routes.length > 0) {
573
- promptBuilder.addActiveRoutes(
574
- this.routes.map((r) => ({
575
- title: r.title,
576
- description: r.description,
577
- conditions: r.conditions,
578
- domains: r.getDomains(),
579
- rules: r.getRules(),
580
- prohibitions: r.getProhibitions(),
581
- }))
853
+ // Get last user message
854
+ const lastUserMessage = getLastMessageFromHistory(history);
855
+
856
+ // Build response schema for this route (with gather fields from state)
857
+ const responseSchema = this.responseEngine.responseSchemaForRoute(
858
+ selectedRoute,
859
+ nextState
582
860
  );
583
- }
584
861
 
585
- // NOTE: Domains/tools are NOT added to the prompt.
586
- // Tools execute automatically based on state transitions and guideline matching,
587
- // NOT based on AI decisions. The AI should never see available tools.
588
-
589
- // Add JSON response schema instructions
590
- promptBuilder.addJsonResponseSchema();
591
-
592
- // Build final prompt
593
- const prompt = promptBuilder.build();
594
-
595
- // Generate message using AI provider with JSON mode enabled
596
- const result = await this.options.ai.generateMessage({
597
- prompt,
598
- history,
599
- context: effectiveContext,
600
- signal,
601
- parameters: {
602
- jsonMode: true,
603
- },
604
- });
862
+ // Build response prompt
863
+ const responsePrompt = this.responseEngine.buildResponsePrompt(
864
+ selectedRoute,
865
+ selectedRoute.getRules(),
866
+ selectedRoute.getProhibitions(),
867
+ responseDirectives,
868
+ history,
869
+ lastUserMessage,
870
+ {
871
+ name: this.options.name,
872
+ goal: this.options.goal,
873
+ description: this.options.description,
874
+ personality: this.options.personality,
875
+ }
876
+ );
605
877
 
606
- // Parse structured response
607
- let message = result.message;
608
- let route: { id: string; title: string } | null = null;
609
- let state: { id: string; description?: string } | null = null;
610
- let toolCalls:
611
- | Array<{ toolName: string; arguments: Record<string, unknown> }>
612
- | undefined;
878
+ // Generate message using AI provider
879
+ const result = await this.options.ai.generateMessage({
880
+ prompt: responsePrompt,
881
+ history,
882
+ context: effectiveContext,
883
+ signal,
884
+ parameters: {
885
+ jsonSchema: responseSchema,
886
+ schemaName: "response_output",
887
+ },
888
+ });
613
889
 
614
- if (result.structured) {
615
- // Extract data from structured response
616
- message = result.structured.message || message;
890
+ message = result.structured?.message || result.message;
617
891
 
618
- // Find route by title
619
- if (result.structured.route) {
620
- const foundRoute = this.routes.find(
621
- (r) => r.title === result.structured?.route
622
- );
623
- if (foundRoute) {
624
- route = {
625
- id: foundRoute.id,
626
- title: foundRoute.title,
627
- };
892
+ // Extract gathered data from response
893
+ if (result.structured && nextState.gatherFields) {
894
+ const gatheredData: Record<string, unknown> = {};
895
+ // The structured response includes both base fields and gathered extraction fields
896
+ const structuredData = result.structured as AgentStructuredResponse &
897
+ Record<string, unknown>;
898
+
899
+ for (const field of nextState.gatherFields) {
900
+ if (field in structuredData) {
901
+ gatheredData[field] = structuredData[field];
902
+ }
628
903
  }
629
- }
630
904
 
631
- // Create state reference if provided
632
- if (result.structured.state) {
633
- state = {
634
- id: "dynamic_state",
635
- description: result.structured.state,
636
- };
905
+ // Merge gathered data into session
906
+ if (Object.keys(gatheredData).length > 0) {
907
+ session = mergeExtracted(session, gatheredData);
908
+ console.log(`[Agent] Extracted data:`, gatheredData);
909
+ }
637
910
  }
638
911
 
639
- // Extract tool calls
912
+ // Extract any additional data from structured response
640
913
  if (
641
- result.structured.toolCalls &&
642
- result.structured.toolCalls.length > 0
914
+ result.structured &&
915
+ typeof result.structured === "object" &&
916
+ "contextUpdate" in result.structured
643
917
  ) {
644
- toolCalls = result.structured.toolCalls;
918
+ await this.updateContext(
919
+ (result.structured as { contextUpdate?: Partial<TContext> })
920
+ .contextUpdate as Partial<TContext>
921
+ );
645
922
  }
923
+ } else {
924
+ // Fallback: No routes defined, generate a simple response
925
+ const fallbackPrompt = new PromptComposer<TContext>()
926
+ .addAgentMeta({
927
+ name: this.options.name,
928
+ goal: this.options.goal,
929
+ description: this.options.description,
930
+ })
931
+ .addPersonality(this.options.personality)
932
+ .addInteractionHistory(history)
933
+ .addGlossary(this.terms)
934
+ .addGuidelines(this.guidelines)
935
+ .addCapabilities(this.capabilities)
936
+ .build();
937
+
938
+ const result = await this.options.ai.generateMessage({
939
+ prompt: fallbackPrompt,
940
+ history,
941
+ context: effectiveContext,
942
+ signal,
943
+ parameters: {
944
+ jsonSchema: {
945
+ type: "object",
946
+ properties: {
947
+ message: { type: "string" },
948
+ },
949
+ required: ["message"],
950
+ additionalProperties: false,
951
+ },
952
+ schemaName: "fallback_response",
953
+ },
954
+ });
955
+
956
+ message = result.structured?.message || result.message;
957
+ }
958
+
959
+ // Auto-save session state to persistence if configured
960
+ if (
961
+ this.persistenceManager &&
962
+ session.metadata?.sessionId &&
963
+ this.options.persistence?.autoSave !== false
964
+ ) {
965
+ await this.persistenceManager.saveSessionState(
966
+ session.metadata.sessionId,
967
+ session
968
+ );
969
+ console.log(
970
+ `[Agent] Auto-saved session state to persistence: ${session.metadata.sessionId}`
971
+ );
646
972
  }
647
973
 
648
974
  return {
649
975
  message,
650
- route: route || undefined,
651
- state: state || undefined,
976
+ session, // Return updated session with route/state info
652
977
  toolCalls,
653
978
  };
654
979
  }
@@ -656,7 +981,7 @@ export class Agent<TContext = unknown> {
656
981
  /**
657
982
  * Get all routes
658
983
  */
659
- getRoutes(): Route<TContext>[] {
984
+ getRoutes(): Route<TContext, unknown>[] {
660
985
  return [...this.routes];
661
986
  }
662
987
 
@@ -681,13 +1006,6 @@ export class Agent<TContext = unknown> {
681
1006
  return [...this.capabilities];
682
1007
  }
683
1008
 
684
- /**
685
- * Get all observations
686
- */
687
- getObservations(): Observation[] {
688
- return [...this.observations];
689
- }
690
-
691
1009
  /**
692
1010
  * Get the domain registry
693
1011
  */