@visibe.ai/node 0.1.17 → 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.
@@ -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,
@@ -7,19 +7,19 @@ export const activeLangChainStorage = new AsyncLocalStorage();
7
7
  // ---------------------------------------------------------------------------
8
8
  // LangChain token extraction
9
9
  // Different providers nest token usage in different locations.
10
- // Check in the order specified by the spec.
11
10
  // ---------------------------------------------------------------------------
12
11
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
13
12
  function extractTokenUsage(output) {
14
13
  const usage = output?.llmOutput?.tokenUsage
15
14
  ?? output?.llmOutput?.usage
16
15
  ?? output?.generations?.[0]?.[0]?.generationInfo?.usage;
17
- // Use ?? not || so token counts of 0 are preserved correctly.
18
16
  return {
19
17
  inputTokens: usage?.promptTokens ?? usage?.input_tokens ?? 0,
20
18
  outputTokens: usage?.completionTokens ?? usage?.output_tokens ?? 0,
21
19
  };
22
20
  }
21
+ // Internal LangGraph system node names — never emit agent_start spans for these.
22
+ export const LANGGRAPH_INTERNAL_NODES = new Set(['__start__', '__end__', 'LangGraph']);
23
23
  // ---------------------------------------------------------------------------
24
24
  // LangChainCallback
25
25
  // ---------------------------------------------------------------------------
