@visibe.ai/node 0.1.16 → 0.1.18

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/dist/cjs/index.js CHANGED
@@ -1,4 +1,37 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  exports.Visibe = void 0;
4
37
  exports.detectFrameworks = detectFrameworks;
@@ -10,30 +43,35 @@ const client_1 = require("./client");
10
43
  // ---------------------------------------------------------------------------
11
44
  let _globalClient = null;
12
45
  let _shutdownRegistered = false;
13
- // Saved original constructors so shutdown() can restore them.
14
- // Each is typed as `any` because we need to reassign imported class bindings.
46
+ // Saved originals so shutdown() can restore them.
15
47
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
16
48
  let _originalOpenAI = null;
17
49
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
18
- let _originalBedrockClient = null;
19
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
20
- let _originalCompiledStateGraph = null;
21
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
22
50
  let _originalAnthropic = null;
51
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
52
+ let _originalBedrockClient = null;
53
+ // Prototype-restore functions returned by patch helpers.
54
+ let _lgRestore = null;
55
+ let _lcRestore = null;
23
56
  let _vercelAIRestore = null;
57
+ let _autoPatchedFrameworks = [];
58
+ const ALL_FRAMEWORKS = ['openai', 'anthropic', 'bedrock', 'langgraph', 'langchain', 'vercel_ai'];
24
59
  // ---------------------------------------------------------------------------
25
- // detectFrameworks()
60
+ // detectFrameworks() — synchronous, CJS-only legacy helper
61
+ // Auto-patching now uses dynamic import() and works in both CJS and ESM.
26
62
  // ---------------------------------------------------------------------------
