@falai/agent 0.4.0 → 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 +465 -275
  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 -9
  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 +465 -275
  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 -9
  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 +679 -332
  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 -10
  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 -105
  240. package/dist/cjs/core/PreparationEngine.d.ts.map +0 -1
  241. package/dist/cjs/core/PreparationEngine.js +0 -320
  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 -105
  264. package/dist/core/PreparationEngine.d.ts.map +0 -1
  265. package/dist/core/PreparationEngine.js +0 -316
  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 -500
  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,8 +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
58
- this.preparationEngine = new PreparationEngine<TContext>(options.ai);
72
+ // Initialize routing and response engines
73
+ this.routingEngine = new RoutingEngine<TContext>({
74
+ maxCandidates: 5,
75
+ allowRouteSwitch: true,
76
+ switchThreshold: 70,
77
+ });
78
+ this.responseEngine = new ResponseEngine<TContext>();
59
79
 
60
80
  // Initialize persistence if configured
61
81
  if (options.persistence) {
@@ -91,27 +111,7 @@ export class Agent<TContext = unknown> {
91
111
 
92
112
  if (options.routes) {
93
113
  options.routes.forEach((routeOptions) => {
94
- this.createRoute(routeOptions);
95
- });
96
- }
97
-
98
- if (options.observations) {
99
- options.observations.forEach((obsOptions) => {
100
- const obs = this.createObservation(obsOptions.description);
101
-
102
- // If route refs were provided, resolve and disambiguate
103
- if (obsOptions.routeRefs && obsOptions.routeRefs.length > 0) {
104
- const resolvedRoutes = obsOptions.routeRefs
105
- .map((ref) => {
106
- // Try to find route by ID or title
107
- return this.routes.find((r) => r.id === ref || r.title === ref);
108
- })
109
- .filter((r): r is Route<TContext> => r !== undefined);
110
-
111
- if (resolvedRoutes.length > 0) {
112
- obs.disambiguate(resolvedRoutes);
113
- }
114
- }
114
+ this.createRoute<unknown>(routeOptions);
115
115
  });
116
116
  }
117
117
  }
@@ -139,9 +139,12 @@ export class Agent<TContext = unknown> {
139
139
 
140
140
  /**
141
141
  * Create a new route (journey)
142
+ * @template TExtracted - Type of data extracted throughout the route
142
143
  */
143
- createRoute(options: RouteOptions): Route<TContext> {
144
- 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);
145
148
  this.routes.push(route);
146
149
  return route;
147
150
  }
@@ -179,25 +182,34 @@ export class Agent<TContext = unknown> {
179
182
  return this;
180
183
  }
181
184
 
182
- /**
183
- * Create an observation for disambiguation
184
- */
185
- createObservation(description: string): Observation {
186
- const observation = new Observation({ description });
187
- this.observations.push(observation);
188
- return observation;
189
- }
190
-
191
185
  /**
192
186
  * Add a domain with its tools/methods
187
+ * Automatically tags all ToolRef objects with their domain name for security enforcement
193
188
  */
194
189
  addDomain<TName extends string, TDomain extends Record<string, unknown>>(
195
190
  name: TName,
196
191
  domainObject: TDomain
197
192
  ): void {
198
- 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);
199
211
  // Attach to the domain property for easy access
200
- this.domain[name] = domainObject;
212
+ this.domain[name] = taggedDomain;
201
213
  }
202
214
 
203
215
  /**
@@ -219,6 +231,36 @@ export class Agent<TContext = unknown> {
219
231
  }
220
232
  }
221
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
+
222
264
  /**
223
265
  * Get current context (fetches from provider if configured)
224
266
  * @internal
@@ -233,20 +275,71 @@ export class Agent<TContext = unknown> {
233
275
  return this.context;
234
276
  }
235
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
+
236
329
  /**
237
330
  * Generate a response based on history and context as a stream
238
331
  */