@@ -30,23 +30,21 @@ export class LangChainCallback {
30
30
  constructor(options) {
31
31
  // Required by @langchain/core v1+ for proper callback registration.
32
32
  // Without `name`, ensureHandler() wraps via fromMethods() which drops prototype methods.
33
- // Without `awaitHandlers`, callbacks run in a background queue (p-queue) and fire
34
- // after model.invoke() returns — causing spans to be missed on flush/completeTrace.
33
+ // Without `awaitHandlers`, callbacks run in a background queue and fire after
34
+ // model.invoke() returns — causing spans to be missed on flush/completeTrace.
35
35
  this.name = 'visibe-langchain-callback';
36
36
  this.awaitHandlers = true;
37
37
  this.raiseError = false;
38
38
  // Maps LangChain runId → our spanId so we can set parent_span_id.
39
39
  this.runIdToSpanId = new Map();
40
- // Tracks start times so we can compute durationMs.
41
- this.pendingLLMCalls = new Map(); // runId → startMs
40
+ // Pending LLM calls: runId { startMs, model, inputText }
41
+ this.pendingLLMCalls = new Map();
42
42
  this.pendingToolCalls = new Map();
43
43
  this.stepCounter = 0;
44
- // Agents we have already emitted agent_start spans for.
45
- this.seenAgents = new Set();
46
- // Token / call accumulators — updated by handleLLMEnd, read by patchCompiledStateGraph
47
- // and patchRunnableSequence to populate completeTrace totals.
44
+ // Token / call / cost accumulators read by patchCompiledStateGraph / patchRunnableSequence.
48
45
  this.totalInputTokens = 0;
49
46
  this.totalOutputTokens = 0;
47
+ this.totalCost = 0;
50
48
  this.llmCallCount = 0;
51
49
  this.visibe = options.visibe;
52
50
  this.traceId = options.traceId;
@@ -55,17 +53,40 @@ export class LangChainCallback {
55
53
  // ---------------------------------------------------------------------------
56
54
  // LLM events
57
55
  // ---------------------------------------------------------------------------
56
+ // Called for text-completion models.
58
57
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
59
- async handleLLMStart(_llm, _messages, runId) {
60
- this.pendingLLMCalls.set(runId, Date.now());
58
+ async handleLLMStart(llm, prompts, runId) {
59
+ const model = llm?.kwargs?.model ?? llm?.kwargs?.model_name ?? undefined;
60
+ const inputText = Array.isArray(prompts) ? prompts.join('\n') : '';
61
+ this.pendingLLMCalls.set(runId, { startMs: Date.now(), model, inputText });
62
+ }
63
+ // Called for chat models (ChatOpenAI, ChatAnthropic, etc.).
64
+ // messages is BaseMessage[][] — one array per parallel completion request.
65
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
66
+ async handleChatModelStart(llm, messages, runId) {
67
+ const model = llm?.kwargs?.model ?? llm?.kwargs?.model_name ?? undefined;
68
+ let inputText = '';
69
+ try {
70
+ const msgs = messages?.[0] ?? [];
71
+ inputText = msgs.map((m) => {
72
+ const role = m?.getType?.() ?? m?._getType?.() ?? m?.role ?? 'user';
73
+ const content = typeof m?.content === 'string'
74
+ ? m.content
75
+ : JSON.stringify(m?.content ?? '');
76
+ return `${role}: ${content}`;
77
+ }).join('\n');
78
+ }
79
+ catch { /* ignore serialisation errors */ }
80
+ this.pendingLLMCalls.set(runId, { startMs: Date.now(), model, inputText });
61
81
  }
62
82
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
63
83
  async handleLLMEnd(output, runId, parentRunId) {
64
- const startMs = this.pendingLLMCalls.get(runId) ?? Date.now();
84
+ const pending = this.pendingLLMCalls.get(runId) ?? { startMs: Date.now() };
65
85
  this.pendingLLMCalls.delete(runId);
66
86
  const { inputTokens, outputTokens } = extractTokenUsage(output);
67
87
  const gen = output?.generations?.[0]?.[0];
68
- const model = gen?.generationInfo?.model ?? this.agentName;
88
+ // Prefer model saved at LLM-start, fall back to generationInfo, then agentName.
89
+ const model = pending.model ?? gen?.generationInfo?.model ?? this.agentName;
69
90
  const cost = calculateCost(model, inputTokens, outputTokens);
70
91
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
71
92
  const rawText = gen?.text ?? gen?.message?.content ?? '';
@@ -81,16 +102,15 @@ export class LangChainCallback {
81
102
  status: 'success',
82
103
  inputTokens,
83
104
  outputTokens,
84
- inputText: '', // LangChain doesn't surface the raw prompt here
105
+ inputText: pending.inputText ?? '',
85
106
  outputText,
86
- durationMs: Date.now() - startMs,
107
+ durationMs: Date.now() - pending.startMs,
87
108
  });
88
109
  this.visibe.batcher.add(this.traceId, span);
89
- // Update local accumulators (used by patchCompiledStateGraph / patchRunnableSequence).
90
110
  this.totalInputTokens += inputTokens;
91
111
  this.totalOutputTokens += outputTokens;
112
+ this.totalCost += cost;
92
113
  this.llmCallCount++;
93
- // Notify track() accumulator if running inside a group tracker.
94
114
  this._onLLMSpan?.(inputTokens, outputTokens, cost);
95
115
  }
96
116
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -139,16 +159,15 @@ export class LangChainCallback {
139
159
  }
140
160
  // ---------------------------------------------------------------------------
141
161
  // Chain events
162
+ //
163
+ // In LangGraph v1.2+, the node key is passed as the 8th `name` parameter, NOT
164
+ // via chain.name. We always store runId → spanId so nested LLM/tool calls get
165
+ // a valid parent_span_id regardless of whether the chain is a user-defined node.
142
166
  // ---------------------------------------------------------------------------
143
167
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
144
- async handleChainStart(chain, _inputs, runId, parentRunId) {
145
- // Emit an agent_start span the first time we see a named chain.
146
- const chainName = chain?.id?.at(-1) ?? '';
147
- if (chainName && !this.seenAgents.has(chainName)) {
148
- this.seenAgents.add(chainName);
149
- const spanId = this.nextSpanId();
150
- this.runIdToSpanId.set(runId, spanId);
151
- void parentRunId; // suppress unused warning
168
+ async handleChainStart(_chain, _inputs, runId, _parentRunId, _tags, _metadata, _runType, _name) {
169
+ if (!this.runIdToSpanId.has(runId)) {
170
+ this.runIdToSpanId.set(runId, this.nextSpanId());
152
171
  }
153
172
  }
154
173
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -174,7 +193,6 @@ export function patchRunnableSequence(lcModule, visibe) {
174
193
  const originalStream = RunnableSequence.prototype.stream;
175
194
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
176
195
  RunnableSequence.prototype.invoke = async function (input, config) {
177
- // If already inside a LangChain trace, pass through.
178
196
  if (activeLangChainStorage.getStore() !== undefined) {
179
197
  return originalInvoke.call(this, input, config);
180
198
  }
@@ -189,6 +207,7 @@ export function patchRunnableSequence(lcModule, visibe) {
189
207
  ...(visibe.sessionId ? { session_id: visibe.sessionId } : {}),
190
208
  });
191
209
  const cb = new LangChainCallback({ visibe, traceId, agentName: 'langchain' });
210
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
192
211
  let result;
193
212
  let status = 'completed';
194
213
  try {
@@ -205,6 +224,7 @@ export function patchRunnableSequence(lcModule, visibe) {
205
224
  ended_at: new Date().toISOString(),
206
225
  duration_ms: Date.now() - startMs,
207
226
  llm_call_count: cb.llmCallCount,
227
+ total_cost: cb.totalCost,
208
228
  total_tokens: cb.totalInputTokens + cb.totalOutputTokens,
209
229
  total_input_tokens: cb.totalInputTokens,
210
230
  total_output_tokens: cb.totalOutputTokens,
@@ -231,9 +251,6 @@ export function patchRunnableSequence(lcModule, visibe) {
231
251
  const cb = new LangChainCallback({ visibe, traceId, agentName: 'langchain' });
232
252
  let status = 'completed';
233
253
  try {
234
- // RunnableSequence.stream() is an async function (not async generator) returning
235
- // a Promise<AsyncIterable>. activeLangChainStorage.run returns that Promise,
236
- // so we must await before yield*.
237
254
  const gen = await activeLangChainStorage.run(cb, () => originalStream.call(this, input, _mergeCallbacks(config, cb)));
238
255
  yield* gen;
239
256
  }
@@ -248,6 +265,7 @@ export function patchRunnableSequence(lcModule, visibe) {
248
265
  ended_at: new Date().toISOString(),
249
266
  duration_ms: Date.now() - startMs,
250
267
  llm_call_count: cb.llmCallCount,
268
+ total_cost: cb.totalCost,
251
269
  total_tokens: cb.totalInputTokens + cb.totalOutputTokens,
252
270
  total_input_tokens: cb.totalInputTokens,
253
271
  total_output_tokens: cb.totalOutputTokens,
@@ -262,7 +280,6 @@ export function patchRunnableSequence(lcModule, visibe) {
262
280
  // ---------------------------------------------------------------------------
263
281
  // Private helpers
264
282
  // ---------------------------------------------------------------------------
265
- // Merge our callback into an existing LangChain config object.
266
283
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
267
284
  function _mergeCallbacks(config, cb) {
268
285
  if (!config)
@@ -1,5 +1,5 @@
1
1
  import { randomUUID } from 'node:crypto';
2
- import { LangChainCallback, activeLangChainStorage } from './langchain.js';
2
+ import { LangChainCallback, activeLangChainStorage, LANGGRAPH_INTERNAL_NODES } from './langchain.js';
3
3
  // ---------------------------------------------------------------------------
4
4
  // LangGraphCallback
5
5
  // Extends LangChainCallback and adds node-level agent_start spans.
@@ -9,24 +9,26 @@ export class LangGraphCallback extends LangChainCallback {
9
9
  super(options);
10
10
  this.nodeNames = new Set(options.nodeNames ?? []);
11
11
  }
12
- // Override handleChainStart to emit agent_start for known graph nodes.
12
+ // Override handleChainStart to emit agent_start spans for LangGraph nodes.
13
+ //
14
+ // In LangGraph v1.2+, the node key is the 8th `name` parameter (not chain.name).
15
+ // Internal system nodes (__start__, __end__, LangGraph) are filtered out.
13
16
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
14
- async handleChainStart(chain, inputs, runId, parentRunId) {
15
- // LangGraph may surface the node key in chain.name (run name) or chain.id[-1] (class name).
16
- const chainName = chain?.name ?? chain?.id?.at(-1) ?? '';
17
- if (chainName && this.nodeNames.has(chainName)) {
18
- const spanId = this.nextSpanId();
17
+ async handleChainStart(chain, inputs, runId, parentRunId, tags, metadata, runType, name) {
18
+ // Always track the runId so child LLM calls can resolve parent_span_id.
19
+ await super.handleChainStart(chain, inputs, runId, parentRunId, tags, metadata, runType, name);
20
+ // The node key is passed as the 8th `name` parameter in LangGraph v1.2+.
21
+ // Fall back to chain.name for older versions.
22
+ const nodeName = name ?? chain?.name ?? '';
23
+ if (nodeName && !LANGGRAPH_INTERNAL_NODES.has(nodeName)) {
24
+ // Use the spanId already assigned by super for this runId.
25
+ const spanId = this.runIdToSpanId.get(runId) ?? this.nextSpanId();
19
26
  this.runIdToSpanId.set(runId, spanId);
20
- // Emit an agent_start span for this node.
21
- // type MUST be exactly "agent_start" — the backend validates this string.
22
27
  this.visibe.batcher.add(this.traceId, this.visibe.buildAgentStartSpan({
23
28
  spanId,
24
- agentName: chainName,
29
+ agentName: nodeName,
25
30
  }));
26
- // Don't call super — we've already set the runId mapping.
27
- return;
28
31
  }
29
- await super.handleChainStart(chain, inputs, runId, parentRunId);
30
32
  }
31
33
  }
32
34
  // ---------------------------------------------------------------------------
@@ -82,6 +84,7 @@ export function patchCompiledStateGraph(lgModule, visibe) {
82
84
  ended_at: new Date().toISOString(),
83
85
  duration_ms: Date.now() - startMs,
84
86
  llm_call_count: cb.llmCallCount,
87
+ total_cost: cb.totalCost,
85
88
  total_tokens: cb.totalInputTokens + cb.totalOutputTokens,
86
89
  total_input_tokens: cb.totalInputTokens,
87
90
  total_output_tokens: cb.totalOutputTokens,
@@ -133,6 +136,7 @@ export function patchCompiledStateGraph(lgModule, visibe) {
133
136
  ended_at: new Date().toISOString(),
134
137
  duration_ms: Date.now() - startMs,
135
138
  llm_call_count: cb.llmCallCount,
139
+ total_cost: cb.totalCost,
136
140
  total_tokens: cb.totalInputTokens + cb.totalOutputTokens,
137
141
  total_input_tokens: cb.totalInputTokens,
138
142
  total_output_tokens: cb.totalOutputTokens,
@@ -1,6 +1,7 @@
1
1
  import { AsyncLocalStorage } from 'node:async_hooks';
2
2
  import type { Visibe } from '../client';
3
3
  export declare const activeLangChainStorage: AsyncLocalStorage<LangChainCallback>;
4
+ export declare const LANGGRAPH_INTERNAL_NODES: Set<string>;
4
5
  export declare class LangChainCallback {
5
6
  readonly name = "visibe-langchain-callback";
6
7
  readonly awaitHandlers = true;
@@ -9,7 +10,11 @@ export declare class LangChainCallback {
9
10
  protected readonly traceId: string;
10
11
  protected readonly agentName: string;
11
12
  protected runIdToSpanId: Map<string, string>;
12
- protected pendingLLMCalls: Map<string, number>;
13
+ protected pendingLLMCalls: Map<string, {
14
+ startMs: number;
15
+ model?: string;
16
+ inputText?: string;
17
+ }>;
13
18
  protected pendingToolCalls: Map<string, {
14
19
  startMs: number;
15
20
  inputText: string;
@@ -17,22 +22,23 @@ export declare class LangChainCallback {
17
22
  }>;
18
23
  protected stepCounter: number;
19
24
  protected nextSpanId(): string;
20
- protected seenAgents: Set<string>;
21
25
  totalInputTokens: number;
22
26
  totalOutputTokens: number;
27
+ totalCost: number;
23
28
  llmCallCount: number;
24
29
  constructor(options: {
25
30
  visibe: Visibe;
26
31
  traceId: string;
27
32
  agentName: string;
28
33
  });
29
- handleLLMStart(_llm: any, _messages: any[], runId: string): Promise<void>;
34
+ handleLLMStart(llm: any, prompts: any[], runId: string): Promise<void>;
35
+ handleChatModelStart(llm: any, messages: any[][], runId: string): Promise<void>;
30
36
  handleLLMEnd(output: any, runId: string, parentRunId?: string): Promise<void>;
31
37
  handleLLMError(err: Error, runId: string): Promise<void>;
32
38
  handleToolStart(tool: any, input: string, runId: string): Promise<void>;
33
39
  handleToolEnd(output: string, runId: string, parentRunId?: string): Promise<void>;
34
40
  handleToolError(err: Error, runId: string): Promise<void>;
35
- handleChainStart(chain: any, _inputs: any, runId: string, parentRunId?: string): Promise<void>;
41
+ handleChainStart(_chain: any, _inputs: any, runId: string, _parentRunId?: string, _tags?: string[], _metadata?: any, _runType?: string, _name?: string): Promise<void>;
36
42
  handleChainEnd(_outputs: any, _runId: string): Promise<void>;
37
43
  handleChainError(err: Error, _runId: string): Promise<void>;
38
44
  _onLLMSpan?: (inputTokens: number, outputTokens: number, cost: number) => void;
@@ -8,6 +8,6 @@ export declare class LangGraphCallback extends LangChainCallback {
8
8
  agentName: string;
9
9
  nodeNames?: string[];
10
10
  });
11
- handleChainStart(chain: any, inputs: any, runId: string, parentRunId?: string): Promise<void>;
11
+ handleChainStart(chain: any, inputs: any, runId: string, parentRunId?: string, tags?: string[], metadata?: any, runType?: string, name?: string): Promise<void>;
12
12
  }
13
13
  export declare function patchCompiledStateGraph(lgModule: any, visibe: Visibe): () => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@visibe.ai/node",
3
- "version": "0.1.17",
3
+ "version": "0.1.18",
4
4
  "description": "AI Agent Observability — Track OpenAI, LangChain, LangGraph, Bedrock, Vercel AI, Anthropic",
5
5
  "main": "dist/cjs/index.js",
6
6
  "module": "dist/esm/index.js",