@amitdeshmukh/ax-crew 3.11.3 → 4.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.
package/CHANGELOG.md CHANGED
@@ -1,4 +1,28 @@
1
1
  # Changelog
2
+ ## [4.0.1] - 2025-08-24
3
+
4
+ ### Added
5
+ - Dependency: `big.js` for precise accumulation of estimated costs.
6
+ - Dev Dependency: `@types/big.js` for TypeScript support.
7
+
8
+ ### Changed
9
+ - Switched crew cost reporting to metrics-based API. Use `agent.getMetrics()` and `crew.getCrewMetrics()` instead of legacy cost getters.
10
+ - Estimated cost (`estimatedCostUSD`) is now accumulated with Big.js and reported rounded to 5 decimal places.
11
+ - Tests updated to use metrics assertions; added dummy `ANTHROPIC_API_KEY` in test setup to avoid env failures.
12
+ - Test agent signatures updated to new DSL (`query:string -> queryResponse:string`).
13
+ - Aligned with new `@ax-llm/ax` `ai()` and `agent()` factory methods: enforce canonical provider slugs, pass cost tracker via `options.trackers`, and validate `apiURL` when provided.
14
+ - Updated package versions for `@ax-llm/ax` and `@ax-llm/ax-tools`.
15
+
16
+ ### Deprecated
17
+ - Legacy cost APIs (`getLastUsageCost`, `getAccumulatedCosts`, `getAggregatedCosts`) are deprecated in favor of metrics.
18
+
19
+ ### Removed
20
+ - Tests no longer use `getAggregatedCosts`; replaced with metrics assertions.
21
+
22
+ ### Updated
23
+ - README now documents metrics usage and 5-decimal cost rounding, de-emphasizing legacy cost APIs.
24
+ - README Features now includes "Metrics and Cost Tracking"; examples updated to use metrics (`getMetrics`, `getCrewMetrics`).
25
+
2
26
 
3
27
  This Changelog format is based on [Keep a Changelog]