239
332
  async *respondStream(params: {
240
333
  history: Event[];
241
334
  state?: StateRef;
335
+ session?: SessionState;
242
336
  contextOverride?: Partial<TContext>;
243
337
  signal?: AbortSignal;
244
338
  }): AsyncGenerator<{
245
339
  delta: string;
246
340
  accumulated: string;
247
341
  done: boolean;
248
- route?: { id: string; title: string } | null;
249
- state?: { id: string; description?: string } | null;
342
+ session?: SessionState;
250
343
  toolCalls?: Array<{ toolName: string; arguments: Record<string, unknown> }>;
251
344
  }> {
252
345
  const { history, contextOverride, signal } = params;
@@ -262,171 +355,307 @@ export class Agent<TContext = unknown> {
262
355
  }
263
356
 
264
357
  // Merge context with override
265
- let effectiveContext = {
358
+ const effectiveContext = {
266
359
  ...(currentContext as Record<string, unknown>),
267
360
  ...(contextOverride as Record<string, unknown>),
268
361
  } as TContext;
269
362
 
270
- // RUN PREPARATION ITERATIONS
271
- // This is where tools execute automatically based on:
272
- // 1. Matched guidelines with associated tools
273
- // 2. State machine transitions with toolState
274
- //
275
- // The AI will NEVER see these tools - they execute before message generation
276
- const preparationResult = await this.preparationEngine.prepare({
277
- history,
278
- currentState: params.state,
279
- context: effectiveContext,
280
- routes: this.routes,
281
- guidelines: this.guidelines,
282
- maxIterations: this.options.maxEngineIterations || 1,
283
- });
284
-
285
- // Update context with results from tool executions
286
- effectiveContext = preparationResult.finalContext;
363
+ // Initialize or get session
364
+ let session = params.session || createSession();
287
365
 
288
- // Log tool executions for debugging
289
- if (preparationResult.toolExecutions.length > 0) {
290
- console.log(
291
- `[Agent] Preparation complete: ${preparationResult.toolExecutions.length} tools executed in ${preparationResult.iterations.length} iterations`
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
292
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
+ }
293
415
  }
294
416
 
295
- // Build prompt (same as respond method)
296
- const promptBuilder = new PromptBuilder();
417
+ // PHASE 2: ROUTING - Determine which route to use
418
+ let selectedRoute: Route<TContext> | undefined;
419
+ let responseDirectives: string[] | undefined;
297
420
 
298
- // Add agent identity
299
- if (this.options.description) {
300
- promptBuilder.addAgentIdentity({
301
- name: this.options.name,
302
- description: this.options.description,
303
- });
304
- }
421
+ if (this.routes.length > 0) {
422
+ // Get last user message
423
+ const lastUserMessage = getLastMessageFromHistory(history);
305
424
 
306
- // Add interaction history
307
- promptBuilder.addInteractionHistoryForMessageGeneration(history);
425
+ // Build routing schema
426
+ const routingSchema = this.routingEngine.buildDynamicRoutingSchema(
427
+ this.routes
428
+ );
308
429
 
309
- // Add glossary
310
- if (this.terms.length > 0) {
311
- promptBuilder.addGlossary(this.terms);
312
- }
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 guidelines (convert to GuidelineMatch format, filter enabled only)
315
- const enabledGuidelines = this.guidelines.filter(
316
- (g) => g.enabled !== false
317
- );
318
- if (enabledGuidelines.length > 0) {
319
- const guidelineMatches: GuidelineMatch[] = enabledGuidelines.map((g) => ({
320
- guideline: g,
321
- }));
322
- promptBuilder.addGuidelinesForMessageGeneration(guidelineMatches);
323
- }
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
+ },
457
+ });
324
458
 
325
- // Add capabilities
326
- if (this.capabilities.length > 0) {
327
- promptBuilder.addCapabilitiesForMessageGeneration(this.capabilities);
328
- }
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;
468
+
469
+ if (selectedRoute) {
470
+ console.log(
471
+ `[Agent] Selected route: ${selectedRoute.title} (score: ${decision.maxScore})`
472
+ );
329
473
 
330
- // Add observations
331
- if (this.observations.length > 0) {
332
- const observationsWithRoutes = this.observations
333
- .map((obs) => ({
334
- description: obs.description,
335
- routes: obs.getRoutes().map((routeRef) => {
336
- const route = this.routes.find((r) => r.id === routeRef.id);
337
- return { title: route?.title || routeRef.id };
338
- }),
339
- }))
340
- .filter((obs) => obs.routes.length > 0);
341
-
342
- if (observationsWithRoutes.length > 0) {
343
- promptBuilder.addObservations(observationsWithRoutes);
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
+ }
344
497
  }
345
498
  }
