agentmetrics-langchain 0.1.0

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/README.md ADDED
@@ -0,0 +1,87 @@
1
+ # agentmetrics-langchain (JS/TS)
2
+
3
+ [![npm](https://img.shields.io/npm/v/agentmetrics-langchain?color=6366f1&label=npm&logo=npm&logoColor=white)](https://www.npmjs.com/package/agentmetrics-langchain)
4
+ [![License: MIT](https://img.shields.io/badge/license-MIT-6366f1)](../../LICENSE)
5
+
6
+ AgentMetrics integration for [LangChain.js](https://js.langchain.com). Pass one callback to any chain or agent `.invoke()` call and every run reports back to your dashboard showing latency, cost, token counts, tool calls, and errors, with no changes to your agent logic.
7
+
8
+ ---
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ npm install agentmetrics-langchain
14
+ ```
15
+
16
+ ---
17
+
18
+ ## Quickstart
19
+
20
+ ```typescript
21
+ import { AgentMetricsCallback } from "agentmetrics-langchain";
22
+
23
+ const cb = new AgentMetricsCallback({
24
+ agentId: "my-langchain-agent",
25
+ baseUrl: "http://localhost:8099",
26
+ });
27
+
28
+ const result = await agent.invoke(
29
+ { input: "What is the weather in Paris?" },
30
+ { callbacks: [cb] },
31
+ );
32
+ ```
33
+
34
+ ---
35
+
36
+ ## API
37
+
38
+ ### `new AgentMetricsCallback(opts)`
39
+
40
+ | Option | Default | Description |
41
+ |---|---|---|
42
+ | `agentId` | `"langchain-agent"` | Label shown in the dashboard |
43
+ | `baseUrl` | `"http://localhost:8099"` | AgentMetrics server address |
44
+
45
+ Pass the callback via the `callbacks` array in the second argument to `.invoke()`. It implements `BaseCallbackHandler` from `@langchain/core` and tracks the top-level chain only, with all nested sub-chains aggregated into the same run.
46
+
47
+ Supports both OpenAI (`usage_metadata`) and Anthropic (`input_token_details`) token counting paths.
48
+
49
+ ---
50
+
51
+ ## What gets tracked
52
+
53
+ Each top-level chain invocation emits one event to `/v1/events` on completion or error:
54
+
55
+ | Field | Description |
56
+ |---|---|
57
+ | `status` | `success` or `failed` |
58
+ | `duration_ms` | Wall-clock chain duration |
59
+ | `input_tokens` / `output_tokens` | Aggregated across all LLM calls |
60
+ | `cache_read_tokens` / `cache_write_tokens` | Cache token counts (Anthropic) |
61
+ | `llm_calls` | Number of LLM requests in the chain |
62
+ | `tool_calls` / `tool_errors` | Tool usage counts |
63
+ | `tool_names` | Array of tools invoked |
64
+ | `model` | Model name from the first LLM call |
65
+ | `estimated_cost_usd` | Computed from token counts and model pricing |
66
+ | `error` | First 500 chars of the error message on failure |
67
+
68
+ ---
69
+
70
+ ## LangGraph.js
71
+
72
+ Works with LangGraph.js the same way:
73
+
74
+ ```typescript
75
+ import { AgentMetricsCallback } from "agentmetrics-langchain";
76
+
77
+ const cb = new AgentMetricsCallback({ baseUrl: "http://localhost:8099" });
78
+ const app = buildGraph().compile();
79
+
80
+ const result = await app.invoke(state, { callbacks: [cb] });
81
+ ```
82
+
83
+ ---
84
+
85
+ ## License
86
+
87
+ [MIT](../../LICENSE)
@@ -0,0 +1,31 @@
1
+ import { BaseCallbackHandler } from "@langchain/core/callbacks/base";
2
+ import type { Serialized } from "@langchain/core/load/serializable";
3
+ import type { LLMResult } from "@langchain/core/outputs";
4
+ import type { ChainValues } from "@langchain/core/utils/types";
5
+ export declare class AgentMetricsCallback extends BaseCallbackHandler {
6
+ name: string;
7
+ private readonly _apiKey;
8
+ private readonly _agentId;
9
+ private readonly _baseUrl;
10
+ private readonly _runs;
11
+ private readonly _parentMap;
12
+ private readonly _pendingToolNames;
13
+ constructor(opts: {
14
+ apiKey: string;
15
+ agentId?: string;
16
+ baseUrl?: string;
17
+ });
18
+ handleChainStart(_serialized: Serialized, _inputs: ChainValues, runId: string, parentRunId?: string): Promise<void>;
19
+ handleChainEnd(_outputs: ChainValues, runId: string, parentRunId?: string): Promise<void>;
20
+ handleChainError(err: Error, runId: string, parentRunId?: string): Promise<void>;
21
+ handleLLMStart(_llm: Serialized, _prompts: string[], runId: string, parentRunId?: string): Promise<void>;
22
+ handleChatModelStart(_llm: Serialized, _messages: unknown[][], runId: string, parentRunId?: string): Promise<void>;
23
+ handleLLMEnd(output: LLMResult, runId: string): Promise<void>;
24
+ handleLLMError(_err: Error, _runId: string): Promise<void>;
25
+ handleToolStart(serialized: Serialized, _input: string, runId: string, parentRunId?: string): Promise<void>;
26
+ handleToolEnd(_output: unknown, runId: string): Promise<void>;
27
+ handleToolError(err: Error, runId: string): Promise<void>;
28
+ private _findTopRun;
29
+ private _emit;
30
+ }
31
+ //# sourceMappingURL=callback.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"callback.d.ts","sourceRoot":"","sources":["../src/callback.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,mBAAmB,EAAE,MAAM,gCAAgC,CAAC;AACrE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mCAAmC,CAAC;AACpE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AACzD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;AA2G/D,qBAAa,oBAAqB,SAAQ,mBAAmB;IAC3D,IAAI,SAAkB;IAEtB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAW;IACnC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAU;IACnC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAU;IAGnC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAoC;IAE1D,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA6B;IAExD,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAA6B;gBAEnD,IAAI,EAAE;QAChB,MAAM,EAAI,MAAM,CAAC;QACjB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,OAAO,CAAC,EAAE,MAAM,CAAC;KAClB;IAQK,gBAAgB,CACpB,WAAW,EAAE,UAAU,EACvB,OAAO,EAAE,WAAW,EACpB,KAAK,EAAE,MAAM,EACb,WAAW,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,IAAI,CAAC;IAQV,cAAc,CAClB,QAAQ,EAAE,WAAW,EACrB,KAAK,EAAE,MAAM,EACb,WAAW,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,IAAI,CAAC;IAMV,gBAAgB,CACpB,GAAG,EAAE,KAAK,EACV,KAAK,EAAE,MAAM,EACb,WAAW,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,IAAI,CAAC;IAYV,cAAc,CAClB,IAAI,EAAE,UAAU,EAChB,QAAQ,EAAE,MAAM,EAAE,EAClB,KAAK,EAAE,MAAM,EACb,WAAW,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,IAAI,CAAC;IAIV,oBAAoB,CACxB,IAAI,EAAE,UAAU,EAChB,SAAS,EAAE,OAAO,EAAE,EAAE,EACtB,KAAK,EAAE,MAAM,EACb,WAAW,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,IAAI,CAAC;IAIV,YAAY,CAChB,MAAM,EAAE,SAAS,EACjB,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,IAAI,CAAC;IAuCV,cAAc,CAAC,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAK1D,eAAe,CACnB,UAAU,EAAE,UAAU,EACtB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,EACb,WAAW,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,IAAI,CAAC;IAQV,aAAa,CACjB,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,IAAI,CAAC;IAUV,eAAe,CAAC,GAAG,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAY/D,OAAO,CAAC,WAAW;YAYL,KAAK;CA0CpB"}
@@ -0,0 +1,247 @@
1
+ import { randomUUID } from "crypto";
2
+ import { BaseCallbackHandler } from "@langchain/core/callbacks/base";
3
+ const PRICING = {
4
+ // [input, output, cache_read, cache_write]
5
+ "gpt-4o": [2.50, 10.00, 1.25, 0],
6
+ "gpt-4o-mini": [0.15, 0.60, 0.075, 0],
7
+ "gpt-4-turbo": [10.00, 30.00, 0, 0],
8
+ "gpt-3.5-turbo": [0.50, 1.50, 0, 0],
9
+ "claude-3-5-sonnet-20241022": [3.00, 15.00, 0.30, 3.75],
10
+ "claude-3-5-haiku-20241022": [0.80, 4.00, 0.08, 1.00],
11
+ "claude-3-opus-20240229": [15.00, 75.00, 1.50, 18.75],
12
+ "claude-sonnet-4-6": [3.00, 15.00, 0.30, 3.75],
13
+ "claude-opus-4-7": [15.00, 75.00, 1.50, 18.75],
14
+ "claude-haiku-4-5-20251001": [0.80, 4.00, 0.08, 1.00],
15
+ "gemini-1.5-pro": [1.25, 5.00, 0, 0],
16
+ "gemini-1.5-flash": [0.075, 0.30, 0, 0],
17
+ };
18
+ function estimateCost(model, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens) {
19
+ if (!model)
20
+ return null;
21
+ const key = Object.keys(PRICING).find(k => model.startsWith(k)) ?? null;
22
+ if (!key)
23
+ return null;
24
+ const [ip, op, cr, cw] = PRICING[key];
25
+ return ((inputTokens * ip + outputTokens * op +
26
+ cacheReadTokens * cr + cacheWriteTokens * cw) / 1_000_000);
27
+ }
28
+ const MAX_RETRY_ATTEMPTS = 3;
29
+ function sleep(ms) {
30
+ return new Promise(resolve => setTimeout(resolve, ms));
31
+ }
32
+ async function sendWithRetry(url, apiKey, payload) {
33
+ const body = JSON.stringify(payload);
34
+ for (let attempt = 0; attempt < MAX_RETRY_ATTEMPTS; attempt++) {
35
+ try {
36
+ const res = await fetch(`${url}/v1/events`, {
37
+ method: "POST",
38
+ headers: {
39
+ "Content-Type": "application/json",
40
+ Authorization: `Bearer ${apiKey}`,
41
+ },
42
+ body,
43
+ });
44
+ if (res.ok || (res.status >= 400 && res.status < 500))
45
+ return;
46
+ if (attempt < MAX_RETRY_ATTEMPTS - 1) {
47
+ await sleep(Math.min(1000 * 2 ** attempt + Math.random() * 200, 10_000));
48
+ }
49
+ }
50
+ catch {
51
+ if (attempt < MAX_RETRY_ATTEMPTS - 1) {
52
+ await sleep(Math.min(1000 * 2 ** attempt + Math.random() * 200, 10_000));
53
+ }
54
+ }
55
+ }
56
+ }
57
+ function newRunState(agentId) {
58
+ return {
59
+ agentId,
60
+ startMs: performance.now(),
61
+ inputTokens: 0,
62
+ outputTokens: 0,
63
+ cacheReadTokens: 0,
64
+ cacheWriteTokens: 0,
65
+ llmCalls: 0,
66
+ toolCalls: 0,
67
+ toolErrors: 0,
68
+ toolNames: new Set(),
69
+ model: null,
70
+ status: "success",
71
+ error: null,
72
+ };
73
+ }
74
+ export class AgentMetricsCallback extends BaseCallbackHandler {
75
+ name = "agentmetrics";
76
+ _apiKey;
77
+ _agentId;
78
+ _baseUrl;
79
+ // top-level run_id → RunState
80
+ _runs = new Map();
81
+ // run_id → parent run_id (ancestry chain)
82
+ _parentMap = new Map();
83
+ // tool run_id → tool name (resolved on end/error)
84
+ _pendingToolNames = new Map();
85
+ constructor(opts) {
86
+ super();
87
+ this._apiKey = opts.apiKey;
88
+ this._agentId = opts.agentId ?? "langchain-agent";
89
+ this._baseUrl = opts.baseUrl ?? "http://localhost:8099";
90
+ }
91
+ async handleChainStart(_serialized, _inputs, runId, parentRunId) {
92
+ if (!parentRunId) {
93
+ this._runs.set(runId, newRunState(this._agentId));
94
+ }
95
+ else {
96
+ this._parentMap.set(runId, parentRunId);
97
+ }
98
+ }
99
+ async handleChainEnd(_outputs, runId, parentRunId) {
100
+ if (!parentRunId) {
101
+ await this._emit(runId);
102
+ }
103
+ }
104
+ async handleChainError(err, runId, parentRunId) {
105
+ if (!parentRunId) {
106
+ const run = this._runs.get(runId);
107
+ if (run) {
108
+ run.status = "failed";
109
+ run.error = String(err).slice(0, 500);
110
+ }
111
+ await this._emit(runId);
112
+ }
113
+ }
114
+ async handleLLMStart(_llm, _prompts, runId, parentRunId) {
115
+ if (parentRunId)
116
+ this._parentMap.set(runId, parentRunId);
117
+ }
118
+ async handleChatModelStart(_llm, _messages, runId, parentRunId) {
119
+ if (parentRunId)
120
+ this._parentMap.set(runId, parentRunId);
121
+ }
122
+ async handleLLMEnd(output, runId) {
123
+ const run = this._findTopRun(runId);
124
+ if (!run)
125
+ return;
126
+ run.llmCalls++;
127
+ // Path 1: ChatGeneration → message.usage_metadata (Anthropic / OpenAI)
128
+ let foundUsage = false;
129
+ for (const genList of output.generations) {
130
+ for (const gen of genList) {
131
+ const msg = gen.message;
132
+ const umeta = msg?.usage_metadata;
133
+ if (umeta) {
134
+ run.inputTokens += umeta["input_tokens"] ?? 0;
135
+ run.outputTokens += umeta["output_tokens"] ?? 0;
136
+ const details = umeta["input_token_details"] ?? {};
137
+ run.cacheReadTokens += details["cache_read"] ?? 0;
138
+ run.cacheWriteTokens += details["cache_creation"] ?? 0;
139
+ if (!run.model) {
140
+ const rmeta = msg?.response_metadata;
141
+ run.model = rmeta?.["model_name"] ?? rmeta?.["model"] ?? null;
142
+ }
143
+ foundUsage = true;
144
+ }
145
+ }
146
+ }
147
+ if (foundUsage)
148
+ return;
149
+ // Path 2: llm_output dict (older/non-chat)
150
+ const lo = (output.llmOutput ?? {});
151
+ const usage = (lo["token_usage"] ?? lo["usage"] ?? {});
152
+ run.inputTokens += usage["prompt_tokens"] ?? 0;
153
+ run.outputTokens += usage["completion_tokens"] ?? 0;
154
+ run.cacheReadTokens += usage["cache_read_input_tokens"] ?? 0;
155
+ run.cacheWriteTokens += usage["cache_creation_input_tokens"] ?? 0;
156
+ if (!run.model) {
157
+ run.model = lo["model_name"] ?? lo["model"] ?? null;
158
+ }
159
+ }
160
+ async handleLLMError(_err, _runId) {
161
+ // LLM errors do not affect tool error counts
162
+ }
163
+ async handleToolStart(serialized, _input, runId, parentRunId) {
164
+ const name = serialized.name
165
+ ?? serialized.id?.at(-1)
166
+ ?? "unknown";
167
+ this._pendingToolNames.set(runId, String(name));
168
+ if (parentRunId)
169
+ this._parentMap.set(runId, parentRunId);
170
+ }
171
+ async handleToolEnd(_output, runId) {
172
+ const run = this._findTopRun(runId);
173
+ if (run) {
174
+ run.toolCalls++;
175
+ const name = this._pendingToolNames.get(runId);
176
+ if (name)
177
+ run.toolNames.add(name);
178
+ }
179
+ this._pendingToolNames.delete(runId);
180
+ }
181
+ async handleToolError(err, runId) {
182
+ const run = this._findTopRun(runId);
183
+ if (run) {
184
+ run.toolCalls++;
185
+ run.toolErrors++;
186
+ const name = this._pendingToolNames.get(runId);
187
+ if (name)
188
+ run.toolNames.add(name);
189
+ }
190
+ this._pendingToolNames.delete(runId);
191
+ }
192
+ _findTopRun(runId) {
193
+ const seen = new Set();
194
+ let rid = runId;
195
+ while (rid && !seen.has(rid)) {
196
+ seen.add(rid);
197
+ const state = this._runs.get(rid);
198
+ if (state)
199
+ return state;
200
+ rid = this._parentMap.get(rid);
201
+ }
202
+ return null;
203
+ }
204
+ async _emit(runId) {
205
+ const run = this._runs.get(runId);
206
+ if (!run)
207
+ return;
208
+ this._runs.delete(runId);
209
+ // clean up orphan parent-map entries
210
+ for (const [k, v] of this._parentMap) {
211
+ if (v === runId)
212
+ this._parentMap.delete(k);
213
+ }
214
+ const durationMs = performance.now() - run.startMs;
215
+ const est = estimateCost(run.model, run.inputTokens, run.outputTokens, run.cacheReadTokens, run.cacheWriteTokens);
216
+ const payload = {
217
+ event_id: randomUUID(),
218
+ trace_id: runId,
219
+ agent_id: run.agentId,
220
+ platform: "langchain",
221
+ event_name: "agent_end",
222
+ ts: Date.now(),
223
+ redaction_policy_version: "v1-strict",
224
+ status: run.status,
225
+ duration_ms: Math.round(durationMs * 100) / 100,
226
+ tool_calls: run.toolCalls,
227
+ tool_errors: run.toolErrors,
228
+ tool_names: [...run.toolNames],
229
+ llm_calls: run.llmCalls,
230
+ input_tokens: run.inputTokens,
231
+ output_tokens: run.outputTokens,
232
+ };
233
+ if (run.model)
234
+ payload["model"] = run.model;
235
+ if (run.error)
236
+ payload["error"] = run.error;
237
+ if (run.cacheReadTokens)
238
+ payload["cache_read_tokens"] = run.cacheReadTokens;
239
+ if (run.cacheWriteTokens)
240
+ payload["cache_write_tokens"] = run.cacheWriteTokens;
241
+ if (est !== null)
242
+ payload["estimated_cost_usd"] = est;
243
+ // fire-and-forget: don't block the caller
244
+ sendWithRetry(this._baseUrl, this._apiKey, payload).catch(() => { });
245
+ }
246
+ }
247
+ //# sourceMappingURL=callback.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"callback.js","sourceRoot":"","sources":["../src/callback.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export { AgentMetricsCallback } from "./callback.js";
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { AgentMetricsCallback } from "./callback.js";
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC"}
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "agentmetrics-langchain",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "AgentMetrics observability integration for LangChain.js",
6
+ "license": "MIT",
7
+ "homepage": "https://github.com/andalabx/agentmetrics",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/andalabx/agentmetrics.git"
11
+ },
12
+ "keywords": [
13
+ "langchain",
14
+ "agentmetrics",
15
+ "observability",
16
+ "ai-agents",
17
+ "monitoring"
18
+ ],
19
+ "exports": {
20
+ ".": {
21
+ "import": "./dist/index.js",
22
+ "types": "./dist/index.d.ts"
23
+ }
24
+ },
25
+ "main": "./dist/index.js",
26
+ "types": "./dist/index.d.ts",
27
+ "files": [
28
+ "dist",
29
+ "src"
30
+ ],
31
+ "scripts": {
32
+ "build": "tsc",
33
+ "prepublishOnly": "npm run build"
34
+ },
35
+ "peerDependencies": {
36
+ "@langchain/core": ">=0.2.0"
37
+ },
38
+ "devDependencies": {
39
+ "@langchain/core": ">=0.2.0",
40
+ "typescript": "^5.0.0"
41
+ },
42
+ "engines": {
43
+ "node": ">=18"
44
+ }
45
+ }
@@ -0,0 +1,334 @@
1
+ import { randomUUID } from "crypto";
2
+ import { BaseCallbackHandler } from "@langchain/core/callbacks/base";
3
+ import type { Serialized } from "@langchain/core/load/serializable";
4
+ import type { LLMResult } from "@langchain/core/outputs";
5
+ import type { ChainValues } from "@langchain/core/utils/types";
6
+
7
+
8
+ const PRICING: Record<string, [number, number, number, number]> = {
9
+ // [input, output, cache_read, cache_write]
10
+ "gpt-4o": [2.50, 10.00, 1.25, 0],
11
+ "gpt-4o-mini": [0.15, 0.60, 0.075, 0],
12
+ "gpt-4-turbo": [10.00, 30.00, 0, 0],
13
+ "gpt-3.5-turbo": [0.50, 1.50, 0, 0],
14
+ "claude-3-5-sonnet-20241022":[3.00, 15.00, 0.30, 3.75],
15
+ "claude-3-5-haiku-20241022": [0.80, 4.00, 0.08, 1.00],
16
+ "claude-3-opus-20240229": [15.00, 75.00, 1.50, 18.75],
17
+ "claude-sonnet-4-6": [3.00, 15.00, 0.30, 3.75],
18
+ "claude-opus-4-7": [15.00, 75.00, 1.50, 18.75],
19
+ "claude-haiku-4-5-20251001": [0.80, 4.00, 0.08, 1.00],
20
+ "gemini-1.5-pro": [1.25, 5.00, 0, 0],
21
+ "gemini-1.5-flash": [0.075, 0.30, 0, 0],
22
+ };
23
+
24
+ function estimateCost(
25
+ model: string | null,
26
+ inputTokens: number,
27
+ outputTokens: number,
28
+ cacheReadTokens: number,
29
+ cacheWriteTokens: number,
30
+ ): number | null {
31
+ if (!model) return null;
32
+ const key = Object.keys(PRICING).find(k => model.startsWith(k)) ?? null;
33
+ if (!key) return null;
34
+ const [ip, op, cr, cw] = PRICING[key];
35
+ return (
36
+ (inputTokens * ip + outputTokens * op +
37
+ cacheReadTokens * cr + cacheWriteTokens * cw) / 1_000_000
38
+ );
39
+ }
40
+
41
+
42
+ const MAX_RETRY_ATTEMPTS = 3;
43
+
44
+ function sleep(ms: number): Promise<void> {
45
+ return new Promise(resolve => setTimeout(resolve, ms));
46
+ }
47
+
48
+ async function sendWithRetry(
49
+ url: string,
50
+ apiKey: string,
51
+ payload: Record<string, unknown>,
52
+ ): Promise<void> {
53
+ const body = JSON.stringify(payload);
54
+ for (let attempt = 0; attempt < MAX_RETRY_ATTEMPTS; attempt++) {
55
+ try {
56
+ const res = await fetch(`${url}/v1/events`, {
57
+ method: "POST",
58
+ headers: {
59
+ "Content-Type": "application/json",
60
+ Authorization: `Bearer ${apiKey}`,
61
+ },
62
+ body,
63
+ });
64
+ if (res.ok || (res.status >= 400 && res.status < 500)) return;
65
+ if (attempt < MAX_RETRY_ATTEMPTS - 1) {
66
+ await sleep(Math.min(1000 * 2 ** attempt + Math.random() * 200, 10_000));
67
+ }
68
+ } catch {
69
+ if (attempt < MAX_RETRY_ATTEMPTS - 1) {
70
+ await sleep(Math.min(1000 * 2 ** attempt + Math.random() * 200, 10_000));
71
+ }
72
+ }
73
+ }
74
+ }
75
+
76
+
77
+ interface RunState {
78
+ agentId: string;
79
+ startMs: number;
80
+ inputTokens: number;
81
+ outputTokens: number;
82
+ cacheReadTokens: number;
83
+ cacheWriteTokens: number;
84
+ llmCalls: number;
85
+ toolCalls: number;
86
+ toolErrors: number;
87
+ toolNames: Set<string>;
88
+ model: string | null;
89
+ status: "success" | "failed";
90
+ error: string | null;
91
+ }
92
+
93
+ function newRunState(agentId: string): RunState {
94
+ return {
95
+ agentId,
96
+ startMs: performance.now(),
97
+ inputTokens: 0,
98
+ outputTokens: 0,
99
+ cacheReadTokens: 0,
100
+ cacheWriteTokens: 0,
101
+ llmCalls: 0,
102
+ toolCalls: 0,
103
+ toolErrors: 0,
104
+ toolNames: new Set(),
105
+ model: null,
106
+ status: "success",
107
+ error: null,
108
+ };
109
+ }
110
+
111
+
112
+ export class AgentMetricsCallback extends BaseCallbackHandler {
113
+ name = "agentmetrics";
114
+
115
+ private readonly _apiKey: string;
116
+ private readonly _agentId: string;
117
+ private readonly _baseUrl: string;
118
+
119
+ // top-level run_id → RunState
120
+ private readonly _runs = new Map<string, RunState>();
121
+ // run_id → parent run_id (ancestry chain)
122
+ private readonly _parentMap = new Map<string, string>();
123
+ // tool run_id → tool name (resolved on end/error)
124
+ private readonly _pendingToolNames = new Map<string, string>();
125
+
126
+ constructor(opts: {
127
+ apiKey: string;
128
+ agentId?: string;
129
+ baseUrl?: string;
130
+ }) {
131
+ super();
132
+ this._apiKey = opts.apiKey;
133
+ this._agentId = opts.agentId ?? "langchain-agent";
134
+ this._baseUrl = opts.baseUrl ?? "http://localhost:8099";
135
+ }
136
+
137
+
138
+ async handleChainStart(
139
+ _serialized: Serialized,
140
+ _inputs: ChainValues,
141
+ runId: string,
142
+ parentRunId?: string,
143
+ ): Promise<void> {
144
+ if (!parentRunId) {
145
+ this._runs.set(runId, newRunState(this._agentId));
146
+ } else {
147
+ this._parentMap.set(runId, parentRunId);
148
+ }
149
+ }
150
+
151
+ async handleChainEnd(
152
+ _outputs: ChainValues,
153
+ runId: string,
154
+ parentRunId?: string,
155
+ ): Promise<void> {
156
+ if (!parentRunId) {
157
+ await this._emit(runId);
158
+ }
159
+ }
160
+
161
+ async handleChainError(
162
+ err: Error,
163
+ runId: string,
164
+ parentRunId?: string,
165
+ ): Promise<void> {
166
+ if (!parentRunId) {
167
+ const run = this._runs.get(runId);
168
+ if (run) {
169
+ run.status = "failed";
170
+ run.error = String(err).slice(0, 500);
171
+ }
172
+ await this._emit(runId);
173
+ }
174
+ }
175
+
176
+
177
+ async handleLLMStart(
178
+ _llm: Serialized,
179
+ _prompts: string[],
180
+ runId: string,
181
+ parentRunId?: string,
182
+ ): Promise<void> {
183
+ if (parentRunId) this._parentMap.set(runId, parentRunId);
184
+ }
185
+
186
+ async handleChatModelStart(
187
+ _llm: Serialized,
188
+ _messages: unknown[][],
189
+ runId: string,
190
+ parentRunId?: string,
191
+ ): Promise<void> {
192
+ if (parentRunId) this._parentMap.set(runId, parentRunId);
193
+ }
194
+
195
+ async handleLLMEnd(
196
+ output: LLMResult,
197
+ runId: string,
198
+ ): Promise<void> {
199
+ const run = this._findTopRun(runId);
200
+ if (!run) return;
201
+ run.llmCalls++;
202
+
203
+ // Path 1: ChatGeneration → message.usage_metadata (Anthropic / OpenAI)
204
+ let foundUsage = false;
205
+ for (const genList of output.generations) {
206
+ for (const gen of genList) {
207
+ const msg = (gen as any).message;
208
+ const umeta = msg?.usage_metadata as Record<string, any> | undefined;
209
+ if (umeta) {
210
+ run.inputTokens += (umeta["input_tokens"] as number | null) ?? 0;
211
+ run.outputTokens += (umeta["output_tokens"] as number | null) ?? 0;
212
+ const details = (umeta["input_token_details"] as Record<string, number> | null) ?? {};
213
+ run.cacheReadTokens += details["cache_read"] ?? 0;
214
+ run.cacheWriteTokens += details["cache_creation"] ?? 0;
215
+ if (!run.model) {
216
+ const rmeta = msg?.response_metadata as Record<string, string> | undefined;
217
+ run.model = rmeta?.["model_name"] ?? rmeta?.["model"] ?? null;
218
+ }
219
+ foundUsage = true;
220
+ }
221
+ }
222
+ }
223
+ if (foundUsage) return;
224
+
225
+ // Path 2: llm_output dict (older/non-chat)
226
+ const lo = (output.llmOutput ?? {}) as Record<string, any>;
227
+ const usage = (lo["token_usage"] ?? lo["usage"] ?? {}) as Record<string, number>;
228
+ run.inputTokens += usage["prompt_tokens"] ?? 0;
229
+ run.outputTokens += usage["completion_tokens"] ?? 0;
230
+ run.cacheReadTokens += usage["cache_read_input_tokens"] ?? 0;
231
+ run.cacheWriteTokens += usage["cache_creation_input_tokens"] ?? 0;
232
+ if (!run.model) {
233
+ run.model = lo["model_name"] ?? lo["model"] ?? null;
234
+ }
235
+ }
236
+
237
+ async handleLLMError(_err: Error, _runId: string): Promise<void> {
238
+ // LLM errors do not affect tool error counts
239
+ }
240
+
241
+
242
+ async handleToolStart(
243
+ serialized: Serialized,
244
+ _input: string,
245
+ runId: string,
246
+ parentRunId?: string,
247
+ ): Promise<void> {
248
+ const name = (serialized as any).name
249
+ ?? ((serialized as any).id as string[] | undefined)?.at(-1)
250
+ ?? "unknown";
251
+ this._pendingToolNames.set(runId, String(name));
252
+ if (parentRunId) this._parentMap.set(runId, parentRunId);
253
+ }
254
+
255
+ async handleToolEnd(
256
+ _output: unknown,
257
+ runId: string,
258
+ ): Promise<void> {
259
+ const run = this._findTopRun(runId);
260
+ if (run) {
261
+ run.toolCalls++;
262
+ const name = this._pendingToolNames.get(runId);
263
+ if (name) run.toolNames.add(name);
264
+ }
265
+ this._pendingToolNames.delete(runId);
266
+ }
267
+
268
+ async handleToolError(err: Error, runId: string): Promise<void> {
269
+ const run = this._findTopRun(runId);
270
+ if (run) {
271
+ run.toolCalls++;
272
+ run.toolErrors++;
273
+ const name = this._pendingToolNames.get(runId);
274
+ if (name) run.toolNames.add(name);
275
+ }
276
+ this._pendingToolNames.delete(runId);
277
+ }
278
+
279
+
280
+ private _findTopRun(runId: string): RunState | null {
281
+ const seen = new Set<string>();
282
+ let rid: string | undefined = runId;
283
+ while (rid && !seen.has(rid)) {
284
+ seen.add(rid);
285
+ const state = this._runs.get(rid);
286
+ if (state) return state;
287
+ rid = this._parentMap.get(rid);
288
+ }
289
+ return null;
290
+ }
291
+
292
+ private async _emit(runId: string): Promise<void> {
293
+ const run = this._runs.get(runId);
294
+ if (!run) return;
295
+ this._runs.delete(runId);
296
+
297
+ // clean up orphan parent-map entries
298
+ for (const [k, v] of this._parentMap) {
299
+ if (v === runId) this._parentMap.delete(k);
300
+ }
301
+
302
+ const durationMs = performance.now() - run.startMs;
303
+ const est = estimateCost(
304
+ run.model, run.inputTokens, run.outputTokens,
305
+ run.cacheReadTokens, run.cacheWriteTokens,
306
+ );
307
+
308
+ const payload: Record<string, unknown> = {
309
+ event_id: randomUUID(),
310
+ trace_id: runId,
311
+ agent_id: run.agentId,
312
+ platform: "langchain",
313
+ event_name: "agent_end",
314
+ ts: Date.now(),
315
+ redaction_policy_version: "v1-strict",
316
+ status: run.status,
317
+ duration_ms: Math.round(durationMs * 100) / 100,
318
+ tool_calls: run.toolCalls,
319
+ tool_errors: run.toolErrors,
320
+ tool_names: [...run.toolNames],
321
+ llm_calls: run.llmCalls,
322
+ input_tokens: run.inputTokens,
323
+ output_tokens: run.outputTokens,
324
+ };
325
+ if (run.model) payload["model"] = run.model;
326
+ if (run.error) payload["error"] = run.error;
327
+ if (run.cacheReadTokens) payload["cache_read_tokens"] = run.cacheReadTokens;
328
+ if (run.cacheWriteTokens) payload["cache_write_tokens"] = run.cacheWriteTokens;
329
+ if (est !== null) payload["estimated_cost_usd"] = est;
330
+
331
+ // fire-and-forget: don't block the caller
332
+ sendWithRetry(this._baseUrl, this._apiKey, payload).catch(() => {});
333
+ }
334
+ }
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export { AgentMetricsCallback } from "./callback.js";