4
28
  (https://keepachangelog.com/en/1.0.0/), and this project
package/README.md CHANGED
@@ -10,6 +10,7 @@ This repo simplifies development of [AxLLM](https://axllm.dev) AI Agents by usin
10
10
  - **Task Execution**: Plan and execute tasks using agents in the crew.
11
11
  - **Streaming Support**: Stream agent responses in real-time for better user experience and faster feedback.
12
12
  - **Model Context Protocol (MCP)**: Support for MCP to allow agents to use MCP servers.
13
+ - **Metrics and Cost Tracking**: Built-in per-agent and per-crew metrics, including token usage and estimated USD costs (5-decimal rounding).
13
14
 
14
15
  ## Getting Started
15
16
 
@@ -92,12 +93,11 @@ if (planner) {
92
93
  // Sub-agent usage - when used by another agent (AI is ignored and agent's own config is used)
93
94
  const subAgentResponse = await planner.forward(ai, { task: "Plan something" });
94
95
 
95
- const cost = planner.getUsageCost();
96
-
97
- if (cost) {
98
- console.log(`Total cost: $${cost.totalCost}`);
99
- console.log(`Total tokens: ${cost.tokenMetrics.totalTokens}`);
100
- }
96
+ // Metrics (per-agent and per-crew)
97
+ const agentMetrics = (planner as any).getMetrics?.();
98
+ const crewMetrics = crew.getCrewMetrics();
99
+ console.log('Agent metrics:', agentMetrics);
100
+ console.log('Crew metrics:', crewMetrics);
101
101
  }
102
102
  ```
103
103
 
@@ -514,7 +514,7 @@ For MCP servers that support streamable HTTP communication:
514
514
  "mcpEndpoint": "http://localhost:3002/stream",
515
515
  "options": {
516
516
  "authorization": "Bearer ey.JhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIn0..-1234567890.1234567890",
517
- "headers": { // Custom headers to include with all HTTP requests Note: Content-Type, Accept, and Mcp-Session-Id are managed automatically
517
+ "headers": {
518
518
  "X-Custom-Header": "custom-value"
519
519
  }
520
520
  }
@@ -667,76 +667,45 @@ For new streamable HTTP servers, use:
667
667
 
668
668
  ### Tracking Usage Costs
669
669
 
670
- The package provides precise cost tracking capabilities for monitoring API usage across individual agents and the entire crew. Costs are calculated using high-precision decimal arithmetic to ensure accuracy.
670
+ The package provides metrics for monitoring API usage across individual agents and the entire crew. Estimated costs are accumulated precisely and rounded to 5 decimal places for reporting.
671
671
 
672
672
  ```javascript
673
673
  // After running an agent's forward method
674
- const response = await Planner.forward({ task: userQuery });
675
-
676
- // Get individual agent costs
677
- const agentCost = Planner.getLastUsageCost();
678
- console.log(agentCost);
679
- /* Output example:
680
- {
681
- promptCost: "0.0003637500000",
682
- completionCost: "0.0006100000000",
683
- totalCost: "0.0009737500000",
684
- tokenMetrics: {
685
- promptTokens: 291,
686
- completionTokens: 122,
687
- totalTokens: 413
688
- }
689
- }
690
- */
674
+ await Planner.forward({ task: userQuery });
691
675
 
692
- // Get cumulative costs for the agent
693
- const cumulativeCost = Planner.getAccumulatedCosts();
694
- console.log(cumulativeCost);
695
- /* Output example:
676
+ // Per-agent metrics
677
+ const agentMetrics = (Planner as any).getMetrics?.();
678
+ console.log(agentMetrics);
679
+ /* Example:
696
680
  {
697
- promptCost: "0.0003637500000",
698
- completionCost: "0.0006100000000",
699
- totalCost: "0.0009737500000",
700
- tokenMetrics: {
701
- promptTokens: 291,
702
- completionTokens: 122,
703
- totalTokens: 413
704
- }
681
+ provider: "anthropic",
682
+ model: "claude-3-haiku-20240307",
683
+ requests: { totalRequests, totalErrors, errorRate, totalStreamingRequests, durationMsSum, durationCount },
684
+ tokens: { promptTokens, completionTokens, totalTokens },
685
+ estimatedCostUSD: 0.00091, // rounded to 5 decimals
686
+ functions: { totalFunctionCalls, totalFunctionLatencyMs }
705
687
  }
706
688
  */
707
689
 
708
- // Get aggregated costs for all agents in the crew
709
- const crewCosts = crew.getAggregatedCosts();
710
- console.log(crewCosts);
711
- /* Output example:
690
+ // Crew metrics
691
+ const crewMetrics = crew.getCrewMetrics();
692
+ console.log(crewMetrics);
693
+ /* Example:
712
694
  {
713
- totalCost: "0.0025482500000",
714
- byAgent: {
715
- "Planner": { ... },
716
- "Calculator": { ... },
717
- "Manager": { ... }
718
- },
719
- aggregatedMetrics: {
720
- promptTokens: 850,
721
- completionTokens: 324,
722
- totalTokens: 1174,
723
- promptCost: "0.0010625000000",
724
- completionCost: "0.0014857500000"
725
- }
695
+ requests: { ... },
696
+ tokens: { promptTokens, completionTokens, totalTokens },
697
+ estimatedCostUSD: 0.00255, // rounded to 5 decimals
698
+ functions: { ... }
726
699
  }
727
700
  */
728
701
 
729
- // Reset cost tracking if needed
702
+ // Reset tracked metrics
730
703
  crew.resetCosts();
731
704
  ```
732
705
 
733
- Cost tracking features:
734
- - High-precision decimal calculations using decimal.js
735
- - Per-agent cost breakdown
736
- - Aggregated crew-wide metrics
737
- - Token usage statistics
738
- - Support for different pricing tiers per model
739
- - Persistent cost tracking across multiple agent runs
706
+ Notes:
707
+ - Legacy cost APIs (`getLastUsageCost`, `getAccumulatedCosts`, `getAggregatedCosts`) are superseded by metrics methods.
708
+ - Estimated cost values are numbers rounded to 5 decimal places.
740
709
 
741
710
  ## Changelog
742
711
 
@@ -1,10 +1,9 @@
1
+ import { AxDefaultCostTracker } from '@ax-llm/ax';
1
2
  import type { AxFunction } from '@ax-llm/ax';
2
- import type { AgentConfig, CrewConfigInput, FunctionRegistryType, MCPTransportConfig, MCPStdioTransportConfig, MCPHTTPSSETransportConfig, MCPStreambleHTTPTransportConfig } from '../types.js';
3
- declare const AIConstructors: Record<string, any>;
4
- export type Provider = keyof typeof AIConstructors;
3
+ import type { AgentConfig, CrewConfigInput, FunctionRegistryType, MCPTransportConfig, MCPStdioTransportConfig, MCPHTTPSSETransportConfig, MCPStreamableHTTPTransportConfig } from '../types.js';
5
4
  export declare function isStdioTransport(config: MCPTransportConfig): config is MCPStdioTransportConfig;
6
5
  export declare function isHTTPSSETransport(config: MCPTransportConfig): config is MCPHTTPSSETransportConfig;
7
- export declare function isStreambleHTTPTransport(config: MCPTransportConfig): config is MCPStreambleHTTPTransportConfig;
6
+ export declare function isStreambleHTTPTransport(config: MCPTransportConfig): config is MCPStreamableHTTPTransportConfig;
8
7
  /**
9
8
  * Parses and returns the AxCrew config from either a JSON config file or a direct JSON object.
10
9
  * @param {CrewConfigInput} input - Either a path to the agent config json file or a JSON object with crew configuration.
@@ -27,12 +26,13 @@ declare const parseCrewConfig: (input: CrewConfigInput) => {
27
26
  * the API key is not found, or the provider key name is not specified in the configuration.
28
27
  */
29
28
  declare const parseAgentConfig: (agentName: string, crewConfig: CrewConfigInput, functions: FunctionRegistryType, state: Record<string, any>) => Promise<{
30
- ai: any;
29
+ ai: import("@ax-llm/ax").AxAI<string>;
31
30
  name: string;
32
31
  description: string;
33
- signature: string | import("@ax-llm/ax").AxSignature;
32
+ signature: string | import("@ax-llm/ax").AxSignature<Record<string, any>, Record<string, any>>;
34
33
  functions: AxFunction[];
35
34
  subAgentNames: string[];
36
35
  examples: Record<string, any>[];
36
+ tracker: AxDefaultCostTracker;
37
37
  }>;
38
38
  export { parseAgentConfig, parseCrewConfig };
@@ -1,26 +1,27 @@
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 the MCP client and transports
5
- import { AxMCPClient, AxMCPHTTPSSETransport, AxMCPStreambleHTTPTransport } from '@ax-llm/ax';
2
+ // Import Ax factory and MCP transports (as exported by current package)
3
+ import { ai, AxMCPClient, AxMCPHTTPSSETransport, AxMCPStreambleHTTPTransport, AxDefaultCostTracker } from '@ax-llm/ax';
4
+ // STDIO transport from tools package
6
5
  import { AxMCPStdioTransport } from '@ax-llm/ax-tools';
7
6
  import { PROVIDER_API_KEYS } from '../config/index.js';
8
- // Define a mapping from provider names to their respective constructors
9
- const AIConstructors = {
10
- 'anthropic': AxAIAnthropic,
11
- 'azure-openai': AxAIAzureOpenAI,
12
- 'cohere': AxAICohere,
13
- 'deepseek': AxAIDeepSeek,
14
- 'google-gemini': AxAIGoogleGemini,
15
- 'groq': AxAIGroq,
16
- 'huggingFace': AxAIHuggingFace,
17
- 'mistral': AxAIMistral,
18
- 'ollama': AxAIOllama,
19
- 'openai': AxAIOpenAI,
20
- 'together': AxAITogether,
21
- 'reka': AxAIReka,
22
- 'grok': AxAIGrok
23
- };
7
+ // Canonical provider slugs supported by ai() factory
8
+ const PROVIDER_CANONICAL = new Set([
9
+ 'openai',
10
+ 'anthropic',
11
+ 'google-gemini',
12
+ 'mistral',
13
+ 'groq',
14
+ 'cohere',
15
+ 'together',
16
+ 'deepseek',
17
+ 'ollama',
18
+ 'huggingface',
19
+ 'openrouter',
20
+ 'azure-openai',
21
+ 'reka',
22
+ 'x-grok'
23
+ ]);
24
+ // Provider type lives in src/types.ts
24
25
  // Type guard to check if config is stdio transport
25
26
  export function isStdioTransport(config) {
26
27
  return 'command' in config;
@@ -173,11 +174,12 @@ const parseAgentConfig = async (agentName, crewConfig, functions, state) => {
173
174
  if (!agentConfigData) {
174
175
  throw new Error(`AI agent with name ${agentName} is not configured`);
175
176
  }
176
- // Get the constructor for the AI agent's provider
177
- const AIConstructor = AIConstructors[agentConfigData.provider];
178
- if (!AIConstructor) {
179
- throw new Error(`AI provider ${agentConfigData.provider} is not supported. Did you mean '${agentConfigData.provider.toLowerCase()}'?`);
177
+ // Enforce canonical provider slug
178
+ const lower = agentConfigData.provider.toLowerCase();
179
+ if (!PROVIDER_CANONICAL.has(lower)) {
180
+ throw new Error(`AI provider ${agentConfigData.provider} is not supported. Use one of: ${Array.from(PROVIDER_CANONICAL).join(', ')}`);
180
181
  }
182
+ const provider = lower;
181
183
  // If an API Key property is present, get the API key for the AI agent from the environment variables
182
184
  let apiKey = '';
183
185
  if (agentConfigData.providerKeyName) {
@@ -189,26 +191,30 @@ const parseAgentConfig = async (agentName, crewConfig, functions, state) => {
189
191
  else {
190
192
  throw new Error(`Provider key name is missing in the agent configuration`);
191
193
  }
192
- // Create an instance of the AI agent and set options
193
- const ai = new AIConstructor({
194
+ // Create a cost tracker instance and pass to ai()
195
+ const costTracker = new AxDefaultCostTracker();
196
+ // Create an instance of the AI agent via factory
197
+ const aiArgs = {
198
+ name: provider,
194
199
  apiKey,
195
200
  config: agentConfigData.ai,
196
201
  options: {
197
202
  debug: agentConfigData.debug || false,
198
- ...agentConfigData.options
203
+ ...agentConfigData.options,
204
+ // Attach default cost tracker so usage/costs are recorded by provider layer
205
+ trackers: [costTracker]
199
206
  }
200
- });
201
- // If an apiURL is provided in the agent config, set it in the AI agent
207
+ };
202
208
  if (agentConfigData.apiURL) {
203
209
  try {
204
- // Validate apiURL format
205
210
  new URL(agentConfigData.apiURL);
206
- ai.setAPIURL(agentConfigData.apiURL);
211
+ aiArgs.apiURL = agentConfigData.apiURL;
207
212
  }
208
213
  catch (error) {
209
214
  throw new Error(`Invalid apiURL provided: ${agentConfigData.apiURL}`);
210
215
  }
211
216
  }
217
+ const aiInstance = ai(aiArgs);
212
218
  // If an mcpServers config is provided in the agent config, convert to functions
213
219
  const mcpFunctions = await initializeMCPServers(agentConfigData);
214
220
  // Prepare functions for the AI agent
@@ -229,13 +235,14 @@ const parseAgentConfig = async (agentName, crewConfig, functions, state) => {
229
235
  ];
230
236
  // Return AI instance and Agent parameters
231
237
  return {
232
- ai,
238
+ ai: aiInstance,
233
239
  name: agentName,
234
240
  description: agentConfigData.description,
235
241
  signature: agentConfigData.signature,
236
242
  functions: agentFunctions,
237
243
  subAgentNames: agentConfigData.agents || [],
238
244
  examples: agentConfigData.examples || [],
245
+ tracker: costTracker,
239
246
  };
240
247
  }
241
248
  catch (error) {
@@ -1,11 +1,14 @@
1
1
  import { AxAgent, AxAI } from "@ax-llm/ax";
2
2
  import type { AxSignature, AxAgentic, AxFunction, AxProgramForwardOptions, AxProgramStreamingForwardOptions, AxGenStreamingOut } from "@ax-llm/ax";
3
3
  import type { StateInstance, FunctionRegistryType, UsageCost, CrewConfigInput, MCPTransportConfig } from "../types.js";
4
- import { StateFulAxAgentUsage } from "./agentUseCosts.js";
5
4
  declare class StatefulAxAgent extends AxAgent<any, any> {
6
5
  state: StateInstance;
7
6
  axai: any;
8
7
  private agentName;
8
+ private costTracker?;
9
+ private lastRecordedCostUSD;
10
+ private isAxAIService;
11
+ private isAxAIInstance;
9
12
  constructor(ai: AxAI, options: Readonly<{
10
13
  name: string;
11
14
  description: string;
@@ -15,12 +18,14 @@ declare class StatefulAxAgent extends AxAgent<any, any> {
15
18
  examples?: Array<Record<string, any>> | undefined;
16
19
  mcpServers?: Record<string, MCPTransportConfig> | undefined;
17
20
  }>, state: StateInstance);
18
- forward(values: Record<string, any>, options?: Readonly<AxProgramForwardOptions>): Promise<Record<string, any>>;
19
- forward(ai: AxAI, values: Record<string, any>, options?: Readonly<AxProgramForwardOptions>): Promise<Record<string, any>>;
20
- streamingForward(values: Record<string, any>, options?: Readonly<AxProgramStreamingForwardOptions>): AxGenStreamingOut<any>;
21
- streamingForward(ai: AxAI, values: Record<string, any>, options?: Readonly<AxProgramStreamingForwardOptions>): AxGenStreamingOut<any>;
21
+ forward(values: Record<string, any>, options?: Readonly<AxProgramForwardOptions<any>>): Promise<Record<string, any>>;
22
+ forward(ai: AxAI, values: Record<string, any>, options?: Readonly<AxProgramForwardOptions<any>>): Promise<Record<string, any>>;
23
+ streamingForward(values: Record<string, any>, options?: Readonly<AxProgramStreamingForwardOptions<any>>): AxGenStreamingOut<any>;
24
+ streamingForward(ai: AxAI, values: Record<string, any>, options?: Readonly<AxProgramStreamingForwardOptions<any>>): AxGenStreamingOut<any>;
22
25
  getLastUsageCost(): UsageCost | null;
23
26
  getAccumulatedCosts(): UsageCost | null;
27
+ getMetrics(): import("../metrics/types.js").MetricsSnapshot;
28
+ resetMetrics(): void;
24
29
  }
25
30
  /**
26
31
  * Represents a crew of agents with shared state functionality.
@@ -63,14 +68,12 @@ declare class AxCrew {
63
68
  * Cleans up the crew by dereferencing agents and resetting the state.
64
69
  */
65
70
  destroy(): void;
66
- /**
67
- * Gets aggregated costs for all agents in the crew
68
- * @returns Aggregated cost information for all agents
69
- */
70
- getAggregatedCosts(): ReturnType<typeof StateFulAxAgentUsage.getAggregatedCosts>;
71
71
  /**
72
72
  * Resets all cost tracking for the crew
73
73
  */
74
74
  resetCosts(): void;
75
+ getCrewMetrics(): import("../metrics/types.js").MetricsSnapshot;
76
+ resetCrewMetrics(): void;
75
77
  }
76
78
  export { AxCrew };
79
+ export type { StatefulAxAgent };
@@ -2,12 +2,20 @@ import { v4 as uuidv4 } from "uuid";
2
2
  import { AxAgent } from "@ax-llm/ax";
3
3
  import { createState } from "../state/index.js";
4
4
  import { parseCrewConfig, parseAgentConfig } from "./agentConfig.js";
5
- import { StateFulAxAgentUsage } from "./agentUseCosts.js";
5
+ import { MetricsRegistry } from "../metrics/index.js";
6
6
  // Extend the AxAgent class from ax-llm
7
7
  class StatefulAxAgent extends AxAgent {
8
8
  state;
9
9
  axai;
10
10
  agentName;
11
+ costTracker;
12
+ lastRecordedCostUSD = 0;
13
+ isAxAIService(obj) {
14
+ return !!obj && typeof obj.getName === 'function' && typeof obj.chat === 'function';
15
+ }
16
+ isAxAIInstance(obj) {
17
+ return !!obj && typeof obj === 'object' && ('defaults' in obj || 'modelInfo' in obj);
18
+ }
11
19
  constructor(ai, options, state) {
12
20
  const { examples, ...restOptions } = options;
13
21
  const formattedOptions = {
@@ -26,9 +34,12 @@ class StatefulAxAgent extends AxAgent {
26
34
  // Implementation
27
35
  async forward(first, second, third) {
28
36
  let result;
37
+ const start = performance.now();
38
+ const crewId = this.state?.crewId || (this.state.get?.('crewId')) || 'default';
39
+ const labels = { crewId, agent: this.agentName };
29
40
  // Track costs regardless of whether it's a direct or sub-agent call
30
41
  // This ensures we capture multiple legitimate calls to the same agent
31
- if ('apiURL' in first) {
42
+ if (this.isAxAIService(first)) {
32
43
  // Sub-agent case (called with AI service)
33
44
  result = await super.forward(this.axai, second, third);
34
45
  }
@@ -36,17 +47,51 @@ class StatefulAxAgent extends AxAgent {
36
47
  // Direct call case
37
48
  result = await super.forward(this.axai, first, second);
38
49
  }
39
- // Track costs after the call
40
- const cost = this.getLastUsageCost();
41
- if (cost) {
42
- StateFulAxAgentUsage.trackCostInState(this.agentName, cost, this.state);
50
+ // Track metrics and costs after the call using built-in usage
51
+ const durationMs = performance.now() - start;
52
+ MetricsRegistry.recordRequest(labels, false, durationMs);
53
+ // Always record tokens from built-in usage array if present
54
+ const builtIn = this.getUsage?.();
55
+ if (Array.isArray(builtIn)) {
56
+ const totals = builtIn.reduce((acc, u) => {
57
+ const pt = u.tokens?.promptTokens ?? u.promptTokens ?? 0;
58
+ const ct = u.tokens?.completionTokens ?? u.completionTokens ?? 0;
59
+ acc.promptTokens += typeof pt === 'number' ? pt : 0;
60
+ acc.completionTokens += typeof ct === 'number' ? ct : 0;
61
+ // also aggregate per-model to feed Ax tracker
62
+ const model = u.model || this.axai?.getLastUsedChatModel?.() || this.axai?.defaults?.model;
63
+ if (model) {
64
+ acc.byModel[model] = (acc.byModel[model] || 0) + (pt + ct);
65
+ }
66
+ return acc;
67
+ }, { promptTokens: 0, completionTokens: 0, byModel: {} });
68
+ MetricsRegistry.recordTokens(labels, {
69
+ promptTokens: totals.promptTokens,
70
+ completionTokens: totals.completionTokens,
71
+ totalTokens: totals.promptTokens + totals.completionTokens,
72
+ });
73
+ // Feed Ax's cost tracker with token totals per model; Ax owns pricing
74
+ const costTracker = this.costTracker;
75
+ try {
76
+ for (const [m, count] of Object.entries(totals.byModel)) {
77
+ costTracker?.trackTokens?.(count, m);
78
+ }
79
+ const totalUSD = Number(costTracker?.getCurrentCost?.() ?? 0);
80
+ if (!Number.isNaN(totalUSD) && totalUSD > 0) {
81
+ MetricsRegistry.recordEstimatedCost(labels, totalUSD);
82
+ }
83
+ }
84
+ catch { }
43
85
  }
44
86
  return result;
45
87
  }
46
88
  // Implementation
47
89
  streamingForward(first, second, third) {
90
+ const start = performance.now();
91
+ const crewId = this.state?.crewId || (this.state.get?.('crewId')) || 'default';
92
+ const labels = { crewId, agent: this.agentName };
48
93
  let streamingResult;
49
- if ('apiURL' in first) {
94
+ if (this.isAxAIService(first)) {
50
95
  streamingResult = super.streamingForward(this.axai, second, third);
51
96
  }
52
97
  else {
@@ -60,34 +105,64 @@ class StatefulAxAgent extends AxAgent {
60
105
  }
61
106
  }
62
107
  finally {
63
- const cost = this.getLastUsageCost();
64
- if (cost) {
65
- StateFulAxAgentUsage.trackCostInState(this.agentName, cost, this.state);
108
+ const durationMs = performance.now() - start;
109
+ MetricsRegistry.recordRequest(labels, true, durationMs);
110
+ // Record tokens from built-in usage array if present
111
+ const builtIn = this.getUsage?.();
112
+ if (Array.isArray(builtIn)) {
113
+ const totals = builtIn.reduce((acc, u) => {
114
+ const pt = u.tokens?.promptTokens ?? u.promptTokens ?? 0;
115
+ const ct = u.tokens?.completionTokens ?? u.completionTokens ?? 0;
116
+ acc.promptTokens += typeof pt === 'number' ? pt : 0;
117
+ acc.completionTokens += typeof ct === 'number' ? ct : 0;
118
+ const model = u.model || this.axai?.getLastUsedChatModel?.() || this.axai?.defaults?.model;
119
+ if (model) {
120
+ acc.byModel[model] = (acc.byModel[model] || 0) + (pt + ct);
121
+ }
122
+ return acc;
123
+ }, { promptTokens: 0, completionTokens: 0, byModel: {} });
124
+ MetricsRegistry.recordTokens(labels, {
125
+ promptTokens: totals.promptTokens,
126
+ completionTokens: totals.completionTokens,
127
+ totalTokens: totals.promptTokens + totals.completionTokens,
128
+ });
129
+ const costTracker = this.costTracker;
130
+ try {
131
+ for (const [m, count] of Object.entries(totals.byModel)) {
132
+ costTracker?.trackTokens?.(count, m);
133
+ }
134
+ const totalUSD = Number(costTracker?.getCurrentCost?.() ?? 0);
135
+ if (!Number.isNaN(totalUSD) && totalUSD > 0) {
136
+ MetricsRegistry.recordEstimatedCost(labels, totalUSD);
137
+ }
138
+ }
139
+ catch { }
140
+ }
141
+ // Record estimated cost (USD) via attached tracker if available
142
+ const costTracker = this.costTracker;
143
+ try {
144
+ const totalUSD = Number(costTracker?.getCurrentCost?.() ?? 0);
145
+ if (!Number.isNaN(totalUSD) && totalUSD > 0) {
146
+ MetricsRegistry.recordEstimatedCost(labels, totalUSD);
147
+ }
66
148
  }
149
+ catch { }
67
150
  }
68
151
  }).bind(this)();
69
152
  return wrappedGenerator;
70
153
  }
71
- // Get the usage cost for the most recent run of the agent
72
- getLastUsageCost() {
73
- const { modelUsage, modelInfo, defaults } = this.axai;
74
- // Check if all required properties exist
75
- if (!modelUsage?.promptTokens || !modelUsage?.completionTokens) {
76
- return null;
77
- }
78
- if (!modelInfo || !defaults?.model) {
79
- return null;
80
- }
81
- const currentModelInfo = modelInfo.find((m) => m.name === defaults.model);
82
- if (!currentModelInfo?.promptTokenCostPer1M || !currentModelInfo?.completionTokenCostPer1M) {
83
- return null;
84
- }
85
- return StateFulAxAgentUsage.calculateCost(modelUsage, currentModelInfo);
86
- }
154
+ // Legacy cost API removed: rely on Ax trackers for cost reporting
155
+ getLastUsageCost() { return null; }
87
156
  // Get the accumulated costs for all runs of this agent
88
- getAccumulatedCosts() {
89
- const stateKey = `${StateFulAxAgentUsage.STATE_KEY_PREFIX}${this.agentName}`;
90
- return this.state.get(stateKey);
157
+ getAccumulatedCosts() { return null; }
158
+ // Metrics API for this agent
159
+ getMetrics() {
160
+ const crewId = this.state?.crewId || (this.state.get?.('crewId')) || 'default';
161
+ return MetricsRegistry.snapshot({ crewId, agent: this.agentName });
162
+ }
163
+ resetMetrics() {
164
+ const crewId = this.state?.crewId || (this.state.get?.('crewId')) || 'default';
165
+ MetricsRegistry.reset({ crewId, agent: this.agentName });
91
166
  }
92
167
  }
93
168
  /**
@@ -121,6 +196,8 @@ class AxCrew {
121
196
  this.crewId = crewId;
122
197
  this.agents = new Map();
123
198
  this.state = createState(crewId);
199
+ // Make crewId discoverable to metrics
200
+ this.state.set('crewId', crewId);
124
201
  }
125
202
  /**
126
203
  * Factory function for creating an agent.
@@ -132,7 +209,7 @@ class AxCrew {
132
209
  try {
133
210
  const agentConfig = await parseAgentConfig(agentName, this.crewConfig, this.functionsRegistry, this.state);
134
211
  // Destructure with type assertion
135
- const { ai, name, description, signature, functions, subAgentNames, examples } = agentConfig;
212
+ const { ai, name, description, signature, functions, subAgentNames, examples, tracker } = agentConfig;
136
213
  // Get subagents for the AI agent
137
214
  const subAgents = subAgentNames.map((subAgentName) => {
138
215
  if (!this.agents?.get(subAgentName)) {
@@ -140,15 +217,39 @@ class AxCrew {
140
217
  }
141
218
  return this.agents?.get(subAgentName);
142
219
  });
220
+ // Dedupe sub-agents by name (defensive)
221
+ const subAgentSet = new Map();
222
+ for (const sa of subAgents.filter((agent) => agent !== undefined)) {
223
+ const n = sa?.agentName ?? sa?.name ?? '';
224
+ if (!subAgentSet.has(n))
225
+ subAgentSet.set(n, sa);
226
+ }
227
+ const uniqueSubAgents = Array.from(subAgentSet.values());
228
+ // Dedupe functions by name and avoid collision with sub-agent names
229
+ const subAgentNameSet = new Set(uniqueSubAgents.map((sa) => sa?.agentName ?? sa?.name).filter(Boolean));
230
+ const uniqueFunctions = [];
231
+ const seenFn = new Set();
232
+ for (const fn of functions.filter((fn) => fn !== undefined)) {
233
+ const fnName = fn.name;
234
+ if (subAgentNameSet.has(fnName)) {
235
+ // Skip function that collides with a sub-agent name
236
+ continue;
237
+ }
238
+ if (!seenFn.has(fnName)) {
239
+ seenFn.add(fnName);
240
+ uniqueFunctions.push(fn);
241
+ }
242
+ }
143
243
  // Create an instance of StatefulAxAgent
144
244
  const agent = new StatefulAxAgent(ai, {
145
245
  name,
146
246
  description,
147
247
  signature,
148
- functions: functions.filter((fn) => fn !== undefined),
149
- agents: subAgents.filter((agent) => agent !== undefined),
248
+ functions: uniqueFunctions,
249
+ agents: uniqueSubAgents,
150
250
  examples,
151
251
  }, this.state);
252
+ agent.costTracker = tracker;
152
253
  return agent;
153
254
  }
154
255
  catch (error) {
@@ -273,18 +374,31 @@ class AxCrew {
273
374
  this.agents = null;
274
375
  this.state.reset();
275
376
  }
276
- /**
277
- * Gets aggregated costs for all agents in the crew
278
- * @returns Aggregated cost information for all agents
279
- */
280
- getAggregatedCosts() {
281
- return StateFulAxAgentUsage.getAggregatedCosts(this.state);
282
- }
283
377
  /**
284
378
  * Resets all cost tracking for the crew
285
379
  */
286
380
  resetCosts() {
287
- StateFulAxAgentUsage.resetCosts(this.state);
381
+ // Reset AxAgent built-in usage and our metrics registry
382
+ if (this.agents) {
383
+ for (const [, agent] of this.agents) {
384
+ try {
385
+ agent.resetUsage?.();
386
+ }
387
+ catch { }
388
+ try {
389
+ agent.resetMetrics?.();
390
+ }
391
+ catch { }
392
+ }
393
+ }
394
+ MetricsRegistry.reset({ crewId: this.crewId });
395
+ }
396
+ // Metrics API
397
+ getCrewMetrics() {
398
+ return MetricsRegistry.snapshotCrew(this.crewId);
399
+ }
400
+ resetCrewMetrics() {
401
+ MetricsRegistry.reset({ crewId: this.crewId });
288
402
  }
289
403
  }
290
404
  export { AxCrew };
package/dist/index.d.ts CHANGED
@@ -2,6 +2,8 @@ import { AxCrew } from './agents/index.js';
2
2
  import { AxCrewFunctions } from './functions/index.js';
3
3
  import type { CrewConfigInput, AgentConfig } from './types.js';
4
4
  import type { UsageCost, AggregatedMetrics, AggregatedCosts, StateInstance, FunctionRegistryType } from './types.js';
5
+ export * from './metrics/index.js';
6
+ export { MetricsRegistry } from './metrics/index.js';
5
7
  /**
6
8
  * The configuration for an AxCrew.
7
9
  *
package/dist/index.js CHANGED
@@ -1,3 +1,5 @@
1
1
  import { AxCrew } from './agents/index.js';
2
2
  import { AxCrewFunctions } from './functions/index.js';
3
+ export * from './metrics/index.js';
4
+ export { MetricsRegistry } from './metrics/index.js';
3
5
  export { AxCrew, AxCrewFunctions, };