346
499
 
347
- // Add active routes with their rules and prohibitions
348
- if (this.routes.length > 0) {
349
- promptBuilder.addActiveRoutes(
350
- this.routes.map((r) => ({
351
- title: r.title,
352
- description: r.description,
353
- conditions: r.conditions,
354
- domains: r.getDomains(),
355
- rules: r.getRules(),
356
- prohibitions: r.getProhibitions(),
357
- }))
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
358
511
  );
359
- }
360
512
 
361
- // NOTE: Domains/tools are NOT added to the prompt.
362
- // Tools execute automatically based on state transitions and guideline matching,
363
- // NOT based on AI decisions. The AI should never see available tools.
364
-
365
- // Add JSON response schema instructions
366
- promptBuilder.addJsonResponseSchema();
367
-
368
- // Build final prompt
369
- const prompt = promptBuilder.build();
370
-
371
- // Generate message stream using AI provider with JSON mode enabled
372
- const stream = this.options.ai.generateMessageStream({
373
- prompt,
374
- history,
375
- context: effectiveContext,
376
- signal,
377
- parameters: {
378
- jsonMode: true,
379
- },
380
- });
513
+ // Update session with next state
514
+ session = enterState(session, nextState.id, nextState.description);
515
+ console.log(`[Agent] Entered state: ${nextState.id}`);
381
516
 
382
- // Stream chunks to caller
383
- for await (const chunk of stream) {
384
- // Extract route and state from structured response on final chunk
385
- let route: { id: string; title: string } | null = null;
386
- let state: { id: string; description?: string } | null = null;
387
- let toolCalls:
388
- | Array<{ toolName: string; arguments: Record<string, unknown> }>
389
- | undefined;
390
-
391
- if (chunk.done && chunk.structured) {
392
- // Find route by title
393
- if (chunk.structured.route) {
394
- const foundRoute = this.routes.find(
395
- (r) => r.title === chunk.structured?.route
396
- );
397
- if (foundRoute) {
398
- route = {
399
- id: foundRoute.id,
400
- title: foundRoute.title,
401
- };
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
524
+ );
525
+
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
+ );
541
+
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);
402
577
  }
403
578
  }
404
579
 
405
- // Create state reference if provided
406
- if (chunk.structured.state) {
407
- state = {
408
- id: "dynamic_state",
409
- description: chunk.structured.state,
410
- };
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
+ );
411
591
  }
412
592
 
413
- // Extract tool calls
593
+ // Auto-save session state on final chunk
414
594
  if (
415
- chunk.structured.toolCalls &&
416
- chunk.structured.toolCalls.length > 0
595
+ chunk.done &&
596
+ this.persistenceManager &&
597
+ session.metadata?.sessionId &&
598
+ this.options.persistence?.autoSave !== false
417
599
  ) {
418
- 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
+ );
419
607
  }
608
+
609
+ yield {
610
+ delta: chunk.delta,
611
+ accumulated: chunk.accumulated,
612
+ done: chunk.done,
613
+ session, // Return updated session
614
+ toolCalls,
615
+ };
420
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
+ });
421
649
 
422
- yield {
423
- delta: chunk.delta,
424
- accumulated: chunk.accumulated,
425
- done: chunk.done,
426
- route: route || undefined,
427
- state: state || undefined,
428
- toolCalls,
429
- };
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
+ }
430
659
  }
431
660
  }
432
661
 