27
- function tryRequire(pkg) {
28
- try {
29
- require(pkg);
30
- return true;
31
- }
32
- catch {
33
- return false;
34
- }
35
- }
36
63
  function detectFrameworks() {
64
+ const tryRequire = (pkg) => {
65
+ try {
66
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
67
+ if (typeof require !== 'undefined') {
68
+ require(pkg);
69
+ return true;
70
+ }
71
+ }
72
+ catch { /* not installed */ }
73
+ return false;
74
+ };
37
75
  return {
38
76
  openai: tryRequire('openai'),
39
77
  langchain: tryRequire('@langchain/core'),
@@ -41,20 +79,32 @@ function detectFrameworks() {
41
79
  bedrock: tryRequire('@aws-sdk/client-bedrock-runtime'),
42
80
  vercel_ai: tryRequire('ai'),
43
81
  anthropic: tryRequire('@anthropic-ai/sdk'),
44
- // crewai and autogen are Python-only — no Node.js equivalent
45
82
  };
46
83
  }
47
84
  // ---------------------------------------------------------------------------
48
- // patchFramework() — auto-instruments a framework at the constructor level
85
+ // patchFramework() — async, uses dynamic import() for CJS + ESM compat.
86
+ //
87
+ // Key design notes:
88
+ // - dynamic import() in CJS builds: TypeScript compiles to require()-based
89
+ // Promise, so we get the same mutable module object as require().
90
+ // - dynamic import() in ESM builds: gives the live ESM namespace (same
91
+ // instance as the user's own `import`).
92
+ // - Prototype patching (langgraph, langchain) works in both envs — prototypes
93
+ // are shared mutable objects regardless of CJS/ESM.
94
+ // - Module-export patching (openai, anthropic, bedrock) only works in CJS
95
+ // because ESM namespace objects are sealed. We try and silently skip in ESM.
49
96
  // ---------------------------------------------------------------------------
50
- function patchFramework(framework, client) {
97
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
98
+ const _setProp = (obj, key, val) => Object.defineProperty(obj, key, { value: val, configurable: true, writable: true, enumerable: true });
99
+ async function patchFramework(framework, client) {
51
100
  try {
52
101
  switch (framework) {
53
102
  case 'openai': {
54
- const openaiModule = require('openai');
55
- _originalOpenAI = openaiModule.OpenAI;
56
- // Named 'OpenAI' so client.constructor.name === 'OpenAI' after construction.
57
- // applyIntegration() in client.ts uses constructor.name to detect the client type.
103
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
104
+ const openaiModule = await Promise.resolve().then(() => __importStar(require('openai')));
105
+ _originalOpenAI = openaiModule.OpenAI ?? openaiModule.default;
106
+ if (!_originalOpenAI)
107
+ return;
58
108
  const PatchedOpenAI = class OpenAI extends _originalOpenAI {
59
109
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
60
110
  constructor(...args) {
@@ -65,19 +115,24 @@ function patchFramework(framework, client) {
65
115
  catch { /* never crash new OpenAI() */ }
66
116
  }
67
117
  };
68
- // Use defineProperty because openai exports OpenAI/default as getter-only properties
69
- // (no setter). Direct assignment silently fails in that case.
70
- const setProp = (obj, key, val) => Object.defineProperty(obj, key, { value: val, configurable: true, writable: true, enumerable: true });
71
- setProp(openaiModule, 'OpenAI', PatchedOpenAI);
72
- // Also patch .default so that `import OpenAI from 'openai'` (esModuleInterop)
73
- // picks up the instrumented class — TypeScript compiles default imports to .default.
74
- setProp(openaiModule, 'default', PatchedOpenAI);
118
+ // In ESM, the namespace object is sealed defineProperty throws TypeError.
119
+ // We catch that and skip (user must call client.instrument() explicitly).
120
+ try {
121
+ _setProp(openaiModule, 'OpenAI', PatchedOpenAI);
122
+ _setProp(openaiModule, 'default', PatchedOpenAI);
123
+ }
124
+ catch {
125
+ _originalOpenAI = null;
126
+ return;
127
+ }
75
128
  break;
76
129
  }
77
130
  case 'anthropic': {
78
- const anthropicModule = require('@anthropic-ai/sdk');
79
- _originalAnthropic = anthropicModule.Anthropic;
80
- // Named 'Anthropic' so constructor.name check in applyIntegration() matches.
131
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
132
+ const anthropicModule = await Promise.resolve().then(() => __importStar(require('@anthropic-ai/sdk')));
133
+ _originalAnthropic = anthropicModule.Anthropic ?? anthropicModule.default;
134
+ if (!_originalAnthropic)
135
+ return;
81
136
  const PatchedAnthropic = class Anthropic extends _originalAnthropic {
82
137
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
83
138
  constructor(...args) {
@@ -88,17 +143,23 @@ function patchFramework(framework, client) {
88
143
  catch { /* never crash new Anthropic() */ }
89
144
  }
90
145
  };
91
- // Same getter-only issue as openai — use defineProperty.
92
- const setAnthropicProp = (obj, key, val) => Object.defineProperty(obj, key, { value: val, configurable: true, writable: true, enumerable: true });
93
- setAnthropicProp(anthropicModule, 'Anthropic', PatchedAnthropic);
94
- // Also patch .default for esModuleInterop default import support.
95
- setAnthropicProp(anthropicModule, 'default', PatchedAnthropic);
146
+ try {
147
+ _setProp(anthropicModule, 'Anthropic', PatchedAnthropic);
148
+ _setProp(anthropicModule, 'default', PatchedAnthropic);
149
+ }
150
+ catch {
151
+ _originalAnthropic = null;
152
+ return;
153
+ }
96
154
  break;
97
155
  }
98
156
  case 'bedrock': {
99
- const bedrockModule = require('@aws-sdk/client-bedrock-runtime');
157
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
158
+ const bedrockModule = await Promise.resolve().then(() => __importStar(require('@aws-sdk/client-bedrock-runtime')));
100
159
  _originalBedrockClient = bedrockModule.BedrockRuntimeClient;
101
- bedrockModule.BedrockRuntimeClient = class BedrockRuntimeClient extends _originalBedrockClient {
160
+ if (!_originalBedrockClient)
161
+ return;
162
+ const PatchedBedrock = class BedrockRuntimeClient extends _originalBedrockClient {
102
163
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
103
164
  constructor(...args) {
104
165
  super(...args);
@@ -108,39 +169,61 @@ function patchFramework(framework, client) {
108
169
  catch { /* never crash new BedrockRuntimeClient() */ }
109
170
  }
110
171
  };
172
+ try {
173
+ bedrockModule.BedrockRuntimeClient = PatchedBedrock;
174
+ }
175
+ catch {
176
+ _originalBedrockClient = null;
177
+ return;
178
+ }
111
179
  break;
112
180
  }
113
181
  case 'langgraph': {
114
- const lgModule = require('@langchain/langgraph');
115
- _originalCompiledStateGraph = lgModule.CompiledStateGraph;
116
- // LangGraph instrumentation is applied via LangChainCallback at the class level.
117
- // The actual patching happens inside the langgraph integration module.
118
- const { patchCompiledStateGraph } = require('./integrations/langgraph');
119
- patchCompiledStateGraph(lgModule, client);
182
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
183
+ const lgModule = await Promise.resolve().then(() => __importStar(require('@langchain/langgraph')));
184
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
185
+ const { patchCompiledStateGraph } = await Promise.resolve().then(() => __importStar(require('./integrations/langgraph')));
186
+ // patchCompiledStateGraph modifies CompiledStateGraph.prototype — prototype
187
+ // patching works in ESM because prototypes are shared mutable objects.
188
+ _lgRestore = patchCompiledStateGraph(lgModule, client);
120
189
  break;
121
190
  }
122
191
  case 'langchain': {
123
- // LangChain is instrumented via RunnableSequence constructor patching.
124
- const { patchRunnableSequence } = require('./integrations/langchain');
125
- const lcModule = require('@langchain/core/runnables');
126
- patchRunnableSequence(lcModule, client);
192
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
193
+ const { patchRunnableSequence } = await Promise.resolve().then(() => __importStar(require('./integrations/langchain')));
194
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
195
+ const lcModule = await Promise.resolve().then(() => __importStar(require('@langchain/core/runnables')));
196
+ const result = patchRunnableSequence(lcModule, client);
197
+ if (typeof result === 'function')
198
+ _lcRestore = result;
127
199
  break;
128
200
  }
129
201
  case 'vercel_ai': {
130
- const { patchVercelAI } = require('./integrations/vercel-ai');
131
- const aiModule = require('ai');
202
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
203
+ const { patchVercelAI } = await Promise.resolve().then(() => __importStar(require('./integrations/vercel-ai')));
204
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
205
+ const aiModule = await Promise.resolve().then(() => __importStar(require('ai')));
132
206
  _vercelAIRestore = patchVercelAI(aiModule, client);
133
207
  break;
134
208
  }
135
209
  }
136
- // Record which frameworks were successfully patched for the startup log.
137
210
  _autoPatchedFrameworks.push(framework);
138
211
  }
139
212
  catch {
140
213
  // Package not installed or patch failed — skip silently.
141
214
  }
142
215
  }
143
- let _autoPatchedFrameworks = [];
216
+ // ---------------------------------------------------------------------------
217
+ // _autoPatch() — async; fires from init() without blocking it
218
+ // ---------------------------------------------------------------------------
219
+ async function _autoPatch(client, frameworks) {
220
+ for (const fw of frameworks) {
221
+ await patchFramework(fw, client);
222
+ }
223
+ if (_autoPatchedFrameworks.length > 0) {
224
+ console.log(`[Visibe] Auto-instrumented: ${_autoPatchedFrameworks.join(', ')}`);
225
+ }
226
+ }
144
227
  // ---------------------------------------------------------------------------
145
228
  // init()
146
229
  // ---------------------------------------------------------------------------
@@ -150,17 +233,11 @@ function init(options) {
150
233
  return _globalClient;
151
234
  }
152
235
  _globalClient = new client_1.Visibe(options ?? {});
153
- const detected = detectFrameworks();
154
- const toInstrument = options?.frameworks
155
- ?? Object.keys(detected).filter(k => detected[k]);
156
- for (const fw of toInstrument) {
157
- patchFramework(fw, _globalClient);
158
- }
159
- // Register graceful shutdown handlers.
160
- // NOTE: process.on('exit') fires synchronously — async HTTP requests cannot
161
- // complete there. SIGTERM is what Docker/Kubernetes send before killing a
162
- // container; without handling it all buffered spans are lost.
163
- // We await shutdown() so the batcher's 300 ms window completes before exit.
236
+ // Fire async patching — works in both CJS and ESM via dynamic import().
237
+ // Patching typically completes within microseconds (cached modules) so there
238
+ // is no practical race condition for normal usage patterns.
239
+ const frameworksToTry = options?.frameworks ?? ALL_FRAMEWORKS;
240
+ _autoPatch(_globalClient, frameworksToTry).catch(() => { });
164
241
  if (!_shutdownRegistered) {
165
242
  const graceful = async () => { await shutdown(); process.exit(0); };
166
243
  process.on('SIGTERM', graceful);
@@ -168,9 +245,6 @@ function init(options) {
168
245
  process.on('beforeExit', () => { shutdown().catch(() => { }); });
169
246
  _shutdownRegistered = true;
170
247
  }
171
- if (_autoPatchedFrameworks.length > 0) {
172
- console.log(`[Visibe] Auto-instrumented: ${_autoPatchedFrameworks.join(', ')}`);
173
- }
174
248
  return _globalClient;
175
249
  }
176
250
  // ---------------------------------------------------------------------------
@@ -179,53 +253,65 @@ function init(options) {
179
253
  async function shutdown() {
180
254
  if (_globalClient === null)
181
255
  return;
182
- // Capture the client reference and clear global state immediately so that
183
- // re-init() calls work without needing to await this function.
184
256
  const client = _globalClient;
185
257
  _globalClient = null;
186
258
  _autoPatchedFrameworks = [];
187
- // Restore patched constructors so the SDK leaves no trace after shutdown.
188
- try {
189
- if (_originalOpenAI) {
190
- const m = require('openai');
259
+ // Restore patched module exports (works in CJS; silently no-ops in ESM).
260
+ if (_originalOpenAI) {
261
+ try {
262
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
263
+ const m = await Promise.resolve().then(() => __importStar(require('openai')));
191
264
  Object.defineProperty(m, 'OpenAI', { value: _originalOpenAI, configurable: true, writable: true, enumerable: true });
192
265
  Object.defineProperty(m, 'default', { value: _originalOpenAI, configurable: true, writable: true, enumerable: true });
193
- _originalOpenAI = null;
194
266
  }
267
+ catch { /* ignore */ }
268
+ _originalOpenAI = null;
195
269
  }
196
- catch { /* package may have been unloaded */ }
197
- try {
198
- if (_originalAnthropic) {
199
- const m = require('@anthropic-ai/sdk');
270
+ if (_originalAnthropic) {
271
+ try {
272
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
273
+ const m = await Promise.resolve().then(() => __importStar(require('@anthropic-ai/sdk')));
200
274
  Object.defineProperty(m, 'Anthropic', { value: _originalAnthropic, configurable: true, writable: true, enumerable: true });
201
275
  Object.defineProperty(m, 'default', { value: _originalAnthropic, configurable: true, writable: true, enumerable: true });
202
- _originalAnthropic = null;
203
276
  }
277
+ catch { /* ignore */ }
278
+ _originalAnthropic = null;
204
279
  }
205
- catch { /* package may have been unloaded */ }
206
- try {
207
- if (_originalBedrockClient) {
208
- require('@aws-sdk/client-bedrock-runtime').BedrockRuntimeClient = _originalBedrockClient;
209
- _originalBedrockClient = null;
280
+ if (_originalBedrockClient) {
281
+ try {
282
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
283
+ const m = await Promise.resolve().then(() => __importStar(require('@aws-sdk/client-bedrock-runtime')));
284
+ m.BedrockRuntimeClient = _originalBedrockClient;
210
285
  }
286
+ catch { /* ignore */ }
287
+ _originalBedrockClient = null;
211
288
  }
212
- catch { /* package may have been unloaded */ }
213
- try {
214
- if (_originalCompiledStateGraph) {
215
- require('@langchain/langgraph').CompiledStateGraph = _originalCompiledStateGraph;
216
- _originalCompiledStateGraph = null;
289
+ // Restore prototype patches via stored cleanup functions.
290
+ if (_lgRestore) {
291
+ try {
292
+ _lgRestore();
217
293
  }
294
+ catch { /* ignore */ }
295
+ ;
296
+ _lgRestore = null;
218
297
  }
219
- catch { /* package may have been unloaded */ }
220
- try {
221
- if (_vercelAIRestore) {
298
+ if (_lcRestore) {
299
+ try {
300
+ _lcRestore();
301
+ }
302
+ catch { /* ignore */ }
303
+ ;
304
+ _lcRestore = null;
305
+ }
306
+ if (_vercelAIRestore) {
307
+ try {
222
308
  _vercelAIRestore();
223
- _vercelAIRestore = null;
224
309
  }
310
+ catch { /* ignore */ }
311
+ ;
312
+ _vercelAIRestore = null;
225
313
  }
226
- catch { /* package may have been unloaded */ }
227
- // Flush buffered spans and wait up to 300 ms for in-flight HTTP requests to
228
- // complete. This prevents spans from being lost on SIGTERM.
314
+ // Flush buffered spans and wait up to 300 ms for in-flight HTTP requests.
229
315
  await client.batcher.shutdown();
230
316
  }
231
317
  // ---------------------------------------------------------------------------
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.LangChainCallback = exports.activeLangChainStorage = void 0;
3
+ exports.LangChainCallback = exports.LANGGRAPH_INTERNAL_NODES = exports.activeLangChainStorage = void 0;
4
4
  exports.patchRunnableSequence = patchRunnableSequence;
5
5
  const node_async_hooks_1 = require("node:async_hooks");
6
6
  const node_crypto_1 = require("node:crypto");
@@ -11,19 +11,19 @@ exports.activeLangChainStorage = new node_async_hooks_1.AsyncLocalStorage();
11
11
  // ---------------------------------------------------------------------------
12
12
  // LangChain token extraction
13
13
  // Different providers nest token usage in different locations.
14
- // Check in the order specified by the spec.
15
14
  // ---------------------------------------------------------------------------
16
15
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
17
16
  function extractTokenUsage(output) {
18
17
  const usage = output?.llmOutput?.tokenUsage
19
18
  ?? output?.llmOutput?.usage
20
19
  ?? output?.generations?.[0]?.[0]?.generationInfo?.usage;
21
- // Use ?? not || so token counts of 0 are preserved correctly.
22
20
  return {
23
21
  inputTokens: usage?.promptTokens ?? usage?.input_tokens ?? 0,
24
22
  outputTokens: usage?.completionTokens ?? usage?.output_tokens ?? 0,
25
23
  };
26
24
  }
25
+ // Internal LangGraph system node names — never emit agent_start spans for these.
26
+ exports.LANGGRAPH_INTERNAL_NODES = new Set(['__start__', '__end__', 'LangGraph']);
27
27
  // ---------------------------------------------------------------------------
28
28
  // LangChainCallback
29
29
  // ---------------------------------------------------------------------------
@@ -34,23 +34,21 @@ class LangChainCallback {
34
34
  constructor(options) {
35
35
  // Required by @langchain/core v1+ for proper callback registration.
36
36
  // Without `name`, ensureHandler() wraps via fromMethods() which drops prototype methods.
37
- // Without `awaitHandlers`, callbacks run in a background queue (p-queue) and fire
38
- // after model.invoke() returns — causing spans to be missed on flush/completeTrace.
37
+ // Without `awaitHandlers`, callbacks run in a background queue and fire after
38
+ // model.invoke() returns — causing spans to be missed on flush/completeTrace.
39
39
  this.name = 'visibe-langchain-callback';
40
40
  this.awaitHandlers = true;
41
41
  this.raiseError = false;
42
42
  // Maps LangChain runId → our spanId so we can set parent_span_id.
43
43
  this.runIdToSpanId = new Map();
44
- // Tracks start times so we can compute durationMs.
45
- this.pendingLLMCalls = new Map(); // runId → startMs
44
+ // Pending LLM calls: runId { startMs, model, inputText }
45
+ this.pendingLLMCalls = new Map();
46
46
  this.pendingToolCalls = new Map();
47
47
  this.stepCounter = 0;
48
- // Agents we have already emitted agent_start spans for.
49
- this.seenAgents = new Set();
50
- // Token / call accumulators — updated by handleLLMEnd, read by patchCompiledStateGraph
51
- // and patchRunnableSequence to populate completeTrace totals.
48
+ // Token / call / cost accumulators read by patchCompiledStateGraph / patchRunnableSequence.
52
49
  this.totalInputTokens = 0;
53
50
  this.totalOutputTokens = 0;
51
+ this.totalCost = 0;
54
52
  this.llmCallCount = 0;
55
53
  this.visibe = options.visibe;
56
54
  this.traceId = options.traceId;
@@ -59,17 +57,40 @@ class LangChainCallback {
59
57
  // ---------------------------------------------------------------------------
60
58
  // LLM events
61
59
  // ---------------------------------------------------------------------------
60
+ // Called for text-completion models.
62
61
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
63
- async handleLLMStart(_llm, _messages, runId) {
64
- this.pendingLLMCalls.set(runId, Date.now());
62
+ async handleLLMStart(llm, prompts, runId) {
63
+ const model = llm?.kwargs?.model ?? llm?.kwargs?.model_name ?? undefined;
64
+ const inputText = Array.isArray(prompts) ? prompts.join('\n') : '';
65
+ this.pendingLLMCalls.set(runId, { startMs: Date.now(), model, inputText });
66
+ }
67
+ // Called for chat models (ChatOpenAI, ChatAnthropic, etc.).
68
+ // messages is BaseMessage[][] — one array per parallel completion request.
69
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
70
+ async handleChatModelStart(llm, messages, runId) {
71
+ const model = llm?.kwargs?.model ?? llm?.kwargs?.model_name ?? undefined;
72
+ let inputText = '';
73
+ try {
74
+ const msgs = messages?.[0] ?? [];
75
+ inputText = msgs.map((m) => {
76
+ const role = m?.getType?.() ?? m?._getType?.() ?? m?.role ?? 'user';
77
+ const content = typeof m?.content === 'string'
78
+ ? m.content
79
+ : JSON.stringify(m?.content ?? '');
80
+ return `${role}: ${content}`;
81
+ }).join('\n');
82
+ }
83
+ catch { /* ignore serialisation errors */ }
84
+ this.pendingLLMCalls.set(runId, { startMs: Date.now(), model, inputText });
65
85
  }
66
86
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
67
87
  async handleLLMEnd(output, runId, parentRunId) {
68
- const startMs = this.pendingLLMCalls.get(runId) ?? Date.now();
88
+ const pending = this.pendingLLMCalls.get(runId) ?? { startMs: Date.now() };
69
89
  this.pendingLLMCalls.delete(runId);
70
90
  const { inputTokens, outputTokens } = extractTokenUsage(output);
71
91
  const gen = output?.generations?.[0]?.[0];
72
- const model = gen?.generationInfo?.model ?? this.agentName;
92
+ // Prefer model saved at LLM-start, fall back to generationInfo, then agentName.
93
+ const model = pending.model ?? gen?.generationInfo?.model ?? this.agentName;
73
94
  const cost = (0, utils_1.calculateCost)(model, inputTokens, outputTokens);
74
95
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
75
96
  const rawText = gen?.text ?? gen?.message?.content ?? '';
@@ -85,16 +106,15 @@ class LangChainCallback {
85
106
  status: 'success',
86
107
  inputTokens,
87
108
  outputTokens,
88
- inputText: '', // LangChain doesn't surface the raw prompt here
109
+ inputText: pending.inputText ?? '',
89
110
  outputText,
90
- durationMs: Date.now() - startMs,
111
+ durationMs: Date.now() - pending.startMs,
91
112
  });
92
113
  this.visibe.batcher.add(this.traceId, span);
93
- // Update local accumulators (used by patchCompiledStateGraph / patchRunnableSequence).
94
114
  this.totalInputTokens += inputTokens;
95
115
  this.totalOutputTokens += outputTokens;
116
+ this.totalCost += cost;
96
117
  this.llmCallCount++;
97
- // Notify track() accumulator if running inside a group tracker.
98
118
  this._onLLMSpan?.(inputTokens, outputTokens, cost);
99
119
  }
100
120
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -143,16 +163,15 @@ class LangChainCallback {
143
163
  }
144
164
  // ---------------------------------------------------------------------------
145
165
  // Chain events
166
+ //
167
+ // In LangGraph v1.2+, the node key is passed as the 8th `name` parameter, NOT
168
+ // via chain.name. We always store runId → spanId so nested LLM/tool calls get
169
+ // a valid parent_span_id regardless of whether the chain is a user-defined node.
146
170
  // ---------------------------------------------------------------------------
147
171
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
148
- async handleChainStart(chain, _inputs, runId, parentRunId) {
149
- // Emit an agent_start span the first time we see a named chain.
150
- const chainName = chain?.id?.at(-1) ?? '';
151
- if (chainName && !this.seenAgents.has(chainName)) {
152
- this.seenAgents.add(chainName);
153
- const spanId = this.nextSpanId();
154
- this.runIdToSpanId.set(runId, spanId);
155
- void parentRunId; // suppress unused warning
172
+ async handleChainStart(_chain, _inputs, runId, _parentRunId, _tags, _metadata, _runType, _name) {
173
+ if (!this.runIdToSpanId.has(runId)) {
174
+ this.runIdToSpanId.set(runId, this.nextSpanId());
156
175
  }
157
176
  }
158
177
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -179,7 +198,6 @@ function patchRunnableSequence(lcModule, visibe) {
179
198
  const originalStream = RunnableSequence.prototype.stream;
180
199
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
181
200
  RunnableSequence.prototype.invoke = async function (input, config) {
182
- // If already inside a LangChain trace, pass through.
183
201
  if (exports.activeLangChainStorage.getStore() !== undefined) {
184
202
  return originalInvoke.call(this, input, config);
185
203
  }
@@ -194,6 +212,7 @@ function patchRunnableSequence(lcModule, visibe) {
194
212
  ...(visibe.sessionId ? { session_id: visibe.sessionId } : {}),
195
213
  });
196
214
  const cb = new LangChainCallback({ visibe, traceId, agentName: 'langchain' });
215
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
197
216
  let result;
198
217
  let status = 'completed';
199
218
  try {
@@ -210,6 +229,7 @@ function patchRunnableSequence(lcModule, visibe) {
210
229
  ended_at: new Date().toISOString(),
211
230
  duration_ms: Date.now() - startMs,
212
231
  llm_call_count: cb.llmCallCount,
232
+ total_cost: cb.totalCost,
213
233
  total_tokens: cb.totalInputTokens + cb.totalOutputTokens,
214
234
  total_input_tokens: cb.totalInputTokens,
215
235
  total_output_tokens: cb.totalOutputTokens,
@@ -236,9 +256,6 @@ function patchRunnableSequence(lcModule, visibe) {
236
256
  const cb = new LangChainCallback({ visibe, traceId, agentName: 'langchain' });
237
257
  let status = 'completed';
238
258
  try {
239
- // RunnableSequence.stream() is an async function (not async generator) returning
240
- // a Promise<AsyncIterable>. activeLangChainStorage.run returns that Promise,
241
- // so we must await before yield*.
242
259
  const gen = await exports.activeLangChainStorage.run(cb, () => originalStream.call(this, input, _mergeCallbacks(config, cb)));
243
260
  yield* gen;
244
261
  }
@@ -253,6 +270,7 @@ function patchRunnableSequence(lcModule, visibe) {
253
270
  ended_at: new Date().toISOString(),
254
271
  duration_ms: Date.now() - startMs,
255
272
  llm_call_count: cb.llmCallCount,
273
+ total_cost: cb.totalCost,
256
274
  total_tokens: cb.totalInputTokens + cb.totalOutputTokens,
257
275
  total_input_tokens: cb.totalInputTokens,
258
276
  total_output_tokens: cb.totalOutputTokens,
@@ -267,7 +285,6 @@ function patchRunnableSequence(lcModule, visibe) {
267
285
  // ---------------------------------------------------------------------------
268
286
  // Private helpers
269
287
  // ---------------------------------------------------------------------------
270
- // Merge our callback into an existing LangChain config object.
271
288
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
272
289
  function _mergeCallbacks(config, cb) {
273
290
  if (!config)
@@ -13,24 +13,26 @@ class LangGraphCallback extends langchain_1.LangChainCallback {
13
13
  super(options);
14
14
  this.nodeNames = new Set(options.nodeNames ?? []);
15
15
  }
16
- // Override handleChainStart to emit agent_start for known graph nodes.
16
+ // Override handleChainStart to emit agent_start spans for LangGraph nodes.
17
+ //
18
+ // In LangGraph v1.2+, the node key is the 8th `name` parameter (not chain.name).
19
+ // Internal system nodes (__start__, __end__, LangGraph) are filtered out.
17
20
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
18
- async handleChainStart(chain, inputs, runId, parentRunId) {
19
- // LangGraph may surface the node key in chain.name (run name) or chain.id[-1] (class name).
20
- const chainName = chain?.name ?? chain?.id?.at(-1) ?? '';
21
- if (chainName && this.nodeNames.has(chainName)) {
22
- const spanId = this.nextSpanId();
21
+ async handleChainStart(chain, inputs, runId, parentRunId, tags, metadata, runType, name) {
22
+ // Always track the runId so child LLM calls can resolve parent_span_id.
23
+ await super.handleChainStart(chain, inputs, runId, parentRunId, tags, metadata, runType, name);
24
+ // The node key is passed as the 8th `name` parameter in LangGraph v1.2+.
25
+ // Fall back to chain.name for older versions.
26
+ const nodeName = name ?? chain?.name ?? '';
27
+ if (nodeName && !langchain_1.LANGGRAPH_INTERNAL_NODES.has(nodeName)) {
28
+ // Use the spanId already assigned by super for this runId.
29
+ const spanId = this.runIdToSpanId.get(runId) ?? this.nextSpanId();
23
30
  this.runIdToSpanId.set(runId, spanId);
24
- // Emit an agent_start span for this node.
25
- // type MUST be exactly "agent_start" — the backend validates this string.
26
31
  this.visibe.batcher.add(this.traceId, this.visibe.buildAgentStartSpan({
27
32
  spanId,
28
- agentName: chainName,
33
+ agentName: nodeName,
29
34
  }));
30
- // Don't call super — we've already set the runId mapping.
31
- return;
32
35
  }
33
- await super.handleChainStart(chain, inputs, runId, parentRunId);
34
36
  }
35
37
  }
36
38
  exports.LangGraphCallback = LangGraphCallback;
@@ -87,6 +89,7 @@ function patchCompiledStateGraph(lgModule, visibe) {
87
89
  ended_at: new Date().toISOString(),
88
90
  duration_ms: Date.now() - startMs,
89
91
  llm_call_count: cb.llmCallCount,
92
+ total_cost: cb.totalCost,
90
93
  total_tokens: cb.totalInputTokens + cb.totalOutputTokens,
91
94
  total_input_tokens: cb.totalInputTokens,
92
95
  total_output_tokens: cb.totalOutputTokens,
@@ -138,6 +141,7 @@ function patchCompiledStateGraph(lgModule, visibe) {
138
141
  ended_at: new Date().toISOString(),
139
142
  duration_ms: Date.now() - startMs,
140
143
  llm_call_count: cb.llmCallCount,
144
+ total_cost: cb.totalCost,
141
145
  total_tokens: cb.totalInputTokens + cb.totalOutputTokens,
142
146
  total_input_tokens: cb.totalInputTokens,
143
147
  total_output_tokens: cb.totalOutputTokens,