@amitdeshmukh/ax-crew 3.11.3 → 4.1.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,4 +1,4 @@
1
- import { AxCrew } from "../dist/index.js";
1
+ import { AxCrew, AxCrewConfig } from "../dist/index.js";
2
2
 
3
3
  import dotenv from "dotenv";
4
4
  dotenv.config();
@@ -9,15 +9,18 @@ const config = {
9
9
  {
10
10
  name: "MapsAgent",
11
11
  description: "A specialized agent with access to Google Maps APIs that can: geocode addresses to coordinates and vice versa, search for and get details about places, calculate travel distances and times between multiple locations, provide elevation data, and generate navigation directions between points.",
12
+ prompt: "You are a precise geospatial assistant with expert knowledge of Google Maps APIs. Answer user queries by combining place search, geocoding, distance matrix, elevation, and directions. Provide concise, actionable answers, include travel mode assumptions, and cite uncertainties. When location is ambiguous, ask a brief clarifying question before proceeding.",
12
13
  signature: 'userQuery:string "a question to be answered" -> answer:string "the answer to the question"',
13
14
  provider: "anthropic",
14
15
  providerKeyName: "ANTHROPIC_API_KEY",
15
16
  ai: {
16
17
  model: "claude-3-5-sonnet-latest",
17
18
  temperature: 0,
19
+ maxTokens: 1000,
20
+ stream: true
18
21
  },
19
22
  options: {
20
- debug: true,
23
+ debug: true
21
24
  },
22
25
  "mcpServers": {
23
26
  "google-maps": {
@@ -35,6 +38,7 @@ const config = {
35
38
  {
36
39
  name: "ManagerAgent",
37
40
  description: "Completes a user specified task",
41
+ prompt: "You are a manager agent that orchestrates tools and sub-agents. Read the user's objective, optionally produce a short plan, then call the MapsAgent when geospatial knowledge is needed. Keep answers direct and avoid extraneous commentary.",
38
42
  signature:
39
43
  'question:string "a question to be answered" -> answer:string "the answer to the question"',
40
44
  provider: "openai",
@@ -43,6 +47,7 @@ const config = {
43
47
  model: "gpt-4o-mini",
44
48
  maxTokens: 1000,
45
49
  temperature: 0,
50
+ stream: true
46
51
  },
47
52
  options: {
48
53
  debug: true,
@@ -59,17 +64,17 @@ const config = {
59
64
  ai: {
60
65
  model: "gemini-1.5-pro",
61
66
  temperature: 0,
67
+ stream: true
62
68
  },
63
69
  options: {
64
70
  debug: false,
65
- codeExecution: true,
66
71
  },
67
72
  },
68
73
  ],
69
74
  };
70
75
 
71
76
  // Create a new instance of AxCrew with the config
72
- const crew = new AxCrew(config);
77
+ const crew = new AxCrew(config as AxCrewConfig);
73
78
 
74
79
  // Add the agents to the crew
75
80
  await crew.addAllAgents();
@@ -89,11 +94,11 @@ const main = async (): Promise<void> => {
89
94
 
90
95
  console.log(`\nAnswer: ${JSON.stringify(managerResponse?.answer, null, 2)}`);
91
96
 
92
- // Print usage costs
93
- console.log("\nUsage:\n+++++++++++++++++++++++++++++++++");
94
- console.log("Manager Agent Cost in $:", JSON.stringify(managerAgent?.getAccumulatedCosts()?.totalCost, null, 2));
95
- console.log("Maps Agent Cost in $:", JSON.stringify(mapsAgent?.getAccumulatedCosts()?.totalCost, null, 2));
96
- console.log("Total Cost in $:", JSON.stringify(crew.getAggregatedCosts()?.totalCost, null, 2));
97
+ // Print metrics
98
+ console.log("\nMetrics:\n+++++++++++++++++++++++++++++++++");
99
+ console.log("Manager Agent Metrics:", JSON.stringify((managerAgent as any)?.getMetrics?.(), null, 2));
100
+ console.log("Maps Agent Metrics:", JSON.stringify((mapsAgent as any)?.getMetrics?.(), null, 2));
101
+ console.log("Crew Metrics:", JSON.stringify((crew as any)?.getCrewMetrics?.(), null, 2));
97
102
  };
98
103
 
99
104
  main()
@@ -1,7 +1,8 @@
1
1
  import { AxCrew } from "../dist/index.js";
2
+ import type { AxCrewConfig } from "../src/index.js";
2
3
 
3
4
  // Define the crew configuration
4
- const config = {
5
+ const config: AxCrewConfig = {
5
6
  crew: [
6
7
  {
7
8
  name: "MathAgent",
@@ -62,10 +63,11 @@ const main = async (): Promise<void> => {
62
63
 
63
64
  console.log(`\nAnswer: ${JSON.stringify(managerResponse.answer, null, 2)}`);
64
65
 
65
- // Print usage costs
66
- console.log("\nUsage:\n+++++++++++++++++++++++++++++++++");
67
- console.log("Manager Agent:", JSON.stringify(managerAgent.getAccumulatedCosts(), null, 2));
68
- console.log("Total Cost:", JSON.stringify(crew.getAggregatedCosts(), null, 2));
66
+ // Print metrics
67
+ console.log("\nMetrics:\n+++++++++++++++++++++++++++++++++");
68
+ console.log("Manager Agent Metrics:", JSON.stringify((managerAgent as any).getMetrics?.(), null, 2));
69
+ console.log("Math Agent Metrics:", JSON.stringify((mathAgent as any).getMetrics?.(), null, 2));
70
+ console.log("Crew Metrics:", JSON.stringify((crew as any).getCrewMetrics?.(), null, 2));
69
71
  }
70
72
  };
71
73
 
@@ -1,10 +1,11 @@
1
1
  import { AxCrew } from "../dist/index.js";
2
+ import type { AxCrewConfig } from "../src/index.js";
2
3
 
3
4
  import dotenv from "dotenv";
4
5
  dotenv.config();
5
6
 
6
7
  // Define the crew configuration
7
- const config = {
8
+ const config: AxCrewConfig = {
8
9
  crew: [
9
10
  {
10
11
  name: "ManagerAgent",
@@ -75,11 +76,11 @@ const main = async (): Promise<void> => {
75
76
  }
76
77
  }
77
78
 
78
- // Print usage costs
79
- console.log("\nUsage:\n+++++++++++++++++++++++++++++++++");
80
- console.log("Manager Agent Cost in $:", JSON.stringify(managerAgent?.getAccumulatedCosts()?.totalCost, null, 2));
81
- console.log("Math Agent Cost in $:", JSON.stringify(mathAgent?.getAccumulatedCosts()?.totalCost, null, 2));
82
- console.log("Total Cost in $:", JSON.stringify(crew.getAggregatedCosts()?.totalCost, null, 2));
79
+ // Print metrics
80
+ console.log("\nMetrics:\n+++++++++++++++++++++++++++++++++");
81
+ console.log("Manager Agent Metrics:", JSON.stringify((managerAgent as any)?.getMetrics?.(), null, 2));
82
+ console.log("Math Agent Metrics:", JSON.stringify((mathAgent as any)?.getMetrics?.(), null, 2));
83
+ console.log("Crew Metrics:", JSON.stringify((crew as any)?.getCrewMetrics?.(), null, 2));
83
84
  };
84
85
 
85
86
  main()
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@amitdeshmukh/ax-crew",
4
- "version": "3.11.3",
4
+ "version": "4.1.1",
5
5
  "description": "Build and launch a crew of AI agents with shared state. Built with axllm.dev",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
@@ -17,17 +17,19 @@
17
17
  "test:ui": "vitest --ui"
18
18
  },
19
19
  "dependencies": {
20
+ "big.js": "^7.0.1",
20
21
  "decimal.js": "^10.5.0",
21
22
  "dotenv": "^16.4.5",
22
23
  "upgrade": "^1.1.0",
23
24
  "uuid": "^10.0.0"
24
25
  },
25
26
  "peerDependencies": {
26
- "@ax-llm/ax": "^13.0.8",
27
- "@ax-llm/ax-tools": "^13.0.8"
27
+ "@ax-llm/ax": "14.0.16",
28
+ "@ax-llm/ax-tools": "14.0.16"
28
29
  },
29
30
  "devDependencies": {
30
31
  "@testing-library/jest-dom": "^6.6.3",
32
+ "@types/big.js": "^6.2.2",
31
33
  "@types/node": "^20.14.9",
32
34
  "@types/uuid": "^10.0.0",
33
35
  "@vitest/coverage-v8": "^3.0.9",
@@ -1,10 +1,8 @@
1
1
  import fs from 'fs';
2
- // Import all the providers
3
- import { AxAIAnthropic, AxAIOpenAI, AxAIAzureOpenAI, AxAICohere, AxAIDeepSeek, AxAIGoogleGemini, AxAIGroq, AxAIHuggingFace, AxAIMistral, AxAIOllama, AxAITogether, AxAIReka, AxAIGrok } from '@ax-llm/ax';
4
- // Import Ax types
2
+ // Import Ax factory and MCP transports (as exported by current package)
3
+ import { ai, AxMCPClient, AxMCPHTTPSSETransport, AxMCPStreambleHTTPTransport, AxDefaultCostTracker } from '@ax-llm/ax'
5
4
  import type { AxFunction } from '@ax-llm/ax';
6
- // Import the MCP client and transports
7
- import { AxMCPClient, AxMCPHTTPSSETransport, AxMCPStreambleHTTPTransport } from '@ax-llm/ax'
5
+ // STDIO transport from tools package
8
6
  import { AxMCPStdioTransport } from '@ax-llm/ax-tools'
9
7
  import { PROVIDER_API_KEYS } from '../config/index.js';
10
8
  import type {
@@ -14,28 +12,29 @@ import type {
14
12
  MCPTransportConfig,
15
13
  MCPStdioTransportConfig,
16
14
  MCPHTTPSSETransportConfig,
17
- MCPStreambleHTTPTransportConfig
15
+ MCPStreamableHTTPTransportConfig
18
16
  } from '../types.js';
17
+ import type { Provider } from '../types.js';
19
18
 
20
- // Define a mapping from provider names to their respective constructors
21
- const AIConstructors: Record<string, any> = {
22
- 'anthropic': AxAIAnthropic,
23
- 'azure-openai': AxAIAzureOpenAI,
24
- 'cohere': AxAICohere,
25
- 'deepseek': AxAIDeepSeek,
26
- 'google-gemini': AxAIGoogleGemini,
27
- 'groq': AxAIGroq,
28
- 'huggingFace': AxAIHuggingFace,
29
- 'mistral': AxAIMistral,
30
- 'ollama': AxAIOllama,
31
- 'openai': AxAIOpenAI,
32
- 'together': AxAITogether,
33
- 'reka': AxAIReka,
34
- 'grok': AxAIGrok
35
- };
19
+ // Canonical provider slugs supported by ai() factory
20
+ const PROVIDER_CANONICAL = new Set([
21
+ 'openai',
22
+ 'anthropic',
23
+ 'google-gemini',
24
+ 'mistral',
25
+ 'groq',
26
+ 'cohere',
27
+ 'together',
28
+ 'deepseek',
29
+ 'ollama',
30
+ 'huggingface',
31
+ 'openrouter',
32
+ 'azure-openai',
33
+ 'reka',
34
+ 'x-grok'
35
+ ]);
36
36
 
37
- // Provider type
38
- export type Provider = keyof typeof AIConstructors;
37
+ // Provider type lives in src/types.ts
39
38
 
40
39
  // Type guard to check if config is stdio transport
41
40
  export function isStdioTransport(config: MCPTransportConfig): config is MCPStdioTransportConfig {
@@ -48,7 +47,7 @@ export function isHTTPSSETransport(config: MCPTransportConfig): config is MCPHTT
48
47
  }
49
48
 
50
49
  // Type guard to check if config is streamable HTTP transport
51
- export function isStreambleHTTPTransport(config: MCPTransportConfig): config is MCPStreambleHTTPTransportConfig {
50
+ export function isStreambleHTTPTransport(config: MCPTransportConfig): config is MCPStreamableHTTPTransportConfig {
52
51
  return 'mcpEndpoint' in config;
53
52
  }
54
53
 
@@ -205,11 +204,12 @@ const parseAgentConfig = async (
205
204
  throw new Error(`AI agent with name ${agentName} is not configured`);
206
205
  }
207
206
 
208
- // Get the constructor for the AI agent's provider
209
- const AIConstructor = AIConstructors[agentConfigData.provider];
210
- if (!AIConstructor) {
211
- throw new Error(`AI provider ${agentConfigData.provider} is not supported. Did you mean '${agentConfigData.provider.toLowerCase()}'?`);
207
+ // Enforce canonical provider slug
208
+ const lower = agentConfigData.provider.toLowerCase();
209
+ if (!PROVIDER_CANONICAL.has(lower)) {
210
+ throw new Error(`AI provider ${agentConfigData.provider} is not supported. Use one of: ${Array.from(PROVIDER_CANONICAL).join(', ')}`);
212
211
  }
212
+ const provider = lower as Provider;
213
213
 
214
214
  // If an API Key property is present, get the API key for the AI agent from the environment variables
215
215
  let apiKey = '';
@@ -223,25 +223,30 @@ const parseAgentConfig = async (
223
223
  throw new Error(`Provider key name is missing in the agent configuration`);
224
224
  }
225
225
 
226
- // Create an instance of the AI agent and set options
227
- const ai = new AIConstructor({
226
+ // Create a cost tracker instance and pass to ai()
227
+ const costTracker = new AxDefaultCostTracker(((agentConfigData as any).options?.costTracking) || undefined);
228
+
229
+ // Create an instance of the AI agent via factory
230
+ const aiArgs: any = {
231
+ name: provider,
228
232
  apiKey,
229
233
  config: agentConfigData.ai,
230
234
  options: {
231
235
  debug: agentConfigData.debug || false,
232
- ...agentConfigData.options
236
+ ...agentConfigData.options,
237
+ // Attach default cost tracker so usage/costs are recorded by provider layer
238
+ trackers: [costTracker]
233
239
  }
234
- });
235
- // If an apiURL is provided in the agent config, set it in the AI agent
240
+ };
236
241
  if (agentConfigData.apiURL) {
237
242
  try {
238
- // Validate apiURL format
239
243
  new URL(agentConfigData.apiURL);
240
- ai.setAPIURL(agentConfigData.apiURL);
244
+ aiArgs.apiURL = agentConfigData.apiURL;
241
245
  } catch (error) {
242
246
  throw new Error(`Invalid apiURL provided: ${agentConfigData.apiURL}`);
243
- }
247
+ }
244
248
  }
249
+ const aiInstance = ai(aiArgs);
245
250
 
246
251
  // If an mcpServers config is provided in the agent config, convert to functions
247
252
  const mcpFunctions = await initializeMCPServers(agentConfigData);
@@ -265,13 +270,15 @@ const parseAgentConfig = async (
265
270
 
266
271
  // Return AI instance and Agent parameters
267
272
  return {
268
- ai,
273
+ ai: aiInstance,
269
274
  name: agentName,
270
275
  description: agentConfigData.description,
276
+ definition: (agentConfigData as any).definition ?? (agentConfigData as any).prompt,
271
277
  signature: agentConfigData.signature,
272
278
  functions: agentFunctions,
273
279
  subAgentNames: agentConfigData.agents || [],
274
280
  examples: agentConfigData.examples || [],
281
+ tracker: costTracker,
275
282
  };
276
283
  } catch (error) {
277
284
  if (error instanceof Error) {
@@ -20,13 +20,14 @@ import type {
20
20
 
21
21
  import { createState } from "../state/index.js";
22
22
  import { parseCrewConfig, parseAgentConfig } from "./agentConfig.js";
23
- import { StateFulAxAgentUsage } from "./agentUseCosts.js";
23
+ import { MetricsRegistry } from "../metrics/index.js";
24
24
 
25
25
  // Define the interface for the agent configuration
26
26
  interface ParsedAgentConfig {
27
27
  ai: AxAI;
28
28
  name: string;
29
29
  description: string;
30
+ definition?: string;
30
31
  signature: string | AxSignature;
31
32
  functions: (
32
33
  | AxFunction
@@ -36,6 +37,7 @@ interface ParsedAgentConfig {
36
37
  mcpServers?: Record<string, MCPTransportConfig>;
37
38
  subAgentNames: string[];
38
39
  examples?: Array<Record<string, any>>;
40
+ tracker?: any;
39
41
  }
40
42
 
41
43
  // Extend the AxAgent class from ax-llm
@@ -43,12 +45,21 @@ class StatefulAxAgent extends AxAgent<any, any> {
43
45
  state: StateInstance;
44
46
  axai: any;
45
47
  private agentName: string;
48
+ private costTracker?: any;
49
+ private lastRecordedCostUSD: number = 0;
50
+ private isAxAIService(obj: any): obj is AxAI {
51
+ return !!obj && typeof obj.getName === 'function' && typeof obj.chat === 'function';
52
+ }
53
+ private isAxAIInstance(obj: any): obj is AxAI {
54
+ return !!obj && typeof obj === 'object' && ('defaults' in obj || 'modelInfo' in obj);
55
+ }
46
56
 
47
57
  constructor(
48
58
  ai: AxAI,
49
59
  options: Readonly<{
50
60
  name: string;
51
61
  description: string;
62
+ definition?: string;
52
63
  signature: string | AxSignature;
53
64
  agents?: AxAgentic<any, any>[] | undefined;
54
65
  functions?: (AxFunction | (() => AxFunction))[] | undefined;
@@ -76,52 +87,92 @@ class StatefulAxAgent extends AxAgent<any, any> {
76
87
  }
77
88
 
78
89
  // Function overloads for forward method
79
- async forward(values: Record<string, any>, options?: Readonly<AxProgramForwardOptions>): Promise<Record<string, any>>;
80
- async forward(ai: AxAI, values: Record<string, any>, options?: Readonly<AxProgramForwardOptions>): Promise<Record<string, any>>;
90
+ async forward(values: Record<string, any>, options?: Readonly<AxProgramForwardOptions<any>>): Promise<Record<string, any>>;
91
+ async forward(ai: AxAI, values: Record<string, any>, options?: Readonly<AxProgramForwardOptions<any>>): Promise<Record<string, any>>;
81
92
 
82
93
  // Implementation
83
94
  async forward(
84
95
  first: Record<string, any> | AxAI,
85
- second?: Record<string, any> | Readonly<AxProgramForwardOptions>,
86
- third?: Readonly<AxProgramForwardOptions>
96
+ second?: Record<string, any> | Readonly<AxProgramForwardOptions<any>>,
97
+ third?: Readonly<AxProgramForwardOptions<any>>
87
98
  ): Promise<Record<string, any>> {
88
99
  let result;
89
100
 
101
+ const start = performance.now();
102
+ const crewId = (this.state as any)?.crewId || (this.state.get?.('crewId')) || 'default';
103
+ const labels = { crewId, agent: this.agentName } as any;
104
+
90
105
  // Track costs regardless of whether it's a direct or sub-agent call
91
106
  // This ensures we capture multiple legitimate calls to the same agent
92
- if ('apiURL' in first) {
107
+ if (this.isAxAIService(first)) {
93
108
  // Sub-agent case (called with AI service)
94
109
  result = await super.forward(this.axai, second as Record<string, any>, third);
95
110
  } else {
96
111
  // Direct call case
97
- result = await super.forward(this.axai, first, second as Readonly<AxProgramForwardOptions>);
112
+ result = await super.forward(this.axai, first, second as Readonly<AxProgramForwardOptions<any>>);
98
113
  }
99
114
 
100
- // Track costs after the call
101
- const cost = this.getLastUsageCost();
102
- if (cost) {
103
- StateFulAxAgentUsage.trackCostInState(this.agentName, cost, this.state);
115
+ // Track metrics and costs after the call using built-in usage
116
+ const durationMs = performance.now() - start;
117
+ MetricsRegistry.recordRequest(labels, false, durationMs);
118
+ // Always record tokens from built-in usage array if present
119
+ const builtIn = (this as any).getUsage?.();
120
+ if (Array.isArray(builtIn)) {
121
+ const totals = builtIn.reduce(
122
+ (acc: any, u: any) => {
123
+ const pt = u.tokens?.promptTokens ?? u.promptTokens ?? 0;
124
+ const ct = u.tokens?.completionTokens ?? u.completionTokens ?? 0;
125
+ acc.promptTokens += typeof pt === 'number' ? pt : 0;
126
+ acc.completionTokens += typeof ct === 'number' ? ct : 0;
127
+ // also aggregate per-model to feed Ax tracker
128
+ const model = u.model || (this.axai as any)?.getLastUsedChatModel?.() || (this.axai as any)?.defaults?.model;
129
+ if (model) {
130
+ acc.byModel[model] = (acc.byModel[model] || 0) + (pt + ct);
131
+ }
132
+ return acc;
133
+ },
134
+ { promptTokens: 0, completionTokens: 0, byModel: {} as Record<string, number> }
135
+ );
136
+ MetricsRegistry.recordTokens(labels, {
137
+ promptTokens: totals.promptTokens,
138
+ completionTokens: totals.completionTokens,
139
+ totalTokens: totals.promptTokens + totals.completionTokens,
140
+ });
141
+ // Feed Ax's cost tracker with token totals per model; Ax owns pricing
142
+ const costTracker = (this as any).costTracker;
143
+ try {
144
+ for (const [m, count] of Object.entries(totals.byModel)) {
145
+ costTracker?.trackTokens?.(count, m);
146
+ }
147
+ const totalUSD = Number(costTracker?.getCurrentCost?.() ?? 0);
148
+ if (!Number.isNaN(totalUSD) && totalUSD > 0) {
149
+ MetricsRegistry.recordEstimatedCost(labels, totalUSD);
150
+ }
151
+ } catch {}
104
152
  }
105
153
 
106
154
  return result;
107
155
  }
108
156
 
109
157
  // Add streaming forward method overloads
110
- streamingForward(values: Record<string, any>, options?: Readonly<AxProgramStreamingForwardOptions>): AxGenStreamingOut<any>;
111
- streamingForward(ai: AxAI, values: Record<string, any>, options?: Readonly<AxProgramStreamingForwardOptions>): AxGenStreamingOut<any>;
158
+ streamingForward(values: Record<string, any>, options?: Readonly<AxProgramStreamingForwardOptions<any>>): AxGenStreamingOut<any>;
159
+ streamingForward(ai: AxAI, values: Record<string, any>, options?: Readonly<AxProgramStreamingForwardOptions<any>>): AxGenStreamingOut<any>;
112
160
 
113
161
  // Implementation
114
162
  streamingForward(
115
163
  first: Record<string, any> | AxAI,
116
- second?: Record<string, any> | Readonly<AxProgramStreamingForwardOptions>,
117
- third?: Readonly<AxProgramStreamingForwardOptions>
164
+ second?: Record<string, any> | Readonly<AxProgramStreamingForwardOptions<any>>,
165
+ third?: Readonly<AxProgramStreamingForwardOptions<any>>
118
166
  ): AxGenStreamingOut<any> {
167
+ const start = performance.now();
168
+ const crewId = (this.state as any)?.crewId || (this.state.get?.('crewId')) || 'default';
169
+ const labels = { crewId, agent: this.agentName } as any;
119
170
  let streamingResult: AxGenStreamingOut<any>;
120
171
 
121
- if ('apiURL' in first) {
172
+ if (this.isAxAIService(first)) {
122
173
  streamingResult = super.streamingForward(this.axai, second as Record<string, any>, third);
123
174
  } else {
124
- streamingResult = super.streamingForward(this.axai, first, second as Readonly<AxProgramStreamingForwardOptions>);
175
+ streamingResult = super.streamingForward(this.axai, first, second as Readonly<AxProgramStreamingForwardOptions<any>>);
125
176
  }
126
177
 
127
178
  // Create a new async generator that tracks costs after completion
@@ -131,43 +182,71 @@ class StatefulAxAgent extends AxAgent<any, any> {
131
182
  yield chunk;
132
183
  }
133
184
  } finally {
134
- const cost = this.getLastUsageCost();
135
- if (cost) {
136
- StateFulAxAgentUsage.trackCostInState(this.agentName, cost, this.state);
185
+ const durationMs = performance.now() - start;
186
+ MetricsRegistry.recordRequest(labels, true, durationMs);
187
+ // Record tokens from built-in usage array if present
188
+ const builtIn = (this as any).getUsage?.();
189
+ if (Array.isArray(builtIn)) {
190
+ const totals = builtIn.reduce(
191
+ (acc: any, u: any) => {
192
+ const pt = u.tokens?.promptTokens ?? u.promptTokens ?? 0;
193
+ const ct = u.tokens?.completionTokens ?? u.completionTokens ?? 0;
194
+ acc.promptTokens += typeof pt === 'number' ? pt : 0;
195
+ acc.completionTokens += typeof ct === 'number' ? ct : 0;
196
+ const model = u.model || (this.axai as any)?.getLastUsedChatModel?.() || (this.axai as any)?.defaults?.model;
197
+ if (model) {
198
+ acc.byModel[model] = (acc.byModel[model] || 0) + (pt + ct);
199
+ }
200
+ return acc;
201
+ },
202
+ { promptTokens: 0, completionTokens: 0, byModel: {} as Record<string, number> }
203
+ );
204
+ MetricsRegistry.recordTokens(labels, {
205
+ promptTokens: totals.promptTokens,
206
+ completionTokens: totals.completionTokens,
207
+ totalTokens: totals.promptTokens + totals.completionTokens,
208
+ });
209
+ const costTracker = (this as any).costTracker;
210
+ try {
211
+ for (const [m, count] of Object.entries(totals.byModel)) {
212
+ costTracker?.trackTokens?.(count, m);
213
+ }
214
+ const totalUSD = Number(costTracker?.getCurrentCost?.() ?? 0);
215
+ if (!Number.isNaN(totalUSD) && totalUSD > 0) {
216
+ MetricsRegistry.recordEstimatedCost(labels, totalUSD);
217
+ }
218
+ } catch {}
137
219
  }
220
+ // Record estimated cost (USD) via attached tracker if available
221
+ const costTracker = (this as any).costTracker;
222
+ try {
223
+ const totalUSD = Number(costTracker?.getCurrentCost?.() ?? 0);
224
+ if (!Number.isNaN(totalUSD) && totalUSD > 0) {
225
+ MetricsRegistry.recordEstimatedCost(labels, totalUSD);
226
+ }
227
+ } catch {}
138
228
  }
139
229
  }).bind(this)();
140
230
 
141
231
  return wrappedGenerator as AxGenStreamingOut<any>;
142
232
  }
143
233
 
144
- // Get the usage cost for the most recent run of the agent
145
- getLastUsageCost(): UsageCost | null {
146
- const { modelUsage, modelInfo, defaults } = this.axai;
147
-
148
- // Check if all required properties exist
149
- if (!modelUsage?.promptTokens || !modelUsage?.completionTokens) {
150
- return null;
151
- }
234
+ // Legacy cost API removed: rely on Ax trackers for cost reporting
235
+ getLastUsageCost(): UsageCost | null { return null; }
152
236
 
153
- if (!modelInfo || !defaults?.model) {
154
- return null;
155
- }
156
-
157
- const currentModelInfo = modelInfo.find((m: { name: string }) => m.name === defaults.model);
158
-
159
- if (!currentModelInfo?.promptTokenCostPer1M || !currentModelInfo?.completionTokenCostPer1M) {
160
- return null;
161
- }
237
+ // Get the accumulated costs for all runs of this agent
238
+ getAccumulatedCosts(): UsageCost | null { return null; }
162
239
 
163
- return StateFulAxAgentUsage.calculateCost(modelUsage, currentModelInfo);
240
+ // Metrics API for this agent
241
+ getMetrics() {
242
+ const crewId = (this.state as any)?.crewId || (this.state.get?.('crewId')) || 'default';
243
+ return MetricsRegistry.snapshot({ crewId, agent: this.agentName } as any);
164
244
  }
165
-
166
- // Get the accumulated costs for all runs of this agent
167
- getAccumulatedCosts(): UsageCost | null {
168
- const stateKey = `${StateFulAxAgentUsage.STATE_KEY_PREFIX}${this.agentName}`;
169
- return this.state.get(stateKey) as UsageCost | null;
245
+ resetMetrics(): void {
246
+ const crewId = (this.state as any)?.crewId || (this.state.get?.('crewId')) || 'default';
247
+ MetricsRegistry.reset({ crewId, agent: this.agentName } as any);
170
248
  }
249
+
171
250
  }
172
251
 
173
252
  /**
@@ -208,6 +287,8 @@ class AxCrew {
208
287
  this.crewId = crewId;
209
288
  this.agents = new Map<string, StatefulAxAgent>();
210
289
  this.state = createState(crewId);
290
+ // Make crewId discoverable to metrics
291
+ this.state.set('crewId', crewId);
211
292
  }
212
293
 
213
294
  /**
@@ -226,7 +307,7 @@ class AxCrew {
226
307
  );
227
308
 
228
309
  // Destructure with type assertion
229
- const { ai, name, description, signature, functions, subAgentNames, examples } = agentConfig;
310
+ const { ai, name, description, signature, functions, subAgentNames, examples, tracker } = agentConfig;
230
311
 
231
312
  // Get subagents for the AI agent
232
313
  const subAgents = subAgentNames.map((subAgentName: string) => {
@@ -238,23 +319,45 @@ class AxCrew {
238
319
  return this.agents?.get(subAgentName);
239
320
  });
240
321
 
322
+ // Dedupe sub-agents by name (defensive)
323
+ const subAgentSet = new Map<string, StatefulAxAgent>();
324
+ for (const sa of subAgents.filter((agent): agent is StatefulAxAgent => agent !== undefined)) {
325
+ const n = (sa as any)?.agentName ?? (sa as any)?.name ?? '';
326
+ if (!subAgentSet.has(n)) subAgentSet.set(n, sa);
327
+ }
328
+ const uniqueSubAgents = Array.from(subAgentSet.values());
329
+
330
+ // Dedupe functions by name and avoid collision with sub-agent names
331
+ const subAgentNameSet = new Set(uniqueSubAgents.map((sa: any) => sa?.agentName ?? sa?.name).filter(Boolean));
332
+ const uniqueFunctions: AxFunction[] = [];
333
+ const seenFn = new Set<string>();
334
+ for (const fn of functions.filter((fn): fn is AxFunction => fn !== undefined)) {
335
+ const fnName = fn.name;
336
+ if (subAgentNameSet.has(fnName)) {
337
+ // Skip function that collides with a sub-agent name
338
+ continue;
339
+ }
340
+ if (!seenFn.has(fnName)) {
341
+ seenFn.add(fnName);
342
+ uniqueFunctions.push(fn);
343
+ }
344
+ }
345
+
241
346
  // Create an instance of StatefulAxAgent
242
347
  const agent = new StatefulAxAgent(
243
348
  ai,
244
349
  {
245
350
  name,
246
351
  description,
352
+ definition: (agentConfig as any).definition,
247
353
  signature,
248
- functions: functions.filter(
249
- (fn): fn is AxFunction => fn !== undefined
250
- ),
251
- agents: subAgents.filter(
252
- (agent): agent is StatefulAxAgent => agent !== undefined
253
- ),
354
+ functions: uniqueFunctions,
355
+ agents: uniqueSubAgents,
254
356
  examples,
255
357
  },
256
358
  this.state
257
359
  );
360
+ (agent as any).costTracker = tracker;
258
361
 
259
362
  return agent;
260
363
  } catch (error) {
@@ -394,20 +497,29 @@ class AxCrew {
394
497
  this.state.reset();
395
498
  }
396
499
 
397
- /**
398
- * Gets aggregated costs for all agents in the crew
399
- * @returns Aggregated cost information for all agents
400
- */
401
- getAggregatedCosts(): ReturnType<typeof StateFulAxAgentUsage.getAggregatedCosts> {
402
- return StateFulAxAgentUsage.getAggregatedCosts(this.state);
403
- }
404
500
 
405
501
  /**
406
502
  * Resets all cost tracking for the crew
407
503
  */
408
504
  resetCosts(): void {
409
- StateFulAxAgentUsage.resetCosts(this.state);
505
+ // Reset AxAgent built-in usage and our metrics registry
506
+ if (this.agents) {
507
+ for (const [, agent] of this.agents) {
508
+ try { (agent as any).resetUsage?.(); } catch {}
509
+ try { (agent as any).resetMetrics?.(); } catch {}
510
+ }
511
+ }
512
+ MetricsRegistry.reset({ crewId: this.crewId });
513
+ }
514
+
515
+ // Metrics API
516
+ getCrewMetrics() {
517
+ return MetricsRegistry.snapshotCrew(this.crewId);
518
+ }
519
+ resetCrewMetrics(): void {
520
+ MetricsRegistry.reset({ crewId: this.crewId });
410
521
  }
411
522
  }
412
523
 
413
- export { AxCrew };
524
+ export { AxCrew };
525
+ export type { StatefulAxAgent };