@@ -436,12 +665,12 @@ export class Agent<TContext = unknown> {
436
665
  async respond(params: {
437
666
  history: Event[];
438
667
  state?: StateRef;
668
+ session?: SessionState;
439
669
  contextOverride?: Partial<TContext>;
440
670
  signal?: AbortSignal;
441
671
  }): Promise<{
442
672
  message: string;
443
- route?: { id: string; title: string } | null;
444
- state?: { id: string; description?: string } | null;
673
+ session?: SessionState;
445
674
  toolCalls?: Array<{ toolName: string; arguments: Record<string, unknown> }>;
446
675
  }> {
447
676
  const { history, contextOverride, signal } = params;
@@ -457,169 +686,294 @@ export class Agent<TContext = unknown> {
457
686
  }
458
687
 
459
688
  // Merge context with override
460
- let effectiveContext = {
689
+ const effectiveContext = {
461
690
  ...(currentContext as Record<string, unknown>),
462
691
  ...(contextOverride as Record<string, unknown>),
463
692
  } as TContext;
464
693
 
465
- // RUN PREPARATION ITERATIONS
466
- // This is where tools execute automatically based on:
467
- // 1. Matched guidelines with associated tools
468
- // 2. State machine transitions with toolState
469
- //
470
- // The AI will NEVER see these tools - they execute before message generation
471
- const preparationResult = await this.preparationEngine.prepare({
472
- history,
473
- currentState: params.state,
474
- context: effectiveContext,
475
- routes: this.routes,
476
- guidelines: this.guidelines,
477
- maxIterations: this.options.maxEngineIterations || 1,
478
- });
694
+ // Initialize or get session
695
+ let session = params.session || createSession();
479
696
 
480
- // Update context with results from tool executions
481
- effectiveContext = preparationResult.finalContext;
482
-
483
- // Log tool executions for debugging
484
- if (preparationResult.toolExecutions.length > 0) {
485
- console.log(
486
- `[Agent] Preparation complete: ${preparationResult.toolExecutions.length} tools executed in ${preparationResult.iterations.length} iterations`
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
487
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
+ }
488
746
  }
489
747
 
490
- // Build prompt
491
- const promptBuilder = new PromptBuilder();
748
+ // PHASE 2: ROUTING - Determine which route to use
749
+ let selectedRoute: Route<TContext> | undefined;
750
+ let responseDirectives: string[] | undefined;
492
751
 
493
- // Add agent identity
494
- if (this.options.description) {
495
- promptBuilder.addAgentIdentity({
496
- name: this.options.name,
497
- description: this.options.description,
498
- });
499
- }
752
+ if (this.routes.length > 0) {
753
+ // Get last user message
754
+ const lastUserMessage = getLastMessageFromHistory(history);
500
755
 
501
- // Add interaction history
502
- promptBuilder.addInteractionHistoryForMessageGeneration(history);
756
+ // Build routing schema
757
+ const routingSchema = this.routingEngine.buildDynamicRoutingSchema(
758
+ this.routes
759
+ );
503
760
 
504
- // Add glossary
505
- if (this.terms.length > 0) {
506
- promptBuilder.addGlossary(this.terms);
507
- }
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
+ );
508
774
 
509
- // Add guidelines (convert to GuidelineMatch format, filter enabled only)
510
- const enabledGuidelines = this.guidelines.filter(
511
- (g) => g.enabled !== false
512
- );
513
- if (enabledGuidelines.length > 0) {
514
- const guidelineMatches: GuidelineMatch[] = enabledGuidelines.map((g) => ({
515
- guideline: g,
516
- }));
517
- promptBuilder.addGuidelinesForMessageGeneration(guidelineMatches);
518
- }
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
+ },
788
+ });
519
789
 
520
- // Add capabilities
521
- if (this.capabilities.length > 0) {
522
- promptBuilder.addCapabilitiesForMessageGeneration(this.capabilities);
523
- }
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;
524
799
 
525
- // Add observations
526
- if (this.observations.length > 0) {
527
- const observationsWithRoutes = this.observations
528
- .map((obs) => ({
529
- description: obs.description,
530
- routes: obs.getRoutes().map((routeRef) => {
531
- const route = this.routes.find((r) => r.id === routeRef.id);
532
- return { title: route?.title || routeRef.id };
533
- }),
534
- }))
535
- .filter((obs) => obs.routes.length > 0);
536
-
537
- if (observationsWithRoutes.length > 0) {
538
- promptBuilder.addObservations(observationsWithRoutes);
800
+ if (selectedRoute) {
801
+ console.log(
802
+ `[Agent] Selected route: ${selectedRoute.title} (score: ${decision.maxScore})`
803
+ );
804
+
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
+ }
539
828
  }
540
829
  }
541
830
 
