@falai/agent 0.9.0-alpha-2 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/README.md +42 -34
  2. package/dist/cjs/src/core/Agent.d.ts +48 -44
  3. package/dist/cjs/src/core/Agent.d.ts.map +1 -1
  4. package/dist/cjs/src/core/Agent.js +151 -1110
  5. package/dist/cjs/src/core/Agent.js.map +1 -1
  6. package/dist/cjs/src/core/ResponseModal.d.ts +211 -0
  7. package/dist/cjs/src/core/ResponseModal.d.ts.map +1 -0
  8. package/dist/cjs/src/core/ResponseModal.js +1394 -0
  9. package/dist/cjs/src/core/ResponseModal.js.map +1 -0
  10. package/dist/cjs/src/core/ResponsePipeline.d.ts +8 -4
  11. package/dist/cjs/src/core/ResponsePipeline.d.ts.map +1 -1
  12. package/dist/cjs/src/core/ResponsePipeline.js +48 -20
  13. package/dist/cjs/src/core/ResponsePipeline.js.map +1 -1
  14. package/dist/cjs/src/core/Route.d.ts +12 -5
  15. package/dist/cjs/src/core/Route.d.ts.map +1 -1
  16. package/dist/cjs/src/core/Route.js +26 -5
  17. package/dist/cjs/src/core/Route.js.map +1 -1
  18. package/dist/cjs/src/core/RoutingEngine.d.ts +5 -0
  19. package/dist/cjs/src/core/RoutingEngine.d.ts.map +1 -1
  20. package/dist/cjs/src/core/RoutingEngine.js +37 -25
  21. package/dist/cjs/src/core/RoutingEngine.js.map +1 -1
  22. package/dist/cjs/src/core/SessionManager.d.ts +9 -1
  23. package/dist/cjs/src/core/SessionManager.d.ts.map +1 -1
  24. package/dist/cjs/src/core/SessionManager.js +27 -5
  25. package/dist/cjs/src/core/SessionManager.js.map +1 -1
  26. package/dist/cjs/src/core/Step.d.ts +60 -7
  27. package/dist/cjs/src/core/Step.d.ts.map +1 -1
  28. package/dist/cjs/src/core/Step.js +151 -4
  29. package/dist/cjs/src/core/Step.js.map +1 -1
  30. package/dist/cjs/src/core/ToolManager.d.ts +234 -0
  31. package/dist/cjs/src/core/ToolManager.d.ts.map +1 -0
  32. package/dist/cjs/src/core/ToolManager.js +1117 -0
  33. package/dist/cjs/src/core/ToolManager.js.map +1 -0
  34. package/dist/cjs/src/index.d.ts +5 -4
  35. package/dist/cjs/src/index.d.ts.map +1 -1
  36. package/dist/cjs/src/index.js +11 -3
  37. package/dist/cjs/src/index.js.map +1 -1
  38. package/dist/cjs/src/types/agent.d.ts +2 -1
  39. package/dist/cjs/src/types/agent.d.ts.map +1 -1
  40. package/dist/cjs/src/types/ai.d.ts +1 -1
  41. package/dist/cjs/src/types/ai.d.ts.map +1 -1
  42. package/dist/cjs/src/types/index.d.ts +3 -2
  43. package/dist/cjs/src/types/index.d.ts.map +1 -1
  44. package/dist/cjs/src/types/index.js +3 -1
  45. package/dist/cjs/src/types/index.js.map +1 -1
  46. package/dist/cjs/src/types/route.d.ts +6 -4
  47. package/dist/cjs/src/types/route.d.ts.map +1 -1
  48. package/dist/cjs/src/types/tool.d.ts +84 -14
  49. package/dist/cjs/src/types/tool.d.ts.map +1 -1
  50. package/dist/cjs/src/types/tool.js +13 -0
  51. package/dist/cjs/src/types/tool.js.map +1 -1
  52. package/dist/cjs/src/utils/clone.d.ts.map +1 -1
  53. package/dist/cjs/src/utils/clone.js +0 -4
  54. package/dist/cjs/src/utils/clone.js.map +1 -1
  55. package/dist/cjs/src/utils/history.d.ts +30 -1
  56. package/dist/cjs/src/utils/history.d.ts.map +1 -1
  57. package/dist/cjs/src/utils/history.js +169 -23
  58. package/dist/cjs/src/utils/history.js.map +1 -1
  59. package/dist/cjs/src/utils/index.d.ts +1 -1
  60. package/dist/cjs/src/utils/index.d.ts.map +1 -1
  61. package/dist/cjs/src/utils/index.js +5 -1
  62. package/dist/cjs/src/utils/index.js.map +1 -1
  63. package/dist/src/core/Agent.d.ts +48 -44
  64. package/dist/src/core/Agent.d.ts.map +1 -1
  65. package/dist/src/core/Agent.js +152 -1111
  66. package/dist/src/core/Agent.js.map +1 -1
  67. package/dist/src/core/ResponseModal.d.ts +211 -0
  68. package/dist/src/core/ResponseModal.d.ts.map +1 -0
  69. package/dist/src/core/ResponseModal.js +1389 -0
  70. package/dist/src/core/ResponseModal.js.map +1 -0
  71. package/dist/src/core/ResponsePipeline.d.ts +8 -4
  72. package/dist/src/core/ResponsePipeline.d.ts.map +1 -1
  73. package/dist/src/core/ResponsePipeline.js +48 -20
  74. package/dist/src/core/ResponsePipeline.js.map +1 -1
  75. package/dist/src/core/Route.d.ts +12 -5
  76. package/dist/src/core/Route.d.ts.map +1 -1
  77. package/dist/src/core/Route.js +26 -5
  78. package/dist/src/core/Route.js.map +1 -1
  79. package/dist/src/core/RoutingEngine.d.ts +5 -0
  80. package/dist/src/core/RoutingEngine.d.ts.map +1 -1
  81. package/dist/src/core/RoutingEngine.js +37 -25
  82. package/dist/src/core/RoutingEngine.js.map +1 -1
  83. package/dist/src/core/SessionManager.d.ts +9 -1
  84. package/dist/src/core/SessionManager.d.ts.map +1 -1
  85. package/dist/src/core/SessionManager.js +27 -5
  86. package/dist/src/core/SessionManager.js.map +1 -1
  87. package/dist/src/core/Step.d.ts +60 -7
  88. package/dist/src/core/Step.d.ts.map +1 -1
  89. package/dist/src/core/Step.js +151 -4
  90. package/dist/src/core/Step.js.map +1 -1
  91. package/dist/src/core/ToolManager.d.ts +234 -0
  92. package/dist/src/core/ToolManager.d.ts.map +1 -0
  93. package/dist/src/core/ToolManager.js +1111 -0
  94. package/dist/src/core/ToolManager.js.map +1 -0
  95. package/dist/src/index.d.ts +5 -4
  96. package/dist/src/index.d.ts.map +1 -1
  97. package/dist/src/index.js +3 -2
  98. package/dist/src/index.js.map +1 -1
  99. package/dist/src/types/agent.d.ts +2 -1
  100. package/dist/src/types/agent.d.ts.map +1 -1
  101. package/dist/src/types/ai.d.ts +1 -1
  102. package/dist/src/types/ai.d.ts.map +1 -1
  103. package/dist/src/types/index.d.ts +3 -2
  104. package/dist/src/types/index.d.ts.map +1 -1
  105. package/dist/src/types/index.js +1 -0
  106. package/dist/src/types/index.js.map +1 -1
  107. package/dist/src/types/route.d.ts +6 -4
  108. package/dist/src/types/route.d.ts.map +1 -1
  109. package/dist/src/types/tool.d.ts +84 -14
  110. package/dist/src/types/tool.d.ts.map +1 -1
  111. package/dist/src/types/tool.js +12 -1
  112. package/dist/src/types/tool.js.map +1 -1
  113. package/dist/src/utils/clone.d.ts.map +1 -1
  114. package/dist/src/utils/clone.js +0 -4
  115. package/dist/src/utils/clone.js.map +1 -1
  116. package/dist/src/utils/history.d.ts +30 -1
  117. package/dist/src/utils/history.d.ts.map +1 -1
  118. package/dist/src/utils/history.js +165 -23
  119. package/dist/src/utils/history.js.map +1 -1
  120. package/dist/src/utils/index.d.ts +1 -1
  121. package/dist/src/utils/index.d.ts.map +1 -1
  122. package/dist/src/utils/index.js +1 -1
  123. package/dist/src/utils/index.js.map +1 -1
  124. package/docs/CONTRIBUTING.md +40 -0
  125. package/docs/README.md +14 -6
  126. package/docs/api/README.md +235 -45
  127. package/docs/api/overview.md +140 -33
  128. package/docs/core/agent/session-management.md +152 -5
  129. package/docs/core/ai-integration/response-processing.md +115 -4
  130. package/docs/core/conversation-flows/routes.md +130 -0
  131. package/docs/core/error-handling.md +638 -0
  132. package/docs/core/tools/tool-definition.md +684 -60
  133. package/docs/core/tools/tool-scoping.md +244 -53
  134. package/docs/guides/error-handling-patterns.md +578 -0
  135. package/docs/guides/getting-started/README.md +139 -28
  136. package/docs/guides/migration/README.md +72 -0
  137. package/docs/guides/migration/response-modal-refactor.md +518 -0
  138. package/examples/advanced-patterns/knowledge-based-agent.ts +6 -6
  139. package/examples/advanced-patterns/persistent-onboarding.ts +30 -43
  140. package/examples/advanced-patterns/streaming-responses.ts +169 -96
  141. package/examples/ai-providers/anthropic-integration.ts +9 -5
  142. package/examples/ai-providers/openai-integration.ts +11 -7
  143. package/examples/core-concepts/basic-agent.ts +106 -67
  144. package/examples/core-concepts/modern-streaming-api.ts +309 -0
  145. package/examples/core-concepts/schema-driven-extraction.ts +10 -7
  146. package/examples/core-concepts/session-management.ts +71 -18
  147. package/examples/integrations/healthcare-integration.ts +15 -29
  148. package/examples/integrations/server-session-management.ts +3 -3
  149. package/examples/persistence/memory-sessions.ts +3 -3
  150. package/examples/tools/basic-tools.ts +293 -89
  151. package/examples/tools/data-enrichment-tools.ts +185 -75
  152. package/package.json +1 -1
  153. package/src/core/Agent.ts +190 -1529
  154. package/src/core/ResponseModal.ts +1798 -0
  155. package/src/core/ResponsePipeline.ts +83 -57
  156. package/src/core/Route.ts +39 -12
  157. package/src/core/RoutingEngine.ts +46 -42
  158. package/src/core/SessionManager.ts +39 -7
  159. package/src/core/Step.ts +198 -20
  160. package/src/core/ToolManager.ts +1394 -0
  161. package/src/index.ts +19 -3
  162. package/src/types/agent.ts +2 -1
  163. package/src/types/ai.ts +1 -1
  164. package/src/types/index.ts +13 -2
  165. package/src/types/route.ts +6 -4
  166. package/src/types/tool.ts +116 -25
  167. package/src/utils/clone.ts +6 -8
  168. package/src/utils/history.ts +190 -27
  169. package/src/utils/index.ts +4 -0
  170. package/dist/cjs/src/core/ToolExecutor.d.ts +0 -45
  171. package/dist/cjs/src/core/ToolExecutor.d.ts.map +0 -1
  172. package/dist/cjs/src/core/ToolExecutor.js +0 -84
  173. package/dist/cjs/src/core/ToolExecutor.js.map +0 -1
  174. package/dist/src/core/ToolExecutor.d.ts +0 -45
  175. package/dist/src/core/ToolExecutor.d.ts.map +0 -1
  176. package/dist/src/core/ToolExecutor.js +0 -80
  177. package/dist/src/core/ToolExecutor.js.map +0 -1
  178. package/docs/core/tools/tool-execution.md +0 -815
  179. package/src/core/ToolExecutor.ts +0 -126
