@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.
@@ -1,1014 +1,3 @@
1
- import { v4 as uuidv4 } from "uuid";
2
- import { AxAgent, AxGen, AxSignature as AxSignatureClass } from "@ax-llm/ax";
3
- import { createState } from "../state/index.js";
4
- import { parseCrewConfig, parseAgentConfig } from "./agentConfig.js";
5
- import { MetricsRegistry } from "../metrics/index.js";
6
- // Extend the AxAgent class from ax-llm
7
- class StatefulAxAgent extends AxAgent {
8
- state;
9
- axai;
10
- agentName;
11
- agentDefinition;
12
- executionMode;
13
- axGenProgram;
14
- costTracker;
15
- debugEnabled = false;
16
- static modernAxAgentRuntime = typeof AxAgent?.prototype?.getFunction === "function" &&
17
- typeof AxAgent?.prototype?.setExamples !== "function";
18
- // ACE-related optional state
19
- aceConfig;
20
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
21
- aceOptimizer;
22
- acePlaybook;
23
- aceBaseInstruction; // Original description before playbook injection
24
- isAxAIService(obj) {
25
- return !!obj && typeof obj.getName === 'function' && typeof obj.chat === 'function';
26
- }
27
- constructor(ai, options, state) {
28
- const { examples, debug } = options;
29
- const resolvedFunctions = (options.functions ?? []).map((fn) => typeof fn === "function" ? fn() : fn);
30
- const resolvedAgents = (options.agents ?? []);
31
- const effectiveDefinition = (options.definition ?? options.description).trim();
32
- if (StatefulAxAgent.modernAxAgentRuntime) {
33
- const configuredAgentOptions = (options.axAgentOptions ?? {});
34
- const configuredAgentGraph = (configuredAgentOptions.agents ?? {});
35
- const configuredFunctionGraph = (configuredAgentOptions.functions ?? {});
36
- const configuredActorOptions = (configuredAgentOptions.actorOptions ?? {});
37
- const configuredResponderOptions = (configuredAgentOptions.responderOptions ?? {});
38
- const modernOptions = {
39
- ...configuredAgentOptions,
40
- debug: debug ?? configuredAgentOptions.debug ?? false,
41
- contextFields: Array.isArray(configuredAgentOptions.contextFields)
42
- ? configuredAgentOptions.contextFields
43
- : [],
44
- };
45
- modernOptions.agents = {
46
- ...configuredAgentGraph,
47
- local: resolvedAgents,
48
- };
49
- modernOptions.functions = {
50
- ...configuredFunctionGraph,
51
- local: resolvedFunctions,
52
- };
53
- if (effectiveDefinition.length > 0) {
54
- modernOptions.actorOptions = {
55
- ...configuredActorOptions,
56
- description: configuredActorOptions.description ?? effectiveDefinition,
57
- };
58
- modernOptions.responderOptions = {
59
- ...configuredResponderOptions,
60
- description: configuredResponderOptions.description ?? effectiveDefinition,
61
- };
62
- }
63
- else {
64
- modernOptions.actorOptions = configuredActorOptions;
65
- modernOptions.responderOptions = configuredResponderOptions;
66
- }
67
- super({
68
- ai,
69
- agentIdentity: {
70
- name: options.name,
71
- description: options.description,
72
- },
73
- signature: options.signature,
74
- }, modernOptions);
75
- }
76
- else {
77
- super({
78
- name: options.name,
79
- description: options.description,
80
- definition: options.definition,
81
- signature: options.signature,
82
- agents: resolvedAgents,
83
- functions: resolvedFunctions,
84
- debug: debug ?? false,
85
- }, {});
86
- }
87
- this.state = state;
88
- this.axai = ai;
89
- this.agentName = options.name;
90
- this.agentDefinition = effectiveDefinition;
91
- this.executionMode = options.executionMode ?? "axgen";
92
- this.debugEnabled = debug ?? false;
93
- // Convert sub-agents to callable functions so AxGen can invoke them as tools
94
- const subAgentFunctions = resolvedAgents
95
- .map(agent => {
96
- try {
97
- return agent.getFunction();
98
- }
99
- catch {
100
- return undefined;
101
- }
102
- })
103
- .filter((fn) => fn !== undefined);
104
- this.axGenProgram = new AxGen(options.signature, {
105
- description: effectiveDefinition,
106
- functions: [...resolvedFunctions, ...subAgentFunctions],
107
- });
108
- for (const agent of resolvedAgents) {
109
- try {
110
- const childName = agent.getFunction().name;
111
- this.axGenProgram.register(agent, childName);
112
- }
113
- catch {
114
- // Best-effort registration for optimizer/introspection support.
115
- }
116
- }
117
- // Apply examples to compatibility layer if provided
118
- if (examples && examples.length > 0) {
119
- this.setExamplesCompat(examples);
120
- }
121
- }
122
- /**
123
- * @deprecated Use setExamplesCompat() to avoid Ax runtime version coupling.
124
- */
125
- setExamples(examples) {
126
- this.setExamplesCompat(examples);
127
- }
128
- setExamplesCompat(examples) {
129
- this.axGenProgram.setExamples(examples);
130
- const baseSetExamples = AxAgent.prototype.setExamples;
131
- if (typeof baseSetExamples === "function") {
132
- baseSetExamples.call(this, examples);
133
- return;
134
- }
135
- const internalProgram = this.program;
136
- if (typeof internalProgram?.setExamples === "function") {
137
- internalProgram.setExamples(examples);
138
- }
139
- }
140
- /**
141
- * @deprecated Use setDescriptionCompat() to avoid Ax runtime version coupling.
142
- */
143
- setDescription(description) {
144
- this.setDescriptionCompat(description);
145
- }
146
- setDescriptionCompat(description) {
147
- this.agentDefinition = description;
148
- this.axGenProgram.setDescription(description);
149
- const baseSetDescription = AxAgent.prototype.setDescription;
150
- if (typeof baseSetDescription === "function") {
151
- baseSetDescription.call(this, description);
152
- return;
153
- }
154
- const agentRuntime = this;
155
- if (typeof agentRuntime.program?.setDescription === "function") {
156
- agentRuntime.program.setDescription(description);
157
- }
158
- agentRuntime.actorDescription = description;
159
- agentRuntime.responderDescription = description;
160
- if (typeof agentRuntime._buildSplitPrograms === "function") {
161
- agentRuntime._buildSplitPrograms();
162
- }
163
- }
164
- getUsage() {
165
- if (this.executionMode === "axgen") {
166
- return this.axGenProgram.getUsage();
167
- }
168
- return super.getUsage();
169
- }
170
- resetUsage() {
171
- this.axGenProgram.resetUsage();
172
- super.resetUsage();
173
- }
174
- resolveInvocationArgs(first, second, third) {
175
- const calledWithAI = this.isAxAIService(first);
176
- const ai = (calledWithAI ? first : this.axai);
177
- if (!ai) {
178
- throw new Error(`No AI instance is configured for agent "${this.agentName}"`);
179
- }
180
- const values = (calledWithAI ? second : first);
181
- const options = (calledWithAI ? third : second);
182
- return { ai, values, options, calledWithAI };
183
- }
184
- async executeForwardByMode(mode, ai, values, options) {
185
- if (mode === "axgen") {
186
- return this.axGenProgram.forward(ai, values, options);
187
- }
188
- return super.forward(ai, values, options);
189
- }
190
- recordUsageMetrics(labels, mode) {
191
- const builtIn = mode === "axgen" ? this.axGenProgram.getUsage?.() : super.getUsage?.();
192
- if (!Array.isArray(builtIn))
193
- return;
194
- const totals = builtIn.reduce((acc, u) => {
195
- const pt = u.tokens?.promptTokens ?? u.promptTokens ?? 0;
196
- const ct = u.tokens?.completionTokens ?? u.completionTokens ?? 0;
197
- acc.promptTokens += typeof pt === "number" ? pt : 0;
198
- acc.completionTokens += typeof ct === "number" ? ct : 0;
199
- const model = u.model ||
200
- this.axai?.getLastUsedChatModel?.() ||
201
- this.axai?.defaults?.model;
202
- if (model) {
203
- acc.byModel[model] = (acc.byModel[model] || 0) + (pt + ct);
204
- }
205
- return acc;
206
- }, { promptTokens: 0, completionTokens: 0, byModel: {} });
207
- MetricsRegistry.recordTokens(labels, {
208
- promptTokens: totals.promptTokens,
209
- completionTokens: totals.completionTokens,
210
- totalTokens: totals.promptTokens + totals.completionTokens,
211
- });
212
- const costTracker = this.costTracker;
213
- try {
214
- for (const [m, count] of Object.entries(totals.byModel)) {
215
- costTracker?.trackTokens?.(count, m);
216
- }
217
- const totalUSD = Number(costTracker?.getCurrentCost?.() ?? 0);
218
- if (!Number.isNaN(totalUSD) && totalUSD > 0) {
219
- MetricsRegistry.recordEstimatedCost(labels, totalUSD);
220
- }
221
- }
222
- catch { }
223
- }
224
- async runForwardInvocation(mode, first, second, third) {
225
- const { ai, values, options, calledWithAI } = this.resolveInvocationArgs(first, second, third);
226
- const start = performance.now();
227
- const crewId = this.state?.crewId || this.state.get?.("crewId") || "default";
228
- const labels = { crewId, agent: this.agentName };
229
- const taskId = `task_${crewId}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
230
- // Track execution context in crew for ACE feedback routing
231
- const crewInstance = this.state?.crew;
232
- if (crewInstance) {
233
- if (calledWithAI) {
234
- const parentTaskId = this.state?.currentTaskId;
235
- if (parentTaskId) {
236
- crewInstance.trackAgentExecution(parentTaskId, this.agentName, values);
237
- }
238
- }
239
- else {
240
- crewInstance.trackAgentExecution(taskId, this.agentName, values);
241
- this.state.currentTaskId = taskId;
242
- }
243
- }
244
- if (this.debugEnabled) {
245
- console.log(`[ACE Debug] forward() called, mode=${mode}, aceConfig=${!!this.aceConfig}`);
246
- }
247
- if (this.aceConfig) {
248
- await this.composeInstructionWithPlaybook();
249
- }
250
- const result = await this.executeForwardByMode(mode, ai, values, options);
251
- const durationMs = performance.now() - start;
252
- MetricsRegistry.recordRequest(labels, false, durationMs);
253
- this.recordUsageMetrics(labels, mode);
254
- if (crewInstance) {
255
- if (calledWithAI) {
256
- const parentTaskId = this.state?.currentTaskId;
257
- if (parentTaskId) {
258
- crewInstance.recordAgentResult(parentTaskId, this.agentName, result);
259
- }
260
- }
261
- else {
262
- crewInstance.recordAgentResult(taskId, this.agentName, result);
263
- delete this.state.currentTaskId;
264
- result._taskId = taskId;
265
- }
266
- }
267
- return result;
268
- }
269
- // Implementation
270
- async forward(first, second, third) {
271
- return this.runForwardInvocation(this.executionMode, first, second, third);
272
- }
273
- runStreamingInvocation(mode, first, second, third) {
274
- const { ai, values, options } = this.resolveInvocationArgs(first, second, third);
275
- const start = performance.now();
276
- const crewId = this.state?.crewId || this.state.get?.("crewId") || "default";
277
- const labels = { crewId, agent: this.agentName };
278
- const createStream = () => mode === "axgen"
279
- ? this.axGenProgram.streamingForward(ai, values, options)
280
- : super.streamingForward(ai, values, options);
281
- const wrappedGenerator = (async function* () {
282
- if (this.aceConfig) {
283
- await this.composeInstructionWithPlaybook();
284
- }
285
- const streamingResult = createStream();
286
- try {
287
- for await (const chunk of streamingResult) {
288
- yield chunk;
289
- }
290
- }
291
- finally {
292
- const durationMs = performance.now() - start;
293
- MetricsRegistry.recordRequest(labels, true, durationMs);
294
- this.recordUsageMetrics(labels, mode);
295
- }
296
- }).bind(this)();
297
- return wrappedGenerator;
298
- }
299
- // Implementation
300
- streamingForward(first, second, third) {
301
- return this.runStreamingInvocation(this.executionMode, first, second, third);
302
- }
303
- // Legacy cost API removed: rely on Ax trackers for cost reporting
304
- getLastUsageCost() { return null; }
305
- // Get the accumulated costs for all runs of this agent
306
- getAccumulatedCosts() { return null; }
307
- // Metrics API for this agent
308
- /**
309
- * Get the current metrics snapshot for this agent.
310
- * Includes request counts, error rates, token usage, estimated USD cost, and function call stats.
311
- *
312
- * @returns A metrics snapshot scoped to this agent within its crew.
313
- */
314
- getMetrics() {
315
- const crewId = this.state?.crewId || (this.state.get?.('crewId')) || 'default';
316
- return MetricsRegistry.snapshot({ crewId, agent: this.agentName });
317
- }
318
- /**
319
- * Reset all tracked metrics for this agent (does not affect other agents).
320
- * Call this to start fresh measurement windows for the agent.
321
- */
322
- resetMetrics() {
323
- const crewId = this.state?.crewId || (this.state.get?.('crewId')) || 'default';
324
- MetricsRegistry.reset({ crewId, agent: this.agentName });
325
- }
326
- // =============
327
- // ACE API - Agentic Context Engineering for online learning
328
- // Reference: https://axllm.dev/ace/
329
- // =============
330
- /**
331
- * Initialize ACE (Agentic Context Engineering) for this agent.
332
- * Builds the optimizer and loads any initial playbook from persistence.
333
- * Sets up the optimizer for online-only mode if compileOnStart is false.
334
- */
335
- async initACE(ace) {
336
- this.aceConfig = ace;
337
- if (!ace)
338
- return;
339
- try {
340
- // Capture base instruction BEFORE any playbook injection (mirrors AxACE.extractProgramInstruction)
341
- this.aceBaseInstruction =
342
- this.agentDefinition || this.getSignature().getDescription() || '';
343
- const { buildACEOptimizer, loadInitialPlaybook, createEmptyPlaybook } = await import('./ace.js');
344
- // Build optimizer with agent's AI as student
345
- this.aceOptimizer = buildACEOptimizer(this.axai, ace);
346
- // For online-only mode (no offline compile), we need to set the program
347
- // reference so applyOnlineUpdate can work. AxACE requires compile() to
348
- // set the program, but we can set it directly for online-only use.
349
- if (!ace.compileOnStart) {
350
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
351
- this.aceOptimizer.program = this;
352
- }
353
- // Load initial playbook or create empty one
354
- const initial = await loadInitialPlaybook(ace.persistence);
355
- this.applyPlaybook(initial ?? createEmptyPlaybook());
356
- if (this.debugEnabled) {
357
- console.log(`[ACE Debug] Initialized for ${this.agentName}, base instruction: ${this.aceBaseInstruction?.slice(0, 50)}...`);
358
- }
359
- }
360
- catch (error) {
361
- console.warn(`Failed to initialize ACE for agent ${this.agentName}:`, error);
362
- }
363
- }
364
- /**
365
- * Run offline ACE compilation with examples and metric.
366
- * Compiles the playbook based on training examples.
367
- */
368
- async optimizeOffline(params) {
369
- if (!this.aceConfig || !this.aceOptimizer)
370
- return;
371
- try {
372
- const { runOfflineCompile, resolveMetric } = await import('./ace.js');
373
- const registry = this.__functionsRegistry;
374
- const metric = params?.metric || resolveMetric(this.aceConfig.metric, registry || {});
375
- const examples = params?.examples || [];
376
- if (!metric || examples.length === 0) {
377
- console.warn(`ACE offline compile skipped for ${this.agentName}: missing metric or examples`);
378
- return;
379
- }
380
- const result = await runOfflineCompile({
381
- program: this,
382
- optimizer: this.aceOptimizer,
383
- metric,
384
- examples,
385
- persistence: this.aceConfig.persistence
386
- });
387
- // Apply optimized playbook if compilation succeeded
388
- if (result?.artifact?.playbook) {
389
- await this.applyPlaybook(result.artifact.playbook);
390
- }
391
- }
392
- catch (error) {
393
- console.warn(`ACE offline compile failed for ${this.agentName}:`, error);
394
- }
395
- }
396
- /**
397
- * Apply online ACE update based on user feedback.
398
- *
399
- * For preference-based feedback (e.g., "only show flights between 9am-12pm"),
400
- * we use our own feedback analyzer that preserves specificity.
401
- *
402
- * Note: AxACE's built-in curator is designed for error correction (severity mismatches)
403
- * and tends to over-abstract preference feedback into generic guidelines.
404
- * We bypass it and directly use our feedback analyzer for better results.
405
- */
406
- async applyOnlineUpdate(params) {
407
- if (!this.aceConfig)
408
- return;
409
- if (!params.feedback?.trim())
410
- return; // Nothing to do without feedback
411
- try {
412
- const { persistPlaybook, addFeedbackToPlaybook, createEmptyPlaybook } = await import('./ace.js');
413
- // Get or create playbook
414
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
415
- let playbook = this.acePlaybook ?? this.aceOptimizer?.playbook;
416
- if (!playbook) {
417
- playbook = createEmptyPlaybook();
418
- }
419
- if (this.debugEnabled) {
420
- console.log(`[ACE Debug] Adding feedback to playbook: "${params.feedback}"`);
421
- }
422
- // Use teacher AI (or student AI as fallback) for smart categorization
423
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
424
- const teacherAI = this.aceOptimizer?.teacherAI;
425
- const aiForAnalysis = teacherAI ?? this.axai;
426
- // Directly add feedback to playbook using our analyzer (preserves specificity)
427
- await addFeedbackToPlaybook(playbook, params.feedback, aiForAnalysis, this.debugEnabled);
428
- // Store updated playbook (injection happens in next forward() call)
429
- this.applyPlaybook(playbook);
430
- // Sync with optimizer if available
431
- if (this.aceOptimizer) {
432
- this.aceOptimizer.playbook = playbook;
433
- }
434
- // Persist if auto-persist enabled
435
- if (this.aceConfig.persistence?.autoPersist) {
436
- await persistPlaybook(playbook, this.aceConfig.persistence);
437
- }
438
- if (this.debugEnabled) {
439
- console.log(`[ACE Debug] Playbook updated, sections: ${Object.keys(playbook.sections || {}).join(', ')}`);
440
- }
441
- }
442
- catch (error) {
443
- console.warn(`ACE online update failed for ${this.agentName}:`, error);
444
- }
445
- }
446
- /**
447
- * Get the current ACE playbook for this agent.
448
- */
449
- getPlaybook() {
450
- return this.acePlaybook;
451
- }
452
- /**
453
- * Apply an ACE playbook to this agent.
454
- * Stores the playbook for use in next forward() call.
455
- * Note: Playbook is composed into instruction BEFORE each forward(), mirroring AxACE.compile behavior.
456
- */
457
- applyPlaybook(pb) {
458
- this.acePlaybook = pb;
459
- // Also update optimizer's internal playbook if possible
460
- try {
461
- this.aceOptimizer.playbook = pb;
462
- }
463
- catch {
464
- // Ignore - optimizer may not be initialized yet
465
- }
466
- }
467
- /**
468
- * Compose instruction with current playbook and set on agent.
469
- * This mirrors what AxACE does internally before each forward() during compile().
470
- * Should be called BEFORE forward() to ensure playbook is in the prompt.
471
- */
472
- async composeInstructionWithPlaybook() {
473
- const playbook = this.acePlaybook ?? this.aceOptimizer?.playbook;
474
- if (this.debugEnabled) {
475
- console.log(`[ACE Debug] composeInstructionWithPlaybook called`);
476
- console.log(`[ACE Debug] playbook exists: ${!!playbook}, sections: ${playbook ? Object.keys(playbook.sections || {}).length : 0}`);
477
- console.log(`[ACE Debug] baseInstruction: "${this.aceBaseInstruction?.slice(0, 50)}..."`);
478
- }
479
- if (!playbook)
480
- return;
481
- try {
482
- const { renderPlaybook } = await import('./ace.js');
483
- const rendered = renderPlaybook(playbook);
484
- if (this.debugEnabled) {
485
- console.log(`[ACE Debug] rendered playbook (${rendered.length} chars): ${rendered.slice(0, 100)}...`);
486
- }
487
- if (!rendered)
488
- return;
489
- // Compose: base instruction + playbook (just like AxACE.composeInstruction)
490
- const baseInstruction = this.aceBaseInstruction || '';
491
- const combinedInstruction = [baseInstruction.trim(), '', rendered]
492
- .filter((part) => part.trim().length > 0)
493
- .join('\n\n');
494
- if (this.debugEnabled) {
495
- console.log(`[ACE Debug] combinedInstruction (${combinedInstruction.length} chars)`);
496
- }
497
- if (combinedInstruction.length >= 20) {
498
- // Call setDescription on the internal program (like AxACE does)
499
- // AxAgent.setDescription() only updates the signature, but we need
500
- // to update the program's description which is used for the system prompt
501
- const program = this.program;
502
- if (program?.setDescription) {
503
- program.setDescription(combinedInstruction);
504
- }
505
- // Also update via AxAgent's setDescription for consistency
506
- this.setDescription(combinedInstruction);
507
- if (this.debugEnabled) {
508
- console.log(`[ACE Debug] setDescription called successfully`);
509
- console.log(`[ACE Debug] Verifying - signature desc length: ${this.getSignature().getDescription()?.length}`);
510
- }
511
- }
512
- }
513
- catch (error) {
514
- console.warn('[ACE Debug] Failed to compose instruction:', error);
515
- }
516
- }
517
- }
518
- /**
519
- * Lightweight proxy that stands in for a real agent in the crew's agent map.
520
- * It exposes the same `getFunction()` interface (built from the crew config)
521
- * but defers the expensive `createAgent()` call — and therefore MCP server
522
- * startup — until the Manager actually delegates to it.
523
- *
524
- * Usage: `crew.addLazyAgent("CreateChart")` instead of `crew.addAgent("CreateChart")`
525
- */
526
- class LazyStatefulAxAgent {
527
- realAgent = null;
528
- crewRef; // AxCrew — forward-declared to avoid circular ref
529
- agentName;
530
- description;
531
- signatureStr;
532
- func;
533
- _id = "lazy";
534
- constructor(crewRef, agentName, crewConfig) {
535
- this.crewRef = crewRef;
536
- this.agentName = agentName;
537
- const agentDef = parseCrewConfig(crewConfig).crew.find((a) => a.name === agentName);
538
- if (!agentDef) {
539
- throw new Error(`Agent "${agentName}" not found in crew config`);
540
- }
541
- this.description = agentDef.description;
542
- this.signatureStr = agentDef.signature;
543
- // Build the tool schema from the signature's input fields
544
- const sig = new AxSignatureClass(this.signatureStr);
545
- const parameters = sig.toInputJSONSchema();
546
- this.func = {
547
- name: agentName.replace(/[^a-zA-Z0-9_]/g, "_").toLowerCase(),
548
- description: this.description,
549
- parameters,
550
- func: async (args) => {
551
- const agent = await this.resolve();
552
- return agent.forward(args);
553
- },
554
- };
555
- }
556
- async resolve() {
557
- if (!this.realAgent) {
558
- const agent = await this.crewRef.createAgent(this.agentName);
559
- agent.setId(this._id);
560
- this.realAgent = agent;
561
- }
562
- return this.realAgent;
563
- }
564
- // AxAgentic interface
565
- getFunction() {
566
- return this.func;
567
- }
568
- getSignature() {
569
- return new AxSignatureClass(this.signatureStr);
570
- }
571
- // AxProgrammable / AxTunable stubs
572
- getId() { return this._id; }
573
- setId(id) { this._id = id; }
574
- getTraces() { return this.realAgent?.getTraces() ?? []; }
575
- setDemos() { }
576
- getUsage() { return this.realAgent?.getUsage() ?? []; }
577
- resetUsage() { this.realAgent?.resetUsage(); }
578
- // Forward / streaming — resolve on demand
579
- async forward(...args) {
580
- const agent = await this.resolve();
581
- return agent.forward(...args);
582
- }
583
- streamingForward(...args) {
584
- // Must be sync to match the interface, so we wrap in an async generator
585
- const self = this;
586
- async function* lazyStream() {
587
- const agent = await self.resolve();
588
- yield* agent.streamingForward(...args);
589
- }
590
- return lazyStream();
591
- }
592
- }
593
- /**
594
- * AxCrew orchestrates a set of Ax agents that share state,
595
- * tools (functions), optional MCP servers, streaming, and a built-in metrics
596
- * registry for tokens, requests, and estimated cost.
597
- *
598
- * Typical usage:
599
- * const crew = new AxCrew(config, AxCrewFunctions)
600
- * await crew.addAllAgents()
601
- * const planner = crew.agents?.get("Planner")
602
- * const res = await planner?.forward({ task: "Plan something" })
603
- *
604
- * Key behaviors:
605
- * - Validates and instantiates agents from a config-first model
606
- * - Shares a mutable state object across all agents in the crew
607
- * - Supports sub-agents and a function registry per agent
608
- * - Tracks per-agent and crew-level metrics via MetricsRegistry
609
- * - Provides helpers to add agents (individually, a subset, or all) and
610
- * to reset metrics/costs when needed
611
- */
612
- class AxCrew {
613
- crewConfig;
614
- options;
615
- functionsRegistry = {};
616
- crewId;
617
- agents;
618
- state;
619
- // Execution history for ACE feedback routing
620
- executionHistory = new Map();
621
- /**
622
- * Creates an instance of AxCrew.
623
- * @param {AxCrewConfig} crewConfig - JSON object with crew configuration.
624
- * @param {FunctionRegistryType} [functionsRegistry={}] - The registry of functions to use in the crew.
625
- * @param {AxCrewOptions} [options] - Optional settings for the crew (e.g., telemetry).
626
- * @param {string} [crewId=uuidv4()] - The unique identifier for the crew.
627
- */
628
- constructor(crewConfig, functionsRegistry = {}, options, crewId = uuidv4()) {
629
- // Basic validation of crew configuration
630
- if (!crewConfig || typeof crewConfig !== 'object' || !('crew' in crewConfig)) {
631
- throw new Error('Invalid crew configuration');
632
- }
633
- // Validate each agent in the crew
634
- crewConfig.crew.forEach((agent) => {
635
- if (!agent.name || agent.name.trim() === '') {
636
- throw new Error('Agent name cannot be empty');
637
- }
638
- });
639
- this.crewConfig = crewConfig;
640
- this.functionsRegistry = functionsRegistry;
641
- this.crewId = crewId;
642
- this.options = options;
643
- this.agents = new Map();
644
- this.state = createState(crewId);
645
- // Make crewId discoverable to metrics
646
- this.state.set('crewId', crewId);
647
- }
648
- /**
649
- * Factory function for creating an agent.
650
- * @param {string} agentName - The name of the agent to create.
651
- * @returns {StatefulAxAgent} The created StatefulAxAgent instance.
652
- * @throws Will throw an error if the agent creation fails.
653
- */
654
- createAgent = async (agentName) => {
655
- try {
656
- const agentConfig = await parseAgentConfig(agentName, this.crewConfig, this.functionsRegistry, this.state, this.options);
657
- // Destructure with type assertion
658
- const { ai, name, executionMode, axAgentOptions, description, signature, functions, subAgentNames, examples, tracker } = agentConfig;
659
- // Get subagents for the AI agent
660
- const subAgents = subAgentNames.map((subAgentName) => {
661
- if (!this.agents?.get(subAgentName)) {
662
- throw new Error(`Sub-agent '${subAgentName}' does not exist in available agents.`);
663
- }
664
- return this.agents?.get(subAgentName);
665
- });
666
- // Dedupe sub-agents by name (defensive)
667
- const subAgentSet = new Map();
668
- for (const sa of subAgents.filter((agent) => agent !== undefined)) {
669
- const n = sa?.agentName ?? sa?.name ?? '';
670
- if (!subAgentSet.has(n))
671
- subAgentSet.set(n, sa);
672
- }
673
- const uniqueSubAgents = Array.from(subAgentSet.values());
674
- // Dedupe functions by name and avoid collision with sub-agent names
675
- const subAgentNameSet = new Set(uniqueSubAgents.map((sa) => sa?.agentName ?? sa?.name).filter(Boolean));
676
- const uniqueFunctions = [];
677
- const seenFn = new Set();
678
- for (const fn of functions.filter((fn) => fn !== undefined)) {
679
- const fnName = fn.name;
680
- if (subAgentNameSet.has(fnName)) {
681
- // Skip function that collides with a sub-agent name
682
- continue;
683
- }
684
- if (!seenFn.has(fnName)) {
685
- seenFn.add(fnName);
686
- uniqueFunctions.push(fn);
687
- }
688
- }
689
- // Resolve factory functions (e.g., () => new Foo(state).toFunction()) into
690
- // AxFunction objects before instrumenting, since spreading a JS function
691
- // does not copy non-enumerable properties like `name`.
692
- const resolvedFunctions = uniqueFunctions.map(fn => typeof fn === 'function' ? fn() : fn);
693
- // Wrap each function handler to record call count and latency in MetricsRegistry
694
- const crewId = this.crewId;
695
- const agentNameForMetrics = name;
696
- const instrumentedFunctions = resolvedFunctions.map(fn => ({
697
- ...fn,
698
- func: async (args, extra) => {
699
- const fnStart = performance.now();
700
- try {
701
- return await fn.func(args, extra);
702
- }
703
- finally {
704
- const latencyMs = performance.now() - fnStart;
705
- MetricsRegistry.recordFunctionCall({ crewId, agent: agentNameForMetrics }, latencyMs, fn.name);
706
- }
707
- },
708
- }));
709
- // Create an instance of StatefulAxAgent
710
- // Set crew reference in state for execution tracking (ACE feedback routing)
711
- const agentState = { ...this.state, crew: this };
712
- const agent = new StatefulAxAgent(ai, {
713
- name,
714
- executionMode,
715
- axAgentOptions,
716
- description,
717
- definition: agentConfig.definition,
718
- signature,
719
- functions: instrumentedFunctions,
720
- agents: uniqueSubAgents,
721
- examples,
722
- debug: agentConfig.debug,
723
- }, agentState);
724
- agent.costTracker = tracker;
725
- agent.__functionsRegistry = this.functionsRegistry;
726
- // Initialize ACE if configured
727
- try {
728
- const crewAgent = parseCrewConfig(this.crewConfig).crew.find(a => a.name === name);
729
- const ace = crewAgent?.ace;
730
- if (ace) {
731
- await agent.initACE?.(ace);
732
- if (ace.compileOnStart) {
733
- const { resolveMetric } = await import('./ace.js');
734
- const metric = resolveMetric(ace.metric, this.functionsRegistry);
735
- await agent.optimizeOffline?.({ metric, examples });
736
- }
737
- }
738
- }
739
- catch { }
740
- return agent;
741
- }
742
- catch (error) {
743
- throw error;
744
- }
745
- };
746
- /**
747
- * Adds an agent to the crew by name.
748
- * @param {string} agentName - The name of the agent to add.
749
- */
750
- async addAgent(agentName) {
751
- try {
752
- if (!this.agents) {
753
- this.agents = new Map();
754
- }
755
- if (!this.agents.has(agentName)) {
756
- this.agents.set(agentName, await this.createAgent(agentName));
757
- }
758
- if (this.agents && !this.agents.has(agentName)) {
759
- this.agents.set(agentName, await this.createAgent(agentName));
760
- }
761
- }
762
- catch (error) {
763
- console.error(`Failed to create agent '${agentName}':`);
764
- throw new Error(`Failed to add agent ${agentName}: ${error instanceof Error ? error.message : String(error)}`);
765
- }
766
- }
767
- /**
768
- * Adds a lazy agent to the crew by name.
769
- * The agent's tool schema is built immediately from the crew config,
770
- * but the expensive initialization (MCP servers, AI client, etc.) is
771
- * deferred until the Manager actually delegates to this agent.
772
- *
773
- * Use this for sub-agents that may not be needed on every request
774
- * (e.g., agents with stdio MCP servers that spawn a process).
775
- *
776
- * @param {string} agentName - The name of the agent to add lazily.
777
- */
778
- addLazyAgent(agentName) {
779
- if (!this.agents) {
780
- this.agents = new Map();
781
- }
782
- if (!this.agents.has(agentName)) {
783
- this.agents.set(agentName, new LazyStatefulAxAgent(this, agentName, this.crewConfig));
784
- }
785
- }
786
- /**
787
- * Sets up agents in the crew by name.
788
- * For an array of Agent names provided, it adds
789
- * the agent to the crew if not already present.
790
- * @param {string[]} agentNames - An array of agent names to configure.
791
- * @returns {Map<string, StatefulAxAgent> | null} A map of agent names to their corresponding instances.
792
- */
793
- async addAgentsToCrew(agentNames) {
794
- try {
795
- // Parse the crew config to get agent dependencies
796
- const parsedConfig = parseCrewConfig(this.crewConfig);
797
- const dependencyMap = new Map();
798
- parsedConfig.crew.forEach(agent => {
799
- dependencyMap.set(agent.name, agent.agents || []);
800
- });
801
- // Function to check if all dependencies are initialized
802
- const areDependenciesInitialized = (agentName) => {
803
- const dependencies = dependencyMap.get(agentName) || [];
804
- return dependencies.every(dep => this.agents?.has(dep));
805
- };
806
- // Initialize agents sequentially based on dependencies
807
- const initializedAgents = new Set();
808
- while (initializedAgents.size < agentNames.length) {
809
- let madeProgress = false;
810
- for (const agentName of agentNames) {
811
- // Skip if already initialized
812
- if (initializedAgents.has(agentName))
813
- continue;
814
- // Check if all dependencies are initialized
815
- if (areDependenciesInitialized(agentName)) {
816
- await this.addAgent(agentName);
817
- initializedAgents.add(agentName);
818
- madeProgress = true;
819
- }
820
- }
821
- // If we couldn't initialize any agents in this iteration, we have a circular dependency
822
- if (!madeProgress) {
823
- const remaining = agentNames.filter(agent => !initializedAgents.has(agent));
824
- throw new Error(`Failed to initialize agents due to missing dependencies: ${remaining.join(', ')}`);
825
- }
826
- }
827
- return this.agents;
828
- }
829
- catch (error) {
830
- throw error;
831
- }
832
- }
833
- async addAllAgents() {
834
- try {
835
- // Parse the crew config and get all agent configs
836
- const parsedConfig = parseCrewConfig(this.crewConfig);
837
- // Create a map of agent dependencies
838
- const dependencyMap = new Map();
839
- parsedConfig.crew.forEach(agent => {
840
- dependencyMap.set(agent.name, agent.agents || []);
841
- });
842
- // Function to check if all dependencies are initialized
843
- const areDependenciesInitialized = (agentName) => {
844
- const dependencies = dependencyMap.get(agentName) || [];
845
- return dependencies.every(dep => this.agents?.has(dep));
846
- };
847
- // Get all agent names
848
- const allAgents = parsedConfig.crew.map(agent => agent.name);
849
- const initializedAgents = new Set();
850
- // Keep trying to initialize agents until all are done or we can't make progress
851
- while (initializedAgents.size < allAgents.length) {
852
- let madeProgress = false;
853
- for (const agentName of allAgents) {
854
- // Skip if already initialized
855
- if (initializedAgents.has(agentName))
856
- continue;
857
- // Check if all dependencies are initialized
858
- if (areDependenciesInitialized(agentName)) {
859
- await this.addAgent(agentName);
860
- initializedAgents.add(agentName);
861
- madeProgress = true;
862
- }
863
- }
864
- // If we couldn't initialize any agents in this iteration, we have a circular dependency
865
- if (!madeProgress) {
866
- const remaining = allAgents.filter(agent => !initializedAgents.has(agent));
867
- throw new Error(`Circular dependency detected or missing dependencies for agents: ${remaining.join(', ')}`);
868
- }
869
- }
870
- return this.agents;
871
- }
872
- catch (error) {
873
- throw error;
874
- }
875
- }
876
- /**
877
- * Track agent execution for ACE feedback routing
878
- */
879
- trackAgentExecution(taskId, agentName, input) {
880
- if (!this.executionHistory.has(taskId)) {
881
- this.executionHistory.set(taskId, {
882
- taskId,
883
- rootAgent: agentName,
884
- involvedAgents: new Set([agentName]),
885
- taskInput: input,
886
- agentResults: new Map(),
887
- startTime: Date.now()
888
- });
889
- }
890
- else {
891
- // Add to involved agents if not already present
892
- const context = this.executionHistory.get(taskId);
893
- context.involvedAgents.add(agentName);
894
- }
895
- }
896
- /**
897
- * Record agent result for ACE feedback routing
898
- */
899
- recordAgentResult(taskId, agentName, result) {
900
- const context = this.executionHistory.get(taskId);
901
- if (context) {
902
- context.agentResults.set(agentName, result);
903
- context.endTime = Date.now();
904
- }
905
- }
906
- /**
907
- * Get agent involvement for a task (used for ACE feedback routing)
908
- */
909
- getTaskAgentInvolvement(taskId) {
910
- const context = this.executionHistory.get(taskId);
911
- if (!context)
912
- return null;
913
- return {
914
- rootAgent: context.rootAgent,
915
- involvedAgents: Array.from(context.involvedAgents),
916
- taskInput: context.taskInput,
917
- agentResults: context.agentResults,
918
- duration: context.endTime ? context.endTime - context.startTime : undefined
919
- };
920
- }
921
- /**
922
- * Apply feedback to agents involved in a task for ACE online learning
923
- */
924
- async applyTaskFeedback(params) {
925
- const involvement = this.getTaskAgentInvolvement(params.taskId);
926
- if (!involvement) {
927
- console.warn(`No execution history found for task ${params.taskId}`);
928
- return;
929
- }
930
- const { involvedAgents, taskInput, agentResults } = involvement;
931
- const strategy = params.strategy || 'all';
932
- // Determine which agents to update based on strategy
933
- let agentsToUpdate = [];
934
- if (strategy === 'primary') {
935
- agentsToUpdate = [involvement.rootAgent];
936
- }
937
- else if (strategy === 'all' || strategy === 'weighted') {
938
- agentsToUpdate = involvedAgents;
939
- }
940
- // Apply feedback to each involved agent
941
- for (const agentName of agentsToUpdate) {
942
- const agent = this.agents?.get(agentName);
943
- if (agent && typeof agent.applyOnlineUpdate === 'function') {
944
- try {
945
- await agent.applyOnlineUpdate({
946
- example: taskInput,
947
- prediction: agentResults.get(agentName),
948
- feedback: params.feedback
949
- });
950
- }
951
- catch (error) {
952
- console.warn(`Failed to apply ACE feedback to agent ${agentName}:`, error);
953
- }
954
- }
955
- }
956
- }
957
- /**
958
- * Clean up old execution history (call periodically to prevent memory leaks)
959
- */
960
- cleanupOldExecutions(maxAgeMs = 3600000) {
961
- const cutoffTime = Date.now() - maxAgeMs;
962
- for (const [taskId, context] of this.executionHistory) {
963
- if (context.startTime < cutoffTime) {
964
- this.executionHistory.delete(taskId);
965
- }
966
- }
967
- }
968
- /**
969
- * Cleans up the crew by dereferencing agents and resetting the state.
970
- */
971
- destroy() {
972
- this.agents = null;
973
- this.executionHistory.clear();
974
- this.state.reset();
975
- }
976
- /**
977
- * Resets all cost and usage tracking for the entire crew.
978
- * Also calls each agent's `resetUsage` (if available) and clears crew-level metrics.
979
- */
980
- resetCosts() {
981
- // Reset AxAgent built-in usage and our metrics registry
982
- if (this.agents) {
983
- for (const [, agent] of this.agents) {
984
- try {
985
- agent.resetUsage?.();
986
- }
987
- catch { }
988
- try {
989
- agent.resetMetrics?.();
990
- }
991
- catch { }
992
- }
993
- }
994
- MetricsRegistry.reset({ crewId: this.crewId });
995
- }
996
- // Metrics API
997
- /**
998
- * Get an aggregate metrics snapshot for the entire crew.
999
- * Sums requests, errors, tokens, and estimated cost across all agents in the crew.
1000
- *
1001
- * @returns Crew-level metrics snapshot.
1002
- */
1003
- getCrewMetrics() {
1004
- return MetricsRegistry.snapshotCrew(this.crewId);
1005
- }
1006
- /**
1007
- * Reset all tracked metrics for the entire crew.
1008
- * Use to clear totals before a new measurement period.
1009
- */
1010
- resetCrewMetrics() {
1011
- MetricsRegistry.reset({ crewId: this.crewId });
1012
- }
1013
- }
1014
- export { AxCrew, LazyStatefulAxAgent };
1
+ export { StatefulAxAgent } from './statefulAgent.js';
2
+ export { LazyStatefulAxAgent } from './lazyAgent.js';
3
+ export { AxCrew } from './crew.js';