542
- // Add active routes with their rules and prohibitions
543
- if (this.routes.length > 0) {
544
- promptBuilder.addActiveRoutes(
545
- this.routes.map((r) => ({
546
- title: r.title,
547
- description: r.description,
548
- conditions: r.conditions,
549
- domains: r.getDomains(),
550
- rules: r.getRules(),
551
- prohibitions: r.getProhibitions(),
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
553
847
  );
554
- }
555
848
 
556
- // NOTE: Domains/tools are NOT added to the prompt.
557
- // Tools execute automatically based on state transitions and guideline matching,
558
- // NOT based on AI decisions. The AI should never see available tools.
559
-
560
- // Add JSON response schema instructions
561
- promptBuilder.addJsonResponseSchema();
562
-
563
- // Build final prompt
564
- const prompt = promptBuilder.build();
565
-
566
- // Generate message using AI provider with JSON mode enabled
567
- const result = await this.options.ai.generateMessage({
568
- prompt,
569
- history,
570
- context: effectiveContext,
571
- signal,
572
- parameters: {
573
- jsonMode: true,
574
- },
575
- });
849
+ // Update session with next state
850
+ session = enterState(session, nextState.id, nextState.description);
851
+ console.log(`[Agent] Entered state: ${nextState.id}`);
576
852
 
577
- // Parse structured response
578
- let message = result.message;
579
- let route: { id: string; title: string } | null = null;
580
- let state: { id: string; description?: string } | null = null;
581
- let toolCalls:
582
- | Array<{ toolName: string; arguments: Record<string, unknown> }>
583
- | undefined;
853
+ // Get last user message
854
+ const lastUserMessage = getLastMessageFromHistory(history);
584
855
 
585
- if (result.structured) {
586
- // Extract data from structured response
587
- message = result.structured.message || message;
856
+ // Build response schema for this route (with gather fields from state)
857
+ const responseSchema = this.responseEngine.responseSchemaForRoute(
858
+ selectedRoute,
859
+ nextState
860
+ );
588
861
 
589
- // Find route by title
590
- if (result.structured.route) {
591
- const foundRoute = this.routes.find(
592
- (r) => r.title === result.structured?.route
593
- );
594
- if (foundRoute) {
595
- route = {
596
- id: foundRoute.id,
597
- title: foundRoute.title,
598
- };
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,
599
875
  }
600
- }
876
+ );
601
877
 
602
- // Create state reference if provided
603
- if (result.structured.state) {
604
- state = {
605
- id: "dynamic_state",
606
- description: result.structured.state,
607
- };
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
+ });
889
+
890
+ message = result.structured?.message || result.message;
891
+
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
+ }
903
+ }
904
+
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
+ }
608
910
  }
609
911
 
610
- // Extract tool calls
912
+ // Extract any additional data from structured response
611
913
  if (
612
- result.structured.toolCalls &&
613
- result.structured.toolCalls.length > 0
914
+ result.structured &&
915
+ typeof result.structured === "object" &&
916
+ "contextUpdate" in result.structured
614
917
  ) {
615
- toolCalls = result.structured.toolCalls;
918
+ await this.updateContext(
919
+ (result.structured as { contextUpdate?: Partial<TContext> })
920
+ .contextUpdate as Partial<TContext>
921
+ );
616
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
+ );
617
972
  }
618
973
 
619
974
  return {
620
975
  message,
621
- route: route || undefined,
622
- state: state || undefined,
976
+ session, // Return updated session with route/state info
623
977
  toolCalls,
624
978
  };
625
979
  }
@@ -627,7 +981,7 @@ export class Agent<TContext = unknown> {
627
981
  /**
628
982
  * Get all routes
629
983
  */
630
- getRoutes(): Route<TContext>[] {
984
+ getRoutes(): Route<TContext, unknown>[] {
631
985
  return [...this.routes];
632
986
  }
633
987
 
@@ -652,13 +1006,6 @@ export class Agent<TContext = unknown> {
652
1006
  return [...this.capabilities];
653
1007
  }
654
1008
 
655
- /**
656
- * Get all observations
657
- */
658
- getObservations(): Observation[] {
659
- return [...this.observations];
660
- }
661
-
662
1009
  /**
663
1010
  * Get the domain registry
664
1011
  */