@@ -0,0 +1,1394 @@
1
+ /**
2
+ * ToolManager - Centralized tool management with simplified creation APIs
3
+ */
4
+
5
+ import type {
6
+ Tool,
7
+ ToolResult,
8
+ ToolExecutionResult,
9
+ DataEnrichmentConfig,
10
+ ValidationConfig,
11
+ ValidationResult,
12
+ ApiCallConfig,
13
+ ComputationConfig,
14
+ ToolContext,
15
+ Event,
16
+ } from "../types";
17
+ import { ToolScope } from "../types";
18
+ import { logger } from "../utils";
19
+ import { Agent } from "./Agent";
20
+ import { Route } from "./Route";
21
+ import { Step } from "./Step";
22
+
23
+ /**
24
+ * Error thrown when tool creation fails
25
+ */
26
+ export class ToolCreationError extends Error {
27
+ constructor(
28
+ message: string,
29
+ public readonly toolId: string,
30
+ public readonly cause?: Error
31
+ ) {
32
+ super(message);
33
+ this.name = 'ToolCreationError';
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Error thrown when tool execution fails
39
+ */
40
+ export class ToolExecutionError extends Error {
41
+ constructor(
42
+ message: string,
43
+ public readonly toolId: string,
44
+ public readonly executionContext?: Record<string, unknown>,
45
+ public readonly cause?: Error
46
+ ) {
47
+ super(message);
48
+ this.name = 'ToolExecutionError';
49
+ }
50
+ }
51
+
52
+
53
+
54
+ /**
55
+ * ToolManager - Centralized tool management with simplified APIs
56
+ */
57
+ export class ToolManager<TContext = unknown, TData = unknown> {
58
+ private toolRegistry: Map<string, Tool<TContext, TData>>;
59
+
60
+ constructor(private agent: Agent<TContext, TData>) {
61
+ this.toolRegistry = new Map();
62
+ }
63
+
64
+ /**
65
+ * Validate a tool definition for completeness and correctness
66
+ */
67
+ private validateToolDefinition(
68
+ definition: Tool<TContext, TData>
69
+ ): void {
70
+ const errors: string[] = [];
71
+
72
+ // Required fields validation
73
+ if (!definition.id || typeof definition.id !== 'string') {
74
+ errors.push('Tool ID is required and must be a non-empty string');
75
+ } else if (definition.id.trim() === '') {
76
+ errors.push('Tool ID cannot be empty or whitespace only');
77
+ } else if (!/^[a-zA-Z0-9_-]+$/.test(definition.id)) {
78
+ errors.push('Tool ID must contain only alphanumeric characters, underscores, and hyphens');
79
+ }
80
+
81
+ if (!definition.handler || typeof definition.handler !== 'function') {
82
+ errors.push('Tool handler is required and must be a function');
83
+ }
84
+
85
+ // Optional fields validation
86
+ if (definition.name !== undefined && (typeof definition.name !== 'string' || definition.name.trim() === '')) {
87
+ errors.push('Tool name must be a non-empty string if provided');
88
+ }
89
+
90
+ if (definition.description !== undefined && (typeof definition.description !== 'string' || definition.description.trim() === '')) {
91
+ errors.push('Tool description must be a non-empty string if provided');
92
+ }
93
+
94
+ // Parameters validation (basic JSON schema check)
95
+ if (definition.parameters !== undefined) {
96
+ try {
97
+ if (typeof definition.parameters === 'object' && definition.parameters !== null) {
98
+ // Basic validation for JSON schema structure
99
+ const params = definition.parameters as Record<string, unknown>;
100
+ if ('type' in params && params.type && typeof params.type !== 'string') {
101
+ errors.push('Tool parameters type must be a string if specified');
102
+ }
103
+ if ('properties' in params && params.properties && typeof params.properties !== 'object') {
104
+ errors.push('Tool parameters properties must be an object if specified');
105
+ }
106
+ } else if (typeof definition.parameters !== 'string') {
107
+ errors.push('Tool parameters must be an object (JSON schema) or string if provided');
108
+ }
109
+ } catch {
110
+ errors.push('Tool parameters must be valid JSON schema or string');
111
+ }
112
+ }
113
+
114
+ if (errors.length > 0) {
115
+ throw new ToolCreationError(
116
+ `Tool definition validation failed: ${errors.join('; ')}`,
117
+ definition.id || 'unknown'
118
+ );
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Validate data enrichment configuration
124
+ */
125
+ private validateDataEnrichmentConfig<TFields extends keyof TData>(
126
+ config: DataEnrichmentConfig<TContext, TData, TFields>
127
+ ): void {
128
+ const errors: string[] = [];
129
+
130
+ if (!config.fields || !Array.isArray(config.fields) || config.fields.length === 0) {
131
+ errors.push('Data enrichment fields must be a non-empty array');
132
+ }
133
+
134
+ if (!config.enricher || typeof config.enricher !== 'function') {
135
+ errors.push('Data enrichment enricher must be a function');
136
+ }
137
+
138
+ if (errors.length > 0) {
139
+ throw new ToolCreationError(
140
+ `Data enrichment configuration validation failed: ${errors.join('; ')}`,
141
+ config.id || 'unknown'
142
+ );
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Validate validation configuration
148
+ */
149
+ private validateValidationConfig<TFields extends keyof TData>(
150
+ config: ValidationConfig<TContext, TData, TFields>
151
+ ): void {
152
+ const errors: string[] = [];
153
+
154
+ if (!config.fields || !Array.isArray(config.fields) || config.fields.length === 0) {
155
+ errors.push('Validation fields must be a non-empty array');
156
+ }
157
+
158
+ if (!config.validator || typeof config.validator !== 'function') {
159
+ errors.push('Validation validator must be a function');
160
+ }
161
+
162
+ if (errors.length > 0) {
163
+ throw new ToolCreationError(
164
+ `Validation configuration validation failed: ${errors.join('; ')}`,
165
+ config.id || 'unknown'
166
+ );
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Validate API call configuration
172
+ */
173
+ private validateApiCallConfig<TResult = unknown>(
174
+ config: ApiCallConfig<TContext, TData, TResult>
175
+ ): void {
176
+ const errors: string[] = [];
177
+
178
+ if (!config.endpoint) {
179
+ errors.push('API call endpoint is required');
180
+ } else if (typeof config.endpoint !== 'string' && typeof config.endpoint !== 'function') {
181
+ errors.push('API call endpoint must be a string or function');
182
+ }
183
+
184
+ if (config.method && !['GET', 'POST', 'PUT', 'DELETE'].includes(config.method)) {
185
+ errors.push('API call method must be one of: GET, POST, PUT, DELETE');
186
+ }
187
+
188
+ if (config.headers && typeof config.headers !== 'object' && typeof config.headers !== 'function') {
189
+ errors.push('API call headers must be an object or function');
190
+ }
191
+
192
+ if (config.body && typeof config.body !== 'function') {
193
+ errors.push('API call body must be a function');
194
+ }
195
+
196
+ if (config.transform && typeof config.transform !== 'function') {
197
+ errors.push('API call transform must be a function');
198
+ }
199
+
200
+ if (errors.length > 0) {
201
+ throw new ToolCreationError(
202
+ `API call configuration validation failed: ${errors.join('; ')}`,
203
+ config.id || 'unknown'
204
+ );
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Validate computation configuration
210
+ */
211
+ private validateComputationConfig<TResult = unknown>(
212
+ config: ComputationConfig<TContext, TData, TResult>
213
+ ): void {
214
+ const errors: string[] = [];
215
+
216
+ if (!config.inputs || !Array.isArray(config.inputs) || config.inputs.length === 0) {
217
+ errors.push('Computation inputs must be a non-empty array');
218
+ }
219
+
220
+ if (!config.compute || typeof config.compute !== 'function') {
221
+ errors.push('Computation compute function is required');
222
+ }
223
+
224
+ if (errors.length > 0) {
225
+ throw new ToolCreationError(
226
+ `Computation configuration validation failed: ${errors.join('; ')}`,
227
+ config.id || 'unknown'
228
+ );
229
+ }
230
+ }
231
+
232
+ /**
233
+ * Create a tool instance with type inference from parent Agent
234
+ * Does not register the tool anywhere - just creates it
235
+ */
236
+ create(
237
+ definition: Tool<TContext, TData>
238
+ ): Tool<TContext, TData> {
239
+ try {
240
+ // Validate the tool definition first
241
+ this.validateToolDefinition(definition);
242
+
243
+ logger.debug(`[ToolManager] Created tool: ${definition.id}`);
244
+ return definition;
245
+ } catch (error) {
246
+ throw new ToolCreationError(
247
+ `Failed to create tool '${definition.id}': ${error instanceof Error ? error.message : String(error)}`,
248
+ definition.id,
249
+ error instanceof Error ? error : undefined
250
+ );
251
+ }
252
+ }
253
+
254
+ /**
255
+ * Register a tool in the registry for later reference by ID
256
+ * Can accept a tool instance
257
+ */
258
+ register(
259
+ tool: Tool<TContext, TData>
260
+ ): Tool<TContext, TData> {
261
+ try {
262
+ if (!tool) {
263
+ throw new ToolCreationError('Tool is required for registration', 'unknown');
264
+ }
265
+
266
+ if (!('handler' in tool) || typeof tool.handler !== 'function') {
267
+ throw new ToolCreationError(
268
+ 'Invalid tool provided for registration - must have a handler function',
269
+ tool?.id || 'unknown'
270
+ );
271
+ }
272
+
273
+ // Validate the tool
274
+ if (!tool.id || typeof tool.id !== 'string' || tool.id.trim() === '') {
275
+ throw new ToolCreationError('Tool ID is required and must be a non-empty string', tool.id || 'unknown');
276
+ }
277
+
278
+ // Check for ID conflicts and provide better error context
279
+ if (this.toolRegistry.has(tool.id)) {
280
+ const existingTool = this.toolRegistry.get(tool.id);
281
+ logger.warn(`[ToolManager] Overwriting existing registered tool: ${tool.id} (previous: ${existingTool?.name || 'unnamed'})`);
282
+ }
283
+
284
+ this.toolRegistry.set(tool.id, tool);
285
+ logger.debug(`[ToolManager] Registered tool: ${tool.id} (${tool.name || 'unnamed'})`);
286
+
287
+ return tool;
288
+ } catch (error) {
289
+ if (error instanceof ToolCreationError) {
290
+ throw error;
291
+ }
292
+
293
+ const toolId = tool?.id || 'unknown';
294
+ throw new ToolCreationError(
295
+ `Failed to register tool '${toolId}': ${error instanceof Error ? error.message : String(error)}`,
296
+ toolId,
297
+ error instanceof Error ? error : undefined
298
+ );
299
+ }
300
+ }
301
+
302
+ /**
303
+ * Register multiple tools at once
304
+ */
305
+ registerMany(
306
+ tools: Array<Tool<TContext, TData>>
307
+ ): Tool<TContext, TData>[] {
308
+ return tools.map(tool => this.register(tool));
309
+ }
310
+
311
+ /**
312
+ * Get a registered tool by ID
313
+ */
314
+ getRegisteredTool(toolId: string): Tool<TContext, TData> | undefined {
315
+ return this.toolRegistry.get(toolId);
316
+ }
317
+
318
+ /**
319
+ * Get all registered tools
320
+ */
321
+ getAllRegistered(): Map<string, Tool<TContext, TData>> {
322
+ return new Map(this.toolRegistry);
323
+ }
324
+
325
+ /**
326
+ * Check if a tool is registered
327
+ */
328
+ isRegistered(toolId: string): boolean {
329
+ return this.toolRegistry.has(toolId);
330
+ }
331
+
332
+ /**
333
+ * Get all registered tool IDs
334
+ */
335
+ getRegisteredIds(): string[] {
336
+ return Array.from(this.toolRegistry.keys());
337
+ }
338
+
339
+ /**
340
+ * Get tool by ID from a specific scope
341
+ */
342
+ getFromScope(toolId: string, scope: ToolScope, step?: Step<TContext, TData>, route?: Route<TContext, TData>): Tool<TContext, TData> | undefined {
343
+ return this.find(toolId, scope, step, route);
344
+ }
345
+
346
+ /**
347
+ * Check if a tool exists in any scope
348
+ */
349
+ exists(toolId: string, step?: Step<TContext, TData>, route?: Route<TContext, TData>): boolean {
350
+ return this.find(toolId, ToolScope.ALL, step, route) !== undefined;
351
+ }
352
+
353
+ /**
354
+ * Get tool count by scope
355
+ */
356
+ getToolCount(scope?: ToolScope, step?: Step<TContext, TData>, route?: Route<TContext, TData>): number {
357
+ return this.getAvailable(scope, step, route).length;
358
+ }
359
+
360
+ /**
361
+ * Clear all registered tools
362
+ */
363
+ clearRegistry(): void {
364
+ this.toolRegistry.clear();
365
+ logger.debug('[ToolManager] Cleared tool registry');
366
+ }
367
+
368
+ /**
369
+ * Remove a tool from registry
370
+ */
371
+ unregister(toolId: string): boolean {
372
+ const existed = this.toolRegistry.has(toolId);
373
+ this.toolRegistry.delete(toolId);
374
+ if (existed) {
375
+ logger.debug(`[ToolManager] Unregistered tool: ${toolId}`);
376
+ }
377
+ return existed;
378
+ }
379
+
380
+ /**
381
+ * Add a tool to the agent scope (creates and adds in one operation)
382
+ */
383
+ addToAgent(
384
+ tool: Tool<TContext, TData>
385
+ ): Tool<TContext, TData> {
386
+ // Validate tool before adding
387
+ if (!tool || !tool.id || !tool.handler) {
388
+ throw new ToolCreationError('Invalid tool: must have id and handler properties', tool?.id || 'unknown');
389
+ }
390
+
391
+ // Add to agent's tools array using the unified interface
392
+ if (this.agent) {
393
+ this.agent.addTool(tool);
394
+ } else {
395
+ logger.warn(`[ToolManager] No agent available, tool not added to agent scope: ${tool.id}`);
396
+ }
397
+
398
+ logger.debug(`[ToolManager] Added tool to agent scope: ${tool.id}`);
399
+ return tool;
400
+ }
401
+
402
+ /**
403
+ * Add a tool to a specific route scope (creates and adds in one operation)
404
+ */
405
+ addToRoute(
406
+ route: Route<TContext, TData>,
407
+ tool: Tool<TContext, TData>
408
+ ): Tool<TContext, TData> {
409
+ // Add to route's tools array using the existing createTool method
410
+ if (route && typeof route.createTool === 'function') {
411
+ route.createTool(tool);
412
+ } else {
413
+ logger.warn(`[ToolManager] Route does not support createTool method, tool not added to route scope: ${tool.id}`);
414
+ }
415
+
416
+ logger.debug(`[ToolManager] Added tool to route scope: ${tool.id}`);
417
+ return tool;
418
+ }
419
+
420
+ /**
421
+ * Find a tool by ID across different scopes with enhanced resolution logic
422
+ * Priority: step → route → agent → registry
423
+ * Supports both ID and name matching for better compatibility
424
+ */
425
+ find(toolId: string, scope?: ToolScope, step?: Step<TContext, TData>, route?: Route<TContext, TData>): Tool<TContext, TData> | undefined {
426
+ logger.debug(`[ToolManager] Finding tool: ${toolId} with scope: ${scope || 'ALL'}`);
427
+
428
+ // Check step-level tools first (if step provided and scope allows)
429
+ if (step && (!scope || scope === ToolScope.STEP || scope === ToolScope.ALL)) {
430
+ if (step.tools) {
431
+ for (const toolRef of step.tools) {
432
+ if (typeof toolRef !== 'string') {
433
+ // Inline tool object - check both id and name
434
+ if (toolRef.id === toolId || toolRef.name === toolId) {
435
+ logger.debug(`[ToolManager] Found tool in step scope: ${toolId}`);
436
+ return toolRef;
437
+ }
438
+ } else {
439
+ // String reference - check if it matches and resolve from registry
440
+ if (toolRef === toolId) {
441
+ const registeredTool = this.toolRegistry.get(toolId);
442
+ if (registeredTool) {
443
+ logger.debug(`[ToolManager] Found tool reference in step, resolved from registry: ${toolId}`);
444
+ return registeredTool;
445
+ }
446
+ }
447
+ }
448
+ }
449
+ }
450
+ }
451
+
452
+ // Check route-level tools (if route provided and scope allows)
453
+ if (route && (!scope || scope === ToolScope.ROUTE || scope === ToolScope.ALL)) {
454
+ if (route.tools) {
455
+ const routeTool = route.tools.find((t) => t.id === toolId || t.name === toolId);
456
+ if (routeTool) {
457
+ logger.debug(`[ToolManager] Found tool in route scope: ${toolId}`);
458
+ return routeTool;
459
+ }
460
+ }
461
+ }
462
+
463
+ // Check agent-level tools (if scope allows)
464
+ if (!scope || scope === ToolScope.AGENT || scope === ToolScope.ALL) {
465
+ if (this.agent) {
466
+ const agentTools = this.agent.getTools();
467
+ const agentTool = agentTools.find((t) => t.id === toolId || t.name === toolId);
468
+ if (agentTool) {
469
+ logger.debug(`[ToolManager] Found tool in agent scope: ${toolId}`);
470
+ return agentTool;
471
+ }
472
+ }
473
+ }
474
+
475
+ // Check registry (if scope allows)
476
+ if (!scope || scope === ToolScope.REGISTERED || scope === ToolScope.ALL) {
477
+ const registeredTool = this.toolRegistry.get(toolId);
478
+ if (registeredTool) {
479
+ logger.debug(`[ToolManager] Found tool in registry: ${toolId}`);
480
+ return registeredTool;
481
+ }
482
+
483
+ // Also check by name in registry
484
+ for (const [id, tool] of Array.from(this.toolRegistry.entries())) {
485
+ if (tool.name === toolId) {
486
+ logger.debug(`[ToolManager] Found tool in registry by name: ${toolId} (id: ${id})`);
487
+ return tool;
488
+ }
489
+ }
490
+ }
491
+
492
+ logger.debug(`[ToolManager] Tool not found: ${toolId}`);
493
+ return undefined;
494
+ }
495
+
496
+ /**
497
+ * Get available tools for current context with enhanced resolution and deduplication
498
+ * Returns tools in priority order with higher-priority scopes taking precedence
499
+ */
500
+ getAvailable(scope?: ToolScope, step?: Step<TContext, TData>, route?: Route<TContext, TData>): Tool<TContext, TData>[] {
501
+ const toolMap = new Map<string, Tool<TContext, TData>>();
502
+ const resolvedTools: Tool<TContext, TData>[] = [];
503
+
504
+ logger.debug(`[ToolManager] Getting available tools with scope: ${scope || 'ALL'}`);
505
+
506
+ // Add registered tools first (lowest priority)
507
+ if (!scope || scope === ToolScope.REGISTERED || scope === ToolScope.ALL) {
508
+ for (const [id, tool] of Array.from(this.toolRegistry.entries())) {
509
+ toolMap.set(id, tool);
510
+ }
511
+ logger.debug(`[ToolManager] Added ${this.toolRegistry.size} registered tools`);
512
+ }
513
+
514
+ // Add agent-level tools (override registered tools with same ID)
515
+ if (!scope || scope === ToolScope.AGENT || scope === ToolScope.ALL) {
516
+ if (this.agent) {
517
+ const agentTools = this.agent.getTools();
518
+ for (const tool of agentTools) {
519
+ toolMap.set(tool.id, tool);
520
+ }
521
+ logger.debug(`[ToolManager] Added ${agentTools.length} agent tools`);
522
+ }
523
+ }
524
+
525
+ // Add route-level tools (override agent and registered tools with same ID)
526
+ if (route && (!scope || scope === ToolScope.ROUTE || scope === ToolScope.ALL)) {
527
+ if (route.tools) {
528
+ for (const tool of route.tools) {
529
+ toolMap.set(tool.id, tool);
530
+ }
531
+ logger.debug(`[ToolManager] Added ${route.tools.length} route tools`);
532
+ }
533
+ }
534
+
535
+ // Add step-level tools (highest priority - override all others with same ID)
536
+ if (step && (!scope || scope === ToolScope.STEP || scope === ToolScope.ALL)) {
537
+ if (step.tools) {
538
+ for (const toolRef of step.tools) {
539
+ if (typeof toolRef !== 'string') {
540
+ // Inline tool object - add directly
541
+ toolMap.set(toolRef.id, toolRef);
542
+ resolvedTools.push(toolRef);
543
+ } else {
544
+ // String reference - resolve from registry and add if found
545
+ const registeredTool = this.toolRegistry.get(toolRef);
546
+ if (registeredTool) {
547
+ toolMap.set(registeredTool.id, registeredTool);
548
+ resolvedTools.push(registeredTool);
549
+ } else {
550
+ logger.warn(`[ToolManager] Step references unknown tool: ${toolRef}`);
551
+ }
552
+ }
553
+ }
554
+ logger.debug(`[ToolManager] Added ${step.tools.length} step tools (${resolvedTools.length} resolved)`);
555
+ }
556
+ }
557
+
558
+ // Convert map to array, preserving priority order
559
+ const allTools = Array.from(toolMap.values());
560
+
561
+ // If we have step-specific tools, prioritize them
562
+ if (resolvedTools.length > 0) {
563
+ // Add resolved step tools first, then other tools not already included
564
+ const stepToolIds = new Set(resolvedTools.map(t => t.id));
565
+ const otherTools = allTools.filter(t => !stepToolIds.has(t.id));
566
+ return [...resolvedTools, ...otherTools];
567
+ }
568
+
569
+ logger.debug(`[ToolManager] Returning ${allTools.length} available tools`);
570
+ return allTools;
571
+ }
572
+
573
+ /**
574
+ * Execute a tool by ID with proper error handling and fallback strategies
575
+ * Consolidates tool execution logic from ToolExecutor and ResponseModal
576
+ */
577
+ async execute(
578
+ toolId: string,
579
+ args?: Record<string, unknown>,
580
+ context?: {
581
+ step?: Step<TContext, TData>;
582
+ route?: Route<TContext, TData>;
583
+ context?: TContext;
584
+ data?: Partial<TData>;
585
+ history?: Event[];
586
+ updateContext?: (updates: Partial<TContext>) => Promise<void>;
587
+ updateData?: (updates: Partial<TData>) => Promise<void>;
588
+ fallbackTools?: string[]; // Alternative tools to try if primary fails
589
+ retryCount?: number; // Number of retries for transient failures
590
+ }
591
+ ): Promise<ToolExecutionResult> {
592
+ const maxRetries = context?.retryCount || 2;
593
+ const fallbackTools = context?.fallbackTools || [];
594
+ let lastError: Error | undefined;
595
+
596
+ // Validate input parameters
597
+ if (!toolId || typeof toolId !== 'string' || toolId.trim() === '') {
598
+ return {
599
+ success: false,
600
+ error: 'Tool ID is required and must be a non-empty string',
601
+ metadata: { toolId, args }
602
+ };
603
+ }
604
+
605
+ // Try primary tool with retries
606
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
607
+ try {
608
+ const tool = this.find(toolId, undefined, context?.step, context?.route);
609
+ if (!tool) {
610
+ // Tool not found - try fallback tools if available
611
+ if (fallbackTools.length > 0) {
612
+ logger.warn(`[ToolManager] Primary tool '${toolId}' not found, trying fallback tools: ${fallbackTools.join(', ')}`);
613
+
614
+ for (const fallbackId of fallbackTools) {
615
+ const fallbackResult = await this.execute(fallbackId, args, {
616
+ ...context,
617
+ fallbackTools: [], // Prevent infinite recursion
618
+ retryCount: 0 // Don't retry fallback tools
619
+ });
620
+
621
+ if (fallbackResult.success) {
622
+ logger.info(`[ToolManager] Fallback tool '${fallbackId}' succeeded for primary tool '${toolId}'`);
623
+ return {
624
+ ...fallbackResult,
625
+ metadata: {
626
+ ...fallbackResult.metadata,
627
+ primaryTool: toolId,
628
+ fallbackUsed: fallbackId
629
+ }
630
+ };
631
+ }
632
+ }
633
+ }
634
+
635
+ return {
636
+ success: false,
637
+ error: `Tool not found: ${toolId}${fallbackTools.length > 0 ? ` (fallback tools also failed: ${fallbackTools.join(', ')})` : ''}`,
638
+ metadata: { toolId, args, fallbackTools }
639
+ };
640
+ }
641
+
642
+ // Execute the tool with proper context
643
+ const result = await this.executeTool({
644
+ tool,
645
+ context: context?.context || (await this.agent.getContext()) as TContext,
646
+ updateContext: context?.updateContext || this.agent.updateContext.bind(this.agent),
647
+ updateData: context?.updateData || this.agent.updateCollectedData.bind(this.agent),
648
+ history: context?.history || [],
649
+ data: context?.data,
650
+ toolArguments: args,
651
+ });
652
+
653
+ // Success - return result with execution metadata
654
+ if (result.success) {
655
+ return {
656
+ ...result,
657
+ metadata: {
658
+ ...result.metadata,
659
+ toolId,
660
+ attempt: attempt + 1,
661
+ maxRetries
662
+ }
663
+ };
664
+ } else {
665
+ // Tool execution returned failure - don't retry, return immediately
666
+ return {
667
+ ...result,
668
+ metadata: {
669
+ ...result.metadata,
670
+ toolId,
671
+ attempt: attempt + 1,
672
+ maxRetries
673
+ }
674
+ };
675
+ }
676
+ } catch (error) {
677
+ lastError = error instanceof Error ? error : new Error(String(error));
678
+ const errorMessage = lastError.message;
679
+
680
+ // Check if this is a transient error that should be retried
681
+ const isTransientError = this.isTransientError(lastError);
682
+
683
+ if (attempt < maxRetries && isTransientError) {
684
+ logger.warn(`[ToolManager] Tool execution attempt ${attempt + 1} failed for ${toolId}, retrying: ${errorMessage}`);
685
+
686
+ // Exponential backoff for retries
687
+ const delay = Math.min(1000 * Math.pow(2, attempt), 5000);
688
+ await new Promise(resolve => setTimeout(resolve, delay));
689
+ continue;
690
+ } else {
691
+ logger.error(`[ToolManager] Tool execution failed for ${toolId} after ${attempt + 1} attempts: ${errorMessage}`);
692
+ break;
693
+ }
694
+ }
695
+ }
696
+
697
+ // All retries failed - try fallback tools
698
+ if (fallbackTools.length > 0) {
699
+ logger.warn(`[ToolManager] Primary tool '${toolId}' failed after retries, trying fallback tools: ${fallbackTools.join(', ')}`);
700
+
701
+ for (const fallbackId of fallbackTools) {
702
+ try {
703
+ const fallbackResult = await this.execute(fallbackId, args, {
704
+ ...context,
705
+ fallbackTools: [], // Prevent infinite recursion
706
+ retryCount: 0 // Don't retry fallback tools
707
+ });
708
+
709
+ if (fallbackResult.success) {
710
+ logger.info(`[ToolManager] Fallback tool '${fallbackId}' succeeded for failed primary tool '${toolId}'`);
711
+ return {
712
+ ...fallbackResult,
713
+ metadata: {
714
+ ...fallbackResult.metadata,
715
+ primaryTool: toolId,
716
+ primaryError: lastError?.message,
717
+ fallbackUsed: fallbackId
718
+ }
719
+ };
720
+ }
721
+ } catch (fallbackError) {
722
+ logger.warn(`[ToolManager] Fallback tool '${fallbackId}' also failed: ${fallbackError instanceof Error ? fallbackError.message : String(fallbackError)}`);
723
+ }
724
+ }
725
+ }
726
+
727
+ // All attempts and fallbacks failed
728
+ throw new ToolExecutionError(
729
+ `Tool execution failed after ${maxRetries + 1} attempts: ${lastError?.message || 'Unknown error'}${fallbackTools.length > 0 ? ` (fallback tools also failed: ${fallbackTools.join(', ')})` : ''}`,
730
+ toolId,
731
+ { args, context, attempts: maxRetries + 1, fallbackTools },
732
+ lastError
733
+ );
734
+ }
735
+
736
+ /**
737
+ * Determine if an error is transient and should be retried
738
+ */
739
+ private isTransientError(error: Error): boolean {
740
+ const transientPatterns = [
741
+ /network/i,
742
+ /timeout/i,
743
+ /connection/i,
744
+ /temporary/i,
745
+ /rate limit/i,
746
+ /503/,
747
+ /502/,
748
+ /504/,
749
+ /ECONNRESET/,
750
+ /ETIMEDOUT/,
751
+ /ENOTFOUND/
752
+ ];
753
+
754
+ return transientPatterns.some(pattern => pattern.test(error.message));
755
+ }
756
+
757
+ /**
758
+ * Execute a single tool with context and collected data
759
+ * Consolidates logic from ToolExecutor class with enhanced error handling
760
+ */
761
+ async executeTool(params: {
762
+ tool: Tool<TContext, TData>;
763
+ context: TContext;
764
+ updateContext: (updates: Partial<TContext>) => Promise<void>;
765
+ updateData: (updates: Partial<TData>) => Promise<void>;
766
+ history: Event[];
767
+ data?: Partial<TData>;
768
+ toolArguments?: Record<string, unknown>;
769
+ }): Promise<ToolExecutionResult> {
770
+ const { tool, context, updateContext, updateData, history, data, toolArguments } = params;
771
+ const startTime = Date.now();
772
+
773
+ try {
774
+ // Validate tool before execution
775
+ if (!tool || !tool.handler || typeof tool.handler !== 'function') {
776
+ return {
777
+ success: false,
778
+ error: `Invalid tool: ${tool?.id || 'unknown'} - missing or invalid handler`,
779
+ metadata: { toolId: tool?.id, executionTime: 0 }
780
+ };
781
+ }
782
+
783
+ // Build tool context with complete agent data
784
+ const toolContext: ToolContext<TContext, TData> = {
785
+ context,
786
+ updateContext,
787
+ updateData,
788
+ history,
789
+ data: data || {},
790
+ getField: <K extends keyof TData>(key: K): TData[K] | undefined => {
791
+ return data?.[key];
792
+ },
793
+ setField: async <K extends keyof TData>(key: K, value: TData[K]): Promise<void> => {
794
+ const update = {} as Partial<TData>;
795
+ update[key] = value;
796
+ await updateData(update);
797
+ },
798
+ hasField: <K extends keyof TData>(key: K): boolean => {
799
+ return data != null && key in data;
800
+ }
801
+ };
802
+
803
+ logger.debug(`[ToolManager] Executing tool: ${tool.id} with args:`, toolArguments);
804
+
805
+ // Execute tool with timeout protection
806
+ const executionTimeout = 30000; // 30 seconds default timeout
807
+ const timeoutPromise = new Promise<never>((_, reject) => {
808
+ setTimeout(() => reject(new Error(`Tool execution timeout after ${executionTimeout}ms`)), executionTimeout);
809
+ });
810
+
811
+ const result = await Promise.race([
812
+ tool.handler(toolContext, toolArguments),
813
+ timeoutPromise
814
+ ]);
815
+
816
+ const executionTime = Date.now() - startTime;
817
+ logger.debug(`[ToolManager] Tool ${tool.id} completed in ${executionTime}ms`);
818
+
819
+ // Handle different result types
820
+ let toolResult: ToolResult<unknown, TContext, TData>;
821
+
822
+ if (result && typeof result === 'object' && ('data' in result || 'success' in result || 'error' in result)) {
823
+ // It's already a ToolResult-like object
824
+ toolResult = result as ToolResult<unknown, TContext, TData>;
825
+ } else {
826
+ // It's a raw result - wrap it
827
+ toolResult = {
828
+ data: result,
829
+ success: true
830
+ };
831
+ }
832
+
833
+ // Apply data updates from tool result with validation
834
+ if (toolResult.dataUpdate) {
835
+ try {
836
+ if (typeof toolResult.dataUpdate === 'object' && toolResult.dataUpdate !== null) {
837
+ await updateData(toolResult.dataUpdate);
838
+ } else {
839
+ logger.warn(`[ToolManager] Tool ${tool.id} returned invalid dataUpdate: expected object, got ${typeof toolResult.dataUpdate}`);
840
+ }
841
+ } catch (updateError) {
842
+ logger.error(`[ToolManager] Failed to apply data update from tool ${tool.id}:`, updateError);
843
+ return {
844
+ success: false,
845
+ error: `Failed to apply data update: ${updateError instanceof Error ? updateError.message : String(updateError)}`,
846
+ metadata: { toolId: tool.id, executionTime, updateError: updateError instanceof Error ? updateError.message : String(updateError) }
847
+ };
848
+ }
849
+ }
850
+
851
+ // Apply context updates from tool result with validation
852
+ if (toolResult.contextUpdate) {
853
+ try {
854
+ if (typeof toolResult.contextUpdate === 'object' && toolResult.contextUpdate !== null) {
855
+ await updateContext(toolResult.contextUpdate);
856
+ } else {
857
+ logger.warn(`[ToolManager] Tool ${tool.id} returned invalid contextUpdate: expected object, got ${typeof toolResult.contextUpdate}`);
858
+ }
859
+ } catch (updateError) {
860
+ logger.error(`[ToolManager] Failed to apply context update from tool ${tool.id}:`, updateError);
861
+ return {
862
+ success: false,
863
+ error: `Failed to apply context update: ${updateError instanceof Error ? updateError.message : String(updateError)}`,
864
+ metadata: { toolId: tool.id, executionTime, updateError: updateError instanceof Error ? updateError.message : String(updateError) }
865
+ };
866
+ }
867
+ }
868
+
869
+ // Return execution result with metadata
870
+ return {
871
+ success: toolResult.success !== false, // Default to true unless explicitly false
872
+ data: toolResult.data,
873
+ contextUpdate: toolResult.contextUpdate,
874
+ dataUpdate: toolResult.dataUpdate,
875
+ error: toolResult.error,
876
+ metadata: {
877
+ toolId: tool.id,
878
+ toolName: tool.name,
879
+ executionTime,
880
+ ...(toolResult.meta || {})
881
+ }
882
+ };
883
+ } catch (error) {
884
+ const executionTime = Date.now() - startTime;
885
+
886
+ logger.error(`[ToolManager] Tool execution error for ${tool.id} after ${executionTime}ms:`, error);
887
+
888
+ // Re-throw the error so the execute method can handle retries
889
+ throw error;
890
+ }
891
+ }
892
+
893
+ /**
894
+ * Execute multiple tools in sequence
895
+ * Consolidates logic from ToolExecutor class
896
+ */
897
+ async executeTools(params: {
898
+ tools: Array<Tool<TContext, TData>>;
899
+ context: TContext;
900
+ updateContext: (updates: Partial<TContext>) => Promise<void>;
901
+ updateData: (updates: Partial<TData>) => Promise<void>;
902
+ history: Event[];
903
+ data?: Partial<TData>;
904
+ }): Promise<ToolExecutionResult[]> {
905
+ const { tools, context, updateContext, updateData, history, data } = params;
906
+ const results: ToolExecutionResult[] = [];
907
+
908
+ for (const tool of tools) {
909
+ const result = await this.executeTool({
910
+ tool,
911
+ context,
912
+ updateContext,
913
+ updateData,
914
+ history,
915
+ data,
916
+ });
917
+ results.push(result);
918
+
919
+ // If tool failed, stop execution chain
920
+ if (!result.success) {
921
+ logger.error(`[ToolManager] Tool ${tool.id || "unknown"} failed:`, result.error);
922
+ break;
923
+ }
924
+
925
+ // Apply context updates from tool result
926
+ if (result.contextUpdate) {
927
+ await updateContext(result.contextUpdate as Partial<TContext>);
928
+ }
929
+
930
+ // Apply data updates from tool result
931
+ if (result.dataUpdate) {
932
+ await updateData(result.dataUpdate as Partial<TData>);
933
+ }
934
+ }
935
+
936
+ return results;
937
+ }
938
+
939
+ /**
940
+ * Create a data enrichment tool that modifies collected data
941
+ * Returns a tool instance that can be registered or added to scope
942
+ */
943
+ createDataEnrichment<TFields extends keyof TData>(
944
+ config: DataEnrichmentConfig<TContext, TData, TFields>
945
+ ): Tool<TContext, TData, void> {
946
+ // Validate configuration first
947
+ this.validateDataEnrichmentConfig(config);
948
+
949
+ const tool: Tool<TContext, TData, void> = {
950
+ id: config.id,
951
+ name: config.name || `Data Enrichment: ${config.id}`,
952
+ description: config.description || `Enriches data fields: ${config.fields.join(', ')}`,
953
+ parameters: {
954
+ type: 'object',
955
+ properties: {
956
+ fields: {
957
+ type: 'array',
958
+ items: { type: 'string' },
959
+ description: `Fields to enrich: ${config.fields.join(', ')}`
960
+ }
961
+ }
962
+ },
963
+ handler: async (context: ToolContext<TContext, TData>) => {
964
+ try {
965
+ // Extract the specified fields from current data
966
+ const fieldData = {} as Pick<TData, TFields>;
967
+ for (const field of config.fields) {
968
+ if (context.hasField(field)) {
969
+ const value = context.getField(field);
970
+ if (value !== undefined) {
971
+ (fieldData as Record<string, unknown>)[field as string] = value;
972
+ }
973
+ }
974
+ }
975
+
976
+ // Call the enricher function
977
+ const enrichedData = await config.enricher(context.context, fieldData);
978
+
979
+ // Update the data with enriched values
980
+ if (enrichedData && typeof enrichedData === 'object') {
981
+ await context.updateData(enrichedData);
982
+ }
983
+
984
+ logger.debug(`[ToolManager] Data enrichment completed for tool: ${config.id}`);
985
+
986
+ return {
987
+ success: true,
988
+ dataUpdate: enrichedData
989
+ };
990
+ } catch (error) {
991
+ const errorMessage = error instanceof Error ? error.message : String(error);
992
+ logger.error(`[ToolManager] Data enrichment failed for ${config.id}: ${errorMessage}`);
993
+ throw new ToolExecutionError(
994
+ `Data enrichment failed: ${errorMessage}`,
995
+ config.id,
996
+ { fields: config.fields },
997
+ error instanceof Error ? error : undefined
998
+ );
999
+ }
1000
+ }
1001
+ };
1002
+
1003
+ return tool;
1004
+ }
1005
+
1006
+ /**
1007
+ * Create a validation tool that validates data fields
1008
+ * Returns a tool instance that can be registered or added to scope
1009
+ */
1010
+ createValidation<TFields extends keyof TData>(
1011
+ config: ValidationConfig<TContext, TData, TFields>
1012
+ ): Tool<TContext, TData, ValidationResult> {
1013
+ // Validate configuration first
1014
+ this.validateValidationConfig(config);
1015
+
1016
+ const tool: Tool<TContext, TData, ValidationResult> = {
1017
+ id: config.id,
1018
+ name: config.name || `Validation: ${config.id}`,
1019
+ description: config.description || `Validates data fields: ${config.fields.join(', ')}`,
1020
+ parameters: {
1021
+ type: 'object',
1022
+ properties: {
1023
+ fields: {
1024
+ type: 'array',
1025
+ items: { type: 'string' },
1026
+ description: `Fields to validate: ${config.fields.join(', ')}`
1027
+ }
1028
+ }
1029
+ },
1030
+ handler: async (context: ToolContext<TContext, TData>): Promise<ValidationResult> => {
1031
+ try {
1032
+ // Extract the specified fields from current data
1033
+ const fieldData = {} as Pick<TData, TFields>;
1034
+ for (const field of config.fields) {
1035
+ if (context.hasField(field)) {
1036
+ const value = context.getField(field);
1037
+ if (value !== undefined) {
1038
+ (fieldData as Record<string, unknown>)[field as string] = value;
1039
+ }
1040
+ }
1041
+ }
1042
+
1043
+ // Call the validator function
1044
+ const result = await config.validator(context.context, fieldData);
1045
+
1046
+ logger.debug(`[ToolManager] Validation completed for tool: ${config.id}, valid: ${result.valid}`);
1047
+ return result;
1048
+ } catch (error) {
1049
+ const errorMessage = error instanceof Error ? error.message : String(error);
1050
+ logger.error(`[ToolManager] Validation failed for ${config.id}: ${errorMessage}`);
1051
+
1052
+ // Return validation failure result instead of throwing
1053
+ return {
1054
+ valid: false,
1055
+ errors: [{
1056
+ field: 'validation',
1057
+ message: `Validation error: ${errorMessage}`,
1058
+ value: undefined,
1059
+ schemaPath: config.id
1060
+ }],
1061
+ warnings: []
1062
+ };
1063
+ }
1064
+ }
1065
+ };
1066
+
1067
+ return tool;
1068
+ }
1069
+
1070
+ /**
1071
+ * Create an API call tool that makes external HTTP requests
1072
+ * Returns a tool instance that can be registered or added to scope
1073
+ */
1074
+ createApiCall<TResult = unknown>(
1075
+ config: ApiCallConfig<TContext, TData, TResult>
1076
+ ): Tool<TContext, TData, TResult> {
1077
+ // Validate configuration first
1078
+ this.validateApiCallConfig(config);
1079
+
1080
+ const tool: Tool<TContext, TData, TResult> = {
1081
+ id: config.id,
1082
+ name: config.name || `API Call: ${config.id}`,
1083
+ description: config.description || `Makes API call to external service`,
1084
+ parameters: {
1085
+ type: 'object',
1086
+ properties: {
1087
+ endpoint: {
1088
+ type: 'string',
1089
+ description: 'API endpoint URL'
1090
+ },
1091
+ method: {
1092
+ type: 'string',
1093
+ enum: ['GET', 'POST', 'PUT', 'DELETE'],
1094
+ description: 'HTTP method'
1095
+ }
1096
+ }
1097
+ },
1098
+ handler: async (context: ToolContext<TContext, TData>, args?: Record<string, unknown>): Promise<TResult> => {
1099
+ try {
1100
+ // Resolve endpoint URL
1101
+ const endpoint = typeof config.endpoint === 'function'
1102
+ ? config.endpoint(context.context, context.data)
1103
+ : config.endpoint;
1104
+
1105
+ // Resolve headers
1106
+ const headers = typeof config.headers === 'function'
1107
+ ? config.headers(context.context)
1108
+ : config.headers || {};
1109
+
1110
+ // Prepare request options
1111
+ const requestOptions: RequestInit = {
1112
+ method: config.method || 'GET',
1113
+ headers: {
1114
+ 'Content-Type': 'application/json',
1115
+ ...headers
1116
+ }
1117
+ };
1118
+
1119
+ // Add body for non-GET requests
1120
+ if (config.body && (config.method === 'POST' || config.method === 'PUT')) {
1121
+ const bodyData = config.body(context.context, context.data, args);
1122
+ requestOptions.body = JSON.stringify(bodyData);
1123
+ }
1124
+
1125
+ // Make the API call
1126
+ const response = await fetch(endpoint, requestOptions);
1127
+
1128
+ if (!response.ok) {
1129
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
1130
+ }
1131
+
1132
+ // Parse response
1133
+ let responseData: unknown;
1134
+ const contentType = response.headers.get('content-type');
1135
+ if (contentType && contentType.includes('application/json')) {
1136
+ responseData = await response.json();
1137
+ } else {
1138
+ responseData = await response.text();
1139
+ }
1140
+
1141
+ // Transform response if transformer provided
1142
+ const result = config.transform ? config.transform(responseData) : responseData as TResult;
1143
+
1144
+ logger.debug(`[ToolManager] API call completed for tool: ${config.id}`);
1145
+ return result;
1146
+ } catch (error) {
1147
+ const errorMessage = error instanceof Error ? error.message : String(error);
1148
+ logger.error(`[ToolManager] API call failed for ${config.id}: ${errorMessage}`);
1149
+
1150
+ throw new ToolExecutionError(
1151
+ `API call failed: ${errorMessage}`,
1152
+ config.id,
1153
+ { endpoint: config.endpoint, method: config.method, args },
1154
+ error instanceof Error ? error : undefined
1155
+ );
1156
+ }
1157
+ }
1158
+ };
1159
+
1160
+ return tool;
1161
+ }
1162
+
1163
+ /**
1164
+ * Create a computation tool that performs calculations on input data
1165
+ * Returns a tool instance that can be registered or added to scope
1166
+ */
1167
+ createComputation<TResult = unknown>(
1168
+ config: ComputationConfig<TContext, TData, TResult>
1169
+ ): Tool<TContext, TData, TResult> {
1170
+ // Validate configuration first
1171
+ this.validateComputationConfig(config);
1172
+
1173
+ const tool: Tool<TContext, TData, TResult> = {
1174
+ id: config.id,
1175
+ name: config.name || `Computation: ${config.id}`,
1176
+ description: config.description || `Performs computation on inputs: ${config.inputs.join(', ')}`,
1177
+ parameters: {
1178
+ type: 'object',
1179
+ properties: {
1180
+ inputs: {
1181
+ type: 'array',
1182
+ items: { type: 'string' },
1183
+ description: `Input fields: ${config.inputs.join(', ')}`
1184
+ }
1185
+ }
1186
+ },
1187
+ handler: async (context: ToolContext<TContext, TData>, args?: Record<string, unknown>): Promise<TResult> => {
1188
+ try {
1189
+ // Extract the specified input fields from current data
1190
+ const inputData = {} as Partial<TData>;
1191
+ for (const input of config.inputs) {
1192
+ if (context.hasField(input)) {
1193
+ const value = context.getField(input);
1194
+ if (value !== undefined) {
1195
+ (inputData as Record<string, unknown>)[input as string] = value;
1196
+ }
1197
+ }
1198
+ }
1199
+
1200
+ // Call the compute function
1201
+ const result = await config.compute(context.context, inputData, args);
1202
+
1203
+ logger.debug(`[ToolManager] Computation completed for tool: ${config.id}`);
1204
+ return result;
1205
+ } catch (error) {
1206
+ const errorMessage = error instanceof Error ? error.message : String(error);
1207
+ logger.error(`[ToolManager] Computation failed for ${config.id}: ${errorMessage}`);
1208
+
1209
+ throw new ToolExecutionError(
1210
+ `Computation failed: ${errorMessage}`,
1211
+ config.id,
1212
+ { inputs: config.inputs, args },
1213
+ error instanceof Error ? error : undefined
1214
+ );
1215
+ }
1216
+ }
1217
+ };
1218
+
1219
+ return tool;
1220
+ }
1221
+
1222
+ /**
1223
+ * Get detailed information about a tool for debugging
1224
+ */
1225
+ getToolInfo(toolId: string, scope?: ToolScope, step?: Step<TContext, TData>, route?: Route<TContext, TData>): {
1226
+ found: boolean;
1227
+ tool?: Tool<TContext, TData>;
1228
+ scope?: string;
1229
+ metadata?: Record<string, unknown>;
1230
+ } {
1231
+ const tool = this.find(toolId, scope, step, route);
1232
+
1233
+ if (!tool) {
1234
+ return { found: false };
1235
+ }
1236
+
1237
+ // Determine which scope the tool was found in
1238
+ let foundScope = 'unknown';
1239
+
1240
+ // Check step scope
1241
+ if (step?.tools) {
1242
+ const stepTool = step.tools.find((t) =>
1243
+ (typeof t === 'string' && t === toolId) ||
1244
+ (typeof t === 'object' && (t.id === toolId || t.name === toolId))
1245
+ );
1246
+ if (stepTool) foundScope = 'step';
1247
+ }
1248
+
1249
+ // Check route scope
1250
+ if (foundScope === 'unknown' && route?.tools) {
1251
+ const routeTool = route.tools.find((t) => t.id === toolId || t.name === toolId);
1252
+ if (routeTool) foundScope = 'route';
1253
+ }
1254
+
1255
+ // Check agent scope
1256
+ if (foundScope === 'unknown' && this.agent) {
1257
+ const agentTools = this.agent.getTools();
1258
+ const agentTool = agentTools.find((t) => t.id === toolId || t.name === toolId);
1259
+ if (agentTool) foundScope = 'agent';
1260
+ }
1261
+
1262
+ // Check registry
1263
+ if (foundScope === 'unknown' && this.toolRegistry.has(toolId)) {
1264
+ foundScope = 'registry';
1265
+ }
1266
+
1267
+ return {
1268
+ found: true,
1269
+ tool,
1270
+ scope: foundScope,
1271
+ metadata: {
1272
+ id: tool.id,
1273
+ name: tool.name,
1274
+ hasDescription: !!tool.description,
1275
+ hasParameters: !!tool.parameters,
1276
+ handlerLength: tool.handler.length
1277
+ }
1278
+ };
1279
+ }
1280
+
1281
+ /**
1282
+ * Validate that all tools in a list exist and are accessible
1283
+ */
1284
+ validateToolReferences(toolIds: string[], step?: Step<TContext, TData>, route?: Route<TContext, TData>): {
1285
+ valid: boolean;
1286
+ missing: string[];
1287
+ found: string[];
1288
+ details: Array<{ id: string; found: boolean; scope?: string; }>;
1289
+ } {
1290
+ const missing: string[] = [];
1291
+ const found: string[] = [];
1292
+ const details: Array<{ id: string; found: boolean; scope?: string; }> = [];
1293
+
1294
+ for (const toolId of toolIds) {
1295
+ const info = this.getToolInfo(toolId, ToolScope.ALL, step, route);
1296
+
1297
+ if (info.found) {
1298
+ found.push(toolId);
1299
+ details.push({ id: toolId, found: true, scope: info.scope });
1300
+ } else {
1301
+ missing.push(toolId);
1302
+ details.push({ id: toolId, found: false });
1303
+ }
1304
+ }
1305
+
1306
+ return {
1307
+ valid: missing.length === 0,
1308
+ missing,
1309
+ found,
1310
+ details
1311
+ };
1312
+ }
1313
+
1314
+ /**
1315
+ * Get comprehensive statistics about the tool system
1316
+ */
1317
+ getStatistics(): {
1318
+ registeredTools: number;
1319
+ agentTools: number;
1320
+ totalAvailable: number;
1321
+ registeredToolIds: string[];
1322
+ duplicateIds: string[];
1323
+ } {
1324
+ const registeredToolIds = Array.from(this.toolRegistry.keys());
1325
+ const agentTools = this.agent ? this.agent.getTools() : [];
1326
+ const agentToolIds = agentTools.map((t) => t.id);
1327
+
1328
+ // Find duplicate IDs between registry and agent
1329
+ const duplicateIds = registeredToolIds.filter(id => agentToolIds.includes(id));
1330
+
1331
+ const allAvailable = this.getAvailable();
1332
+
1333
+ return {
1334
+ registeredTools: this.toolRegistry.size,
1335
+ agentTools: agentTools.length,
1336
+ totalAvailable: allAvailable.length,
1337
+ registeredToolIds,
1338
+ duplicateIds
1339
+ };
1340
+ }
1341
+
1342
+ /**
1343
+ * Perform health check on the tool system
1344
+ */
1345
+ healthCheck(): {
1346
+ healthy: boolean;
1347
+ issues: string[];
1348
+ warnings: string[];
1349
+ statistics: {
1350
+ registeredTools: number;
1351
+ agentTools: number;
1352
+ totalAvailable: number;
1353
+ registeredToolIds: string[];
1354
+ duplicateIds: string[];
1355
+ };
1356
+ } {
1357
+ const issues: string[] = [];
1358
+ const warnings: string[] = [];
1359
+ const stats = this.getStatistics();
1360
+
1361
+ // Check for duplicate tool IDs
1362
+ if (stats.duplicateIds.length > 0) {
1363
+ warnings.push(`Duplicate tool IDs found between registry and agent: ${stats.duplicateIds.join(', ')}`);
1364
+ }
1365
+
1366
+ // Check for tools with missing handlers
1367
+ for (const [id, tool] of Array.from(this.toolRegistry.entries())) {
1368
+ if (!tool.handler || typeof tool.handler !== 'function') {
1369
+ issues.push(`Tool '${id}' has invalid or missing handler`);
1370
+ }
1371
+
1372
+ if (!tool.id || tool.id.trim() === '') {
1373
+ issues.push(`Tool has empty or invalid ID: ${JSON.stringify(tool)}`);
1374
+ }
1375
+ }
1376
+
1377
+ // Check agent tools if available
1378
+ if (this.agent) {
1379
+ const agentTools = this.agent.getTools();
1380
+ for (const tool of agentTools) {
1381
+ if (!tool.handler || typeof tool.handler !== 'function') {
1382
+ issues.push(`Agent tool '${tool.id}' has invalid or missing handler`);
1383
+ }
1384
+ }
1385
+ }
1386
+
1387
+ return {
1388
+ healthy: issues.length === 0,
1389
+ issues,
1390
+ warnings,
1391
+ statistics: stats
1392
+ };
1393
+ }
1394
+ }