@amitdeshmukh/ax-crew 8.7.3 → 9.0.1

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.
@@ -0,0 +1,444 @@
1
+ import { v4 as uuidv4 } from "uuid";
2
+ import type { AxFunction } from "@ax-llm/ax";
3
+
4
+ import type {
5
+ StateInstance,
6
+ FunctionRegistryType,
7
+ AxCrewConfig,
8
+ AxCrewOptions,
9
+ ACEConfig,
10
+ DeferredToolsConfig,
11
+ } from "../types.js";
12
+
13
+ import { createState } from "../state/index.js";
14
+ import { parseCrewConfig, parseAgentConfig } from "./agentConfig.js";
15
+ import { DeferredToolManager } from "./deferredTools.js";
16
+ import { MetricsRegistry } from "../metrics/index.js";
17
+ import { StatefulAxAgent } from "./statefulAgent.js";
18
+ import type { ParsedAgentConfig } from "./statefulAgent.js";
19
+ import { LazyStatefulAxAgent } from "./lazyAgent.js";
20
+
21
+ /**
22
+ * AxCrew orchestrates a set of Ax agents that share state,
23
+ * tools (functions), optional MCP servers, streaming, and a built-in metrics
24
+ * registry for tokens, requests, and estimated cost.
25
+ *
26
+ * Typical usage:
27
+ * const crew = new AxCrew(config, AxCrewFunctions)
28
+ * await crew.addAllAgents()
29
+ * const planner = crew.agents?.get("Planner")
30
+ * const res = await planner?.forward({ task: "Plan something" })
31
+ *
32
+ * Key behaviors:
33
+ * - Validates and instantiates agents from a config-first model
34
+ * - Shares a mutable state object across all agents in the crew
35
+ * - Supports sub-agents and a function registry per agent
36
+ * - Tracks per-agent and crew-level metrics via MetricsRegistry
37
+ * - Provides helpers to add agents (individually, a subset, or all) and
38
+ * to reset metrics/costs when needed
39
+ */
40
+ class AxCrew {
41
+ private crewConfig: AxCrewConfig;
42
+ private options?: AxCrewOptions;
43
+ functionsRegistry: FunctionRegistryType = {};
44
+ crewId: string;
45
+ agents: Map<string, StatefulAxAgent> | null;
46
+ crewState: StateInstance;
47
+ // Execution history for ACE feedback routing
48
+ private executionHistory: Map<string, {
49
+ taskId: string;
50
+ rootAgent: string;
51
+ involvedAgents: Set<string>;
52
+ taskInput: any;
53
+ agentResults: Map<string, any>;
54
+ startTime: number;
55
+ endTime?: number;
56
+ }> = new Map();
57
+
58
+ constructor(
59
+ crewConfig: AxCrewConfig,
60
+ functionsRegistry: FunctionRegistryType = {},
61
+ options?: AxCrewOptions,
62
+ crewId: string = uuidv4(),
63
+ ) {
64
+ if (!crewConfig || typeof crewConfig !== 'object' || !('crew' in crewConfig)) {
65
+ throw new Error('Invalid crew configuration');
66
+ }
67
+
68
+ crewConfig.crew.forEach((agent: any) => {
69
+ if (!agent.name || agent.name.trim() === '') {
70
+ throw new Error('Agent name cannot be empty');
71
+ }
72
+ });
73
+
74
+ this.crewConfig = crewConfig;
75
+ this.functionsRegistry = functionsRegistry;
76
+ this.crewId = crewId;
77
+ this.options = options;
78
+ this.agents = new Map<string, StatefulAxAgent>();
79
+ this.crewState = createState(crewId);
80
+ this.crewState.set('crewId', crewId);
81
+ }
82
+
83
+ /**
84
+ * Factory function for creating an agent.
85
+ */
86
+ createAgent = async (agentName: string): Promise<StatefulAxAgent> => {
87
+ try {
88
+ const agentConfig: ParsedAgentConfig = await parseAgentConfig(
89
+ agentName,
90
+ this.crewConfig,
91
+ this.functionsRegistry,
92
+ this.crewState,
93
+ this.options
94
+ );
95
+
96
+ const { ai, name, executionMode, axAgentOptions, description, signature, functions, subAgentNames, examples, tracker, forwardOptions } = agentConfig;
97
+
98
+ // Get subagents for the AI agent
99
+ const subAgents = subAgentNames.map((subAgentName: string) => {
100
+ if (!this.agents?.get(subAgentName)) {
101
+ throw new Error(
102
+ `Sub-agent '${subAgentName}' does not exist in available agents.`
103
+ );
104
+ }
105
+ return this.agents?.get(subAgentName);
106
+ });
107
+
108
+ // Dedupe sub-agents by name
109
+ const subAgentSet = new Map<string, StatefulAxAgent>();
110
+ for (const sa of subAgents.filter((agent): agent is StatefulAxAgent => agent !== undefined)) {
111
+ const n = (sa as any)?.agentName ?? (sa as any)?.name ?? '';
112
+ if (!subAgentSet.has(n)) subAgentSet.set(n, sa);
113
+ }
114
+ const uniqueSubAgents = Array.from(subAgentSet.values());
115
+
116
+ // Dedupe functions by name and avoid collision with sub-agent names
117
+ const subAgentNameSet = new Set(uniqueSubAgents.map((sa: any) => sa?.agentName ?? sa?.name).filter(Boolean));
118
+ const uniqueFunctions: AxFunction[] = [];
119
+ const seenFn = new Set<string>();
120
+ for (const fn of functions.filter((fn): fn is AxFunction => fn !== undefined)) {
121
+ const fnName = fn.name;
122
+ if (subAgentNameSet.has(fnName)) continue;
123
+ if (!seenFn.has(fnName)) {
124
+ seenFn.add(fnName);
125
+ uniqueFunctions.push(fn);
126
+ }
127
+ }
128
+
129
+ // Resolve factory functions into AxFunction objects
130
+ const resolvedFunctions: AxFunction[] = uniqueFunctions.map(fn =>
131
+ typeof fn === 'function' ? (fn as () => AxFunction)() : fn
132
+ );
133
+
134
+ // Wrap each function handler to record call count and latency
135
+ const crewId = this.crewId;
136
+ const agentNameForMetrics = name;
137
+ const instrumentedFunctions: AxFunction[] = resolvedFunctions.map(fn => ({
138
+ ...fn,
139
+ func: async (args?: any, extra?: any) => {
140
+ const fnStart = performance.now();
141
+ try {
142
+ return await fn.func(args, extra);
143
+ } finally {
144
+ const latencyMs = performance.now() - fnStart;
145
+ MetricsRegistry.recordFunctionCall(
146
+ { crewId, agent: agentNameForMetrics },
147
+ latencyMs,
148
+ fn.name
149
+ );
150
+ }
151
+ },
152
+ }));
153
+
154
+ // Deferred tool loading
155
+ const mcpFnNames: ReadonlySet<string> = (agentConfig as any).mcpFunctionNames ?? new Set();
156
+ const deferredConfig: DeferredToolsConfig | undefined = (agentConfig as any).deferredTools;
157
+ const deferredManager = new DeferredToolManager(instrumentedFunctions, mcpFnNames, deferredConfig);
158
+ const effectiveFunctions = deferredManager.isActive
159
+ ? deferredManager.getInitialFunctions()
160
+ : instrumentedFunctions;
161
+
162
+ if (deferredManager.isActive) {
163
+ console.log(
164
+ `[ax-crew] Deferred tool loading active for "${name}": ` +
165
+ `${effectiveFunctions.length} core + search_tools, ` +
166
+ `${instrumentedFunctions.length - effectiveFunctions.length + 1} deferred`
167
+ );
168
+ }
169
+
170
+ // Create the agent
171
+ const agentState = { ...this.crewState, crew: this };
172
+ const agent = new StatefulAxAgent(
173
+ ai,
174
+ {
175
+ name,
176
+ executionMode,
177
+ axAgentOptions,
178
+ description,
179
+ definition: (agentConfig as any).definition,
180
+ signature,
181
+ functions: effectiveFunctions,
182
+ agents: uniqueSubAgents,
183
+ examples,
184
+ debug: (agentConfig as any).debug,
185
+ forwardOptions,
186
+ },
187
+ agentState as StateInstance
188
+ );
189
+ (agent as any).costTracker = tracker;
190
+ if (deferredManager.isActive) {
191
+ (agent as any).deferredToolManager = deferredManager;
192
+ }
193
+ (agent as any).__functionsRegistry = this.functionsRegistry;
194
+
195
+ // Initialize ACE if configured
196
+ try {
197
+ const crewAgent = parseCrewConfig(this.crewConfig).crew.find(a => a.name === name) as any;
198
+ const ace: ACEConfig | undefined = crewAgent?.ace;
199
+ if (ace) {
200
+ await (agent as any).initACE?.(ace);
201
+ if (ace.compileOnStart) {
202
+ const { resolveMetric } = await import('./ace.js');
203
+ const metric = resolveMetric(ace.metric, this.functionsRegistry);
204
+ await (agent as any).optimizeOffline?.({ metric, examples });
205
+ }
206
+ }
207
+ } catch {}
208
+
209
+ return agent;
210
+ } catch (error) {
211
+ throw error;
212
+ }
213
+ };
214
+
215
+ async addAgent(agentName: string): Promise<void> {
216
+ try {
217
+ if (!this.agents) {
218
+ this.agents = new Map<string, StatefulAxAgent>();
219
+ }
220
+ if (!this.agents.has(agentName)) {
221
+ this.agents.set(agentName, await this.createAgent(agentName));
222
+ }
223
+ if (this.agents && !this.agents.has(agentName)) {
224
+ this.agents.set(agentName, await this.createAgent(agentName));
225
+ }
226
+ } catch (error) {
227
+ console.error(`Failed to create agent '${agentName}':`);
228
+ throw new Error(`Failed to add agent ${agentName}: ${error instanceof Error ? error.message : String(error)}`);
229
+ }
230
+ }
231
+
232
+ addLazyAgent(agentName: string): void {
233
+ if (!this.agents) {
234
+ this.agents = new Map<string, StatefulAxAgent>();
235
+ }
236
+ if (!this.agents.has(agentName)) {
237
+ this.agents.set(
238
+ agentName,
239
+ new LazyStatefulAxAgent(this, agentName, this.crewConfig) as any
240
+ );
241
+ }
242
+ }
243
+
244
+ async addAgentsToCrew(agentNames: string[]): Promise<Map<string, StatefulAxAgent> | null> {
245
+ try {
246
+ const parsedConfig = parseCrewConfig(this.crewConfig);
247
+ const dependencyMap = new Map<string, string[]>();
248
+ parsedConfig.crew.forEach(agent => {
249
+ dependencyMap.set(agent.name, agent.agents || []);
250
+ });
251
+
252
+ const areDependenciesInitialized = (agentName: string): boolean => {
253
+ const dependencies = dependencyMap.get(agentName) || [];
254
+ return dependencies.every(dep => this.agents?.has(dep));
255
+ };
256
+
257
+ const initializedAgents = new Set<string>();
258
+
259
+ while (initializedAgents.size < agentNames.length) {
260
+ let madeProgress = false;
261
+
262
+ for (const agentName of agentNames) {
263
+ if (initializedAgents.has(agentName)) continue;
264
+ if (areDependenciesInitialized(agentName)) {
265
+ await this.addAgent(agentName);
266
+ initializedAgents.add(agentName);
267
+ madeProgress = true;
268
+ }
269
+ }
270
+
271
+ if (!madeProgress) {
272
+ const remaining = agentNames.filter(agent => !initializedAgents.has(agent));
273
+ throw new Error(`Failed to initialize agents due to missing dependencies: ${remaining.join(', ')}`);
274
+ }
275
+ }
276
+
277
+ return this.agents;
278
+ } catch (error) {
279
+ throw error;
280
+ }
281
+ }
282
+
283
+ async addAllAgents(): Promise<Map<string, StatefulAxAgent> | null> {
284
+ try {
285
+ const parsedConfig = parseCrewConfig(this.crewConfig);
286
+
287
+ const dependencyMap = new Map<string, string[]>();
288
+ parsedConfig.crew.forEach(agent => {
289
+ dependencyMap.set(agent.name, agent.agents || []);
290
+ });
291
+
292
+ const areDependenciesInitialized = (agentName: string): boolean => {
293
+ const dependencies = dependencyMap.get(agentName) || [];
294
+ return dependencies.every(dep => this.agents?.has(dep));
295
+ };
296
+
297
+ const allAgents = parsedConfig.crew.map(agent => agent.name);
298
+ const initializedAgents = new Set<string>();
299
+
300
+ while (initializedAgents.size < allAgents.length) {
301
+ let madeProgress = false;
302
+
303
+ for (const agentName of allAgents) {
304
+ if (initializedAgents.has(agentName)) continue;
305
+ if (areDependenciesInitialized(agentName)) {
306
+ await this.addAgent(agentName);
307
+ initializedAgents.add(agentName);
308
+ madeProgress = true;
309
+ }
310
+ }
311
+
312
+ if (!madeProgress) {
313
+ const remaining = allAgents.filter(agent => !initializedAgents.has(agent));
314
+ throw new Error(`Circular dependency detected or missing dependencies for agents: ${remaining.join(', ')}`);
315
+ }
316
+ }
317
+
318
+ return this.agents;
319
+ } catch (error) {
320
+ throw error;
321
+ }
322
+ }
323
+
324
+ // === ACE execution tracking ===
325
+
326
+ trackAgentExecution(taskId: string, agentName: string, input: any): void {
327
+ if (!this.executionHistory.has(taskId)) {
328
+ this.executionHistory.set(taskId, {
329
+ taskId,
330
+ rootAgent: agentName,
331
+ involvedAgents: new Set([agentName]),
332
+ taskInput: input,
333
+ agentResults: new Map(),
334
+ startTime: Date.now()
335
+ });
336
+ } else {
337
+ const context = this.executionHistory.get(taskId)!;
338
+ context.involvedAgents.add(agentName);
339
+ }
340
+ }
341
+
342
+ recordAgentResult(taskId: string, agentName: string, result: any): void {
343
+ const context = this.executionHistory.get(taskId);
344
+ if (context) {
345
+ context.agentResults.set(agentName, result);
346
+ context.endTime = Date.now();
347
+ }
348
+ }
349
+
350
+ getTaskAgentInvolvement(taskId: string): {
351
+ rootAgent: string;
352
+ involvedAgents: string[];
353
+ taskInput: any;
354
+ agentResults: Map<string, any>;
355
+ duration?: number;
356
+ } | null {
357
+ const context = this.executionHistory.get(taskId);
358
+ if (!context) return null;
359
+
360
+ return {
361
+ rootAgent: context.rootAgent,
362
+ involvedAgents: Array.from(context.involvedAgents),
363
+ taskInput: context.taskInput,
364
+ agentResults: context.agentResults,
365
+ duration: context.endTime ? context.endTime - context.startTime : undefined
366
+ };
367
+ }
368
+
369
+ async applyTaskFeedback(params: {
370
+ taskId: string;
371
+ feedback: string;
372
+ strategy?: 'all' | 'primary' | 'weighted';
373
+ }): Promise<void> {
374
+ const involvement = this.getTaskAgentInvolvement(params.taskId);
375
+ if (!involvement) {
376
+ console.warn(`No execution history found for task ${params.taskId}`);
377
+ return;
378
+ }
379
+
380
+ const { involvedAgents, taskInput, agentResults } = involvement;
381
+ const strategy = params.strategy || 'all';
382
+
383
+ let agentsToUpdate: string[] = [];
384
+ if (strategy === 'primary') {
385
+ agentsToUpdate = [involvement.rootAgent];
386
+ } else if (strategy === 'all' || strategy === 'weighted') {
387
+ agentsToUpdate = involvedAgents;
388
+ }
389
+
390
+ for (const agentName of agentsToUpdate) {
391
+ const agent = this.agents?.get(agentName);
392
+ if (agent && typeof (agent as any).applyOnlineUpdate === 'function') {
393
+ try {
394
+ await (agent as any).applyOnlineUpdate({
395
+ example: taskInput,
396
+ prediction: agentResults.get(agentName),
397
+ feedback: params.feedback
398
+ });
399
+ } catch (error) {
400
+ console.warn(`Failed to apply ACE feedback to agent ${agentName}:`, error);
401
+ }
402
+ }
403
+ }
404
+ }
405
+
406
+ cleanupOldExecutions(maxAgeMs: number = 3600000): void {
407
+ const cutoffTime = Date.now() - maxAgeMs;
408
+ for (const [taskId, context] of this.executionHistory) {
409
+ if (context.startTime < cutoffTime) {
410
+ this.executionHistory.delete(taskId);
411
+ }
412
+ }
413
+ }
414
+
415
+ // === Lifecycle ===
416
+
417
+ destroy() {
418
+ this.agents = null;
419
+ this.executionHistory.clear();
420
+ this.crewState.reset();
421
+ }
422
+
423
+ // === Metrics ===
424
+
425
+ resetCosts(): void {
426
+ if (this.agents) {
427
+ for (const [, agent] of this.agents) {
428
+ try { (agent as any).resetUsage?.(); } catch {}
429
+ try { (agent as any).resetMetrics?.(); } catch {}
430
+ }
431
+ }
432
+ MetricsRegistry.reset({ crewId: this.crewId });
433
+ }
434
+
435
+ getCrewMetrics() {
436
+ return MetricsRegistry.snapshotCrew(this.crewId);
437
+ }
438
+
439
+ resetCrewMetrics(): void {
440
+ MetricsRegistry.reset({ crewId: this.crewId });
441
+ }
442
+ }
443
+
444
+ export { AxCrew };