@visibe.ai/node 0.1.4

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.
Files changed (41) hide show
  1. package/README.md +330 -0
  2. package/dist/cjs/api.js +92 -0
  3. package/dist/cjs/client.js +242 -0
  4. package/dist/cjs/index.js +216 -0
  5. package/dist/cjs/integrations/anthropic.js +277 -0
  6. package/dist/cjs/integrations/base.js +32 -0
  7. package/dist/cjs/integrations/bedrock.js +442 -0
  8. package/dist/cjs/integrations/group-context.js +10 -0
  9. package/dist/cjs/integrations/langchain.js +274 -0
  10. package/dist/cjs/integrations/langgraph.js +173 -0
  11. package/dist/cjs/integrations/openai.js +447 -0
  12. package/dist/cjs/integrations/vercel-ai.js +261 -0
  13. package/dist/cjs/types/index.js +5 -0
  14. package/dist/cjs/utils.js +122 -0
  15. package/dist/esm/api.js +87 -0
  16. package/dist/esm/client.js +238 -0
  17. package/dist/esm/index.js +209 -0
  18. package/dist/esm/integrations/anthropic.js +272 -0
  19. package/dist/esm/integrations/base.js +28 -0
  20. package/dist/esm/integrations/bedrock.js +438 -0
  21. package/dist/esm/integrations/group-context.js +7 -0
  22. package/dist/esm/integrations/langchain.js +269 -0
  23. package/dist/esm/integrations/langgraph.js +168 -0
  24. package/dist/esm/integrations/openai.js +442 -0
  25. package/dist/esm/integrations/vercel-ai.js +258 -0
  26. package/dist/esm/types/index.js +4 -0
  27. package/dist/esm/utils.js +116 -0
  28. package/dist/types/api.d.ts +27 -0
  29. package/dist/types/client.d.ts +50 -0
  30. package/dist/types/index.d.ts +7 -0
  31. package/dist/types/integrations/anthropic.d.ts +9 -0
  32. package/dist/types/integrations/base.d.ts +17 -0
  33. package/dist/types/integrations/bedrock.d.ts +11 -0
  34. package/dist/types/integrations/group-context.d.ts +12 -0
  35. package/dist/types/integrations/langchain.d.ts +40 -0
  36. package/dist/types/integrations/langgraph.d.ts +13 -0
  37. package/dist/types/integrations/openai.d.ts +11 -0
  38. package/dist/types/integrations/vercel-ai.d.ts +2 -0
  39. package/dist/types/types/index.d.ts +21 -0
  40. package/dist/types/utils.d.ts +23 -0
  41. package/package.json +80 -0
@@ -0,0 +1,269 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { calculateCost } from '../utils';
4
+ // Re-export the storage so openai.ts and langgraph.ts can share the same instance.
5
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
6
+ export const activeLangChainStorage = new AsyncLocalStorage();
7
+ // ---------------------------------------------------------------------------
8
+ // LangChain token extraction
9
+ // Different providers nest token usage in different locations.
10
+ // Check in the order specified by the spec.
11
+ // ---------------------------------------------------------------------------
12
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
13
+ function extractTokenUsage(output) {
14
+ const usage = output?.llmOutput?.tokenUsage
15
+ ?? output?.llmOutput?.usage
16
+ ?? output?.generations?.[0]?.[0]?.generationInfo?.usage;
17
+ // Use ?? not || so token counts of 0 are preserved correctly.
18
+ return {
19
+ inputTokens: usage?.promptTokens ?? usage?.input_tokens ?? 0,
20
+ outputTokens: usage?.completionTokens ?? usage?.output_tokens ?? 0,
21
+ };
22
+ }
23
+ // ---------------------------------------------------------------------------
24
+ // LangChainCallback
25
+ // ---------------------------------------------------------------------------
26
+ export class LangChainCallback {
27
+ nextSpanId() {
28
+ return `step_${++this.stepCounter}`;
29
+ }
30
+ constructor(options) {
31
+ // Required by @langchain/core v1+ for proper callback registration.
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.
35
+ this.name = 'visibe-langchain-callback';
36
+ this.awaitHandlers = true;
37
+ this.raiseError = false;
38
+ // Maps LangChain runId → our spanId so we can set parent_span_id.
39
+ this.runIdToSpanId = new Map();
40
+ // Tracks start times so we can compute durationMs.
41
+ this.pendingLLMCalls = new Map(); // runId → startMs
42
+ this.pendingToolCalls = new Map();
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.
48
+ this.totalInputTokens = 0;
49
+ this.totalOutputTokens = 0;
50
+ this.llmCallCount = 0;
51
+ this.visibe = options.visibe;
52
+ this.traceId = options.traceId;
53
+ this.agentName = options.agentName;
54
+ }
55
+ // ---------------------------------------------------------------------------
56
+ // LLM events
57
+ // ---------------------------------------------------------------------------
58
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
59
+ async handleLLMStart(_llm, _messages, runId) {
60
+ this.pendingLLMCalls.set(runId, Date.now());
61
+ }
62
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
63
+ async handleLLMEnd(output, runId, parentRunId) {
64
+ const startMs = this.pendingLLMCalls.get(runId) ?? Date.now();
65
+ this.pendingLLMCalls.delete(runId);
66
+ const { inputTokens, outputTokens } = extractTokenUsage(output);
67
+ const gen = output?.generations?.[0]?.[0];
68
+ const model = gen?.generationInfo?.model ?? this.agentName;
69
+ const cost = calculateCost(model, inputTokens, outputTokens);
70
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
71
+ const rawText = gen?.text ?? gen?.message?.content ?? '';
72
+ const outputText = typeof rawText === 'string' ? rawText : JSON.stringify(rawText);
73
+ const spanId = this.nextSpanId();
74
+ this.runIdToSpanId.set(runId, spanId);
75
+ const parentSpanId = parentRunId ? this.runIdToSpanId.get(parentRunId) : undefined;
76
+ const span = this.visibe.buildLLMSpan({
77
+ spanId,
78
+ parentSpanId,
79
+ agentName: this.agentName,
80
+ model,
81
+ status: 'success',
82
+ inputTokens,
83
+ outputTokens,
84
+ inputText: '', // LangChain doesn't surface the raw prompt here
85
+ outputText,
86
+ durationMs: Date.now() - startMs,
87
+ });
88
+ this.visibe.batcher.add(this.traceId, span);
89
+ // Update local accumulators (used by patchCompiledStateGraph / patchRunnableSequence).
90
+ this.totalInputTokens += inputTokens;
91
+ this.totalOutputTokens += outputTokens;
92
+ this.llmCallCount++;
93
+ // Notify track() accumulator if running inside a group tracker.
94
+ this._onLLMSpan?.(inputTokens, outputTokens, cost);
95
+ }
96
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
97
+ async handleLLMError(err, runId) {
98
+ this.pendingLLMCalls.delete(runId);
99
+ this.visibe.batcher.add(this.traceId, this.visibe.buildErrorSpan({
100
+ spanId: this.nextSpanId(),
101
+ errorType: err?.constructor?.name ?? 'Error',
102
+ errorMessage: err?.message ?? String(err),
103
+ }));
104
+ }
105
+ // ---------------------------------------------------------------------------
106
+ // Tool events
107
+ // ---------------------------------------------------------------------------
108
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
109
+ async handleToolStart(_tool, input, runId) {
110
+ this.pendingToolCalls.set(runId, { startMs: Date.now(), inputText: input });
111
+ }
112
+ async handleToolEnd(output, runId, parentRunId) {
113
+ const pending = this.pendingToolCalls.get(runId);
114
+ this.pendingToolCalls.delete(runId);
115
+ const spanId = this.nextSpanId();
116
+ this.runIdToSpanId.set(runId, spanId);
117
+ const parentSpanId = parentRunId ? this.runIdToSpanId.get(parentRunId) : undefined;
118
+ const span = this.visibe.buildToolSpan({
119
+ spanId,
120
+ parentSpanId,
121
+ toolName: 'tool',
122
+ agentName: this.agentName,
123
+ status: 'success',
124
+ durationMs: pending ? Date.now() - pending.startMs : 0,
125
+ inputText: pending?.inputText ?? '',
126
+ outputText: output,
127
+ });
128
+ this.visibe.batcher.add(this.traceId, span);
129
+ this._onToolSpan?.();
130
+ }
131
+ async handleToolError(err, runId) {
132
+ this.pendingToolCalls.delete(runId);
133
+ this.visibe.batcher.add(this.traceId, this.visibe.buildErrorSpan({
134
+ spanId: this.nextSpanId(),
135
+ errorType: err?.constructor?.name ?? 'Error',
136
+ errorMessage: err?.message ?? String(err),
137
+ }));
138
+ }
139
+ // ---------------------------------------------------------------------------
140
+ // Chain events
141
+ // ---------------------------------------------------------------------------
142
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
143
+ async handleChainStart(chain, _inputs, runId, parentRunId) {
144
+ // Emit an agent_start span the first time we see a named chain.
145
+ const chainName = chain?.id?.at(-1) ?? '';
146
+ if (chainName && !this.seenAgents.has(chainName)) {
147
+ this.seenAgents.add(chainName);
148
+ const spanId = this.nextSpanId();
149
+ this.runIdToSpanId.set(runId, spanId);
150
+ void parentRunId; // suppress unused warning
151
+ }
152
+ }
153
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
154
+ async handleChainEnd(_outputs, _runId) { }
155
+ async handleChainError(err, _runId) {
156
+ this.visibe.batcher.add(this.traceId, this.visibe.buildErrorSpan({
157
+ spanId: this.nextSpanId(),
158
+ errorType: err?.constructor?.name ?? 'Error',
159
+ errorMessage: err?.message ?? String(err),
160
+ }));
161
+ }
162
+ }
163
+ // ---------------------------------------------------------------------------
164
+ // patchRunnableSequence — patches RunnableSequence so pipe() chains are
165
+ // automatically instrumented. Called from index.ts patchFramework().
166
+ // ---------------------------------------------------------------------------
167
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
168
+ export function patchRunnableSequence(lcModule, visibe) {
169
+ const RunnableSequence = lcModule?.RunnableSequence;
170
+ if (!RunnableSequence)
171
+ return () => { };
172
+ const originalInvoke = RunnableSequence.prototype.invoke;
173
+ const originalStream = RunnableSequence.prototype.stream;
174
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
175
+ RunnableSequence.prototype.invoke = async function (input, config) {
176
+ // If already inside a LangChain trace, pass through.
177
+ if (activeLangChainStorage.getStore() !== undefined) {
178
+ return originalInvoke.call(this, input, config);
179
+ }
180
+ const traceId = randomUUID();
181
+ const startedAt = new Date().toISOString();
182
+ const startMs = Date.now();
183
+ await visibe.apiClient.createTrace({
184
+ trace_id: traceId,
185
+ name: 'langchain',
186
+ framework: 'langchain',
187
+ started_at: startedAt,
188
+ ...(visibe.sessionId ? { session_id: visibe.sessionId } : {}),
189
+ });
190
+ const cb = new LangChainCallback({ visibe, traceId, agentName: 'langchain' });
191
+ let result;
192
+ let status = 'completed';
193
+ try {
194
+ result = await activeLangChainStorage.run(cb, () => originalInvoke.call(this, input, _mergeCallbacks(config, cb)));
195
+ }
196
+ catch (err) {
197
+ status = 'failed';
198
+ throw err;
199
+ }
200
+ finally {
201
+ visibe.batcher.flush();
202
+ await visibe.apiClient.completeTrace(traceId, {
203
+ status,
204
+ ended_at: new Date().toISOString(),
205
+ duration_ms: Date.now() - startMs,
206
+ total_tokens: cb.totalInputTokens + cb.totalOutputTokens,
207
+ total_input_tokens: cb.totalInputTokens,
208
+ total_output_tokens: cb.totalOutputTokens,
209
+ });
210
+ }
211
+ return result;
212
+ };
213
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
214
+ RunnableSequence.prototype.stream = async function* (input, config) {
215
+ if (activeLangChainStorage.getStore() !== undefined) {
216
+ yield* originalStream.call(this, input, config);
217
+ return;
218
+ }
219
+ const traceId = randomUUID();
220
+ const startedAt = new Date().toISOString();
221
+ const startMs = Date.now();
222
+ await visibe.apiClient.createTrace({
223
+ trace_id: traceId,
224
+ name: 'langchain',
225
+ framework: 'langchain',
226
+ started_at: startedAt,
227
+ ...(visibe.sessionId ? { session_id: visibe.sessionId } : {}),
228
+ });
229
+ const cb = new LangChainCallback({ visibe, traceId, agentName: 'langchain' });
230
+ let status = 'completed';
231
+ try {
232
+ // RunnableSequence.stream() is an async function (not async generator) returning
233
+ // a Promise<AsyncIterable>. activeLangChainStorage.run returns that Promise,
234
+ // so we must await before yield*.
235
+ const gen = await activeLangChainStorage.run(cb, () => originalStream.call(this, input, _mergeCallbacks(config, cb)));
236
+ yield* gen;
237
+ }
238
+ catch (err) {
239
+ status = 'failed';
240
+ throw err;
241
+ }
242
+ finally {
243
+ visibe.batcher.flush();
244
+ await visibe.apiClient.completeTrace(traceId, {
245
+ status,
246
+ ended_at: new Date().toISOString(),
247
+ duration_ms: Date.now() - startMs,
248
+ total_tokens: cb.totalInputTokens + cb.totalOutputTokens,
249
+ total_input_tokens: cb.totalInputTokens,
250
+ total_output_tokens: cb.totalOutputTokens,
251
+ });
252
+ }
253
+ };
254
+ return () => {
255
+ RunnableSequence.prototype.invoke = originalInvoke;
256
+ RunnableSequence.prototype.stream = originalStream;
257
+ };
258
+ }
259
+ // ---------------------------------------------------------------------------
260
+ // Private helpers
261
+ // ---------------------------------------------------------------------------
262
+ // Merge our callback into an existing LangChain config object.
263
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
264
+ function _mergeCallbacks(config, cb) {
265
+ if (!config)
266
+ return { callbacks: [cb] };
267
+ const existing = Array.isArray(config.callbacks) ? config.callbacks : [];
268
+ return { ...config, callbacks: [...existing, cb] };
269
+ }
@@ -0,0 +1,168 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { LangChainCallback, activeLangChainStorage } from './langchain';
3
+ // ---------------------------------------------------------------------------
4
+ // LangGraphCallback
5
+ // Extends LangChainCallback and adds node-level agent_start spans.
6
+ // ---------------------------------------------------------------------------
7
+ export class LangGraphCallback extends LangChainCallback {
8
+ constructor(options) {
9
+ super(options);
10
+ this.nodeNames = new Set(options.nodeNames ?? []);
11
+ }
12
+ // Override handleChainStart to emit agent_start for known graph nodes.
13
+ // 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();
19
+ 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
+ this.visibe.batcher.add(this.traceId, this.visibe.buildAgentStartSpan({
23
+ spanId,
24
+ agentName: chainName,
25
+ }));
26
+ // Don't call super — we've already set the runId mapping.
27
+ return;
28
+ }
29
+ await super.handleChainStart(chain, inputs, runId, parentRunId);
30
+ }
31
+ }
32
+ // ---------------------------------------------------------------------------
33
+ // patchCompiledStateGraph
34
+ // Patches the CompiledStateGraph class so every graph created after init()
35
+ // is automatically instrumented. Class-level patching is required — patching
36
+ // a single instance only instruments that one instance.
37
+ // ---------------------------------------------------------------------------
38
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
39
+ export function patchCompiledStateGraph(lgModule, visibe) {
40
+ const CompiledStateGraph = lgModule?.CompiledStateGraph;
41
+ if (!CompiledStateGraph)
42
+ return () => { };
43
+ const originalInvoke = CompiledStateGraph.prototype.invoke;
44
+ const originalStream = CompiledStateGraph.prototype.stream;
45
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
46
+ CompiledStateGraph.prototype.invoke = async function (input, config) {
47
+ if (activeLangChainStorage.getStore() !== undefined) {
48
+ return originalInvoke.call(this, input, config);
49
+ }
50
+ const traceId = randomUUID();
51
+ const startedAt = new Date().toISOString();
52
+ const startMs = Date.now();
53
+ const graphName = this.name ?? 'langgraph';
54
+ await visibe.apiClient.createTrace({
55
+ trace_id: traceId,
56
+ name: graphName,
57
+ framework: 'langgraph',
58
+ started_at: startedAt,
59
+ ...(visibe.sessionId ? { session_id: visibe.sessionId } : {}),
60
+ });
61
+ // Collect node names from the graph's node registry.
62
+ const nodeNames = _extractNodeNames(this);
63
+ const cb = new LangGraphCallback({
64
+ visibe,
65
+ traceId,
66
+ agentName: graphName,
67
+ nodeNames,
68
+ });
69
+ let result;
70
+ let status = 'completed';
71
+ try {
72
+ result = await activeLangChainStorage.run(cb, () => originalInvoke.call(this, input, _mergeCallbacks(config, cb)));
73
+ }
74
+ catch (err) {
75
+ status = 'failed';
76
+ throw err;
77
+ }
78
+ finally {
79
+ visibe.batcher.flush();
80
+ await visibe.apiClient.completeTrace(traceId, {
81
+ status,
82
+ ended_at: new Date().toISOString(),
83
+ duration_ms: Date.now() - startMs,
84
+ total_tokens: cb.totalInputTokens + cb.totalOutputTokens,
85
+ total_input_tokens: cb.totalInputTokens,
86
+ total_output_tokens: cb.totalOutputTokens,
87
+ });
88
+ }
89
+ return result;
90
+ };
91
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
92
+ CompiledStateGraph.prototype.stream = async function* (input, config) {
93
+ if (activeLangChainStorage.getStore() !== undefined) {
94
+ // Pregel.stream() is a regular async function returning a Promise<AsyncIterable>.
95
+ // We must await it before yield*.
96
+ yield* (await originalStream.call(this, input, config));
97
+ return;
98
+ }
99
+ const traceId = randomUUID();
100
+ const startedAt = new Date().toISOString();
101
+ const startMs = Date.now();
102
+ const graphName = this.name ?? 'langgraph';
103
+ await visibe.apiClient.createTrace({
104
+ trace_id: traceId,
105
+ name: graphName,
106
+ framework: 'langgraph',
107
+ started_at: startedAt,
108
+ ...(visibe.sessionId ? { session_id: visibe.sessionId } : {}),
109
+ });
110
+ const nodeNames = _extractNodeNames(this);
111
+ const cb = new LangGraphCallback({
112
+ visibe,
113
+ traceId,
114
+ agentName: graphName,
115
+ nodeNames,
116
+ });
117
+ let status = 'completed';
118
+ try {
119
+ // activeLangChainStorage.run returns the callback's return value synchronously,
120
+ // which is a Promise<AsyncIterable> from Pregel.stream(). Await before yield*.
121
+ const gen = await activeLangChainStorage.run(cb, () => originalStream.call(this, input, _mergeCallbacks(config, cb)));
122
+ yield* gen;
123
+ }
124
+ catch (err) {
125
+ status = 'failed';
126
+ throw err;
127
+ }
128
+ finally {
129
+ visibe.batcher.flush();
130
+ await visibe.apiClient.completeTrace(traceId, {
131
+ status,
132
+ ended_at: new Date().toISOString(),
133
+ duration_ms: Date.now() - startMs,
134
+ total_tokens: cb.totalInputTokens + cb.totalOutputTokens,
135
+ total_input_tokens: cb.totalInputTokens,
136
+ total_output_tokens: cb.totalOutputTokens,
137
+ });
138
+ }
139
+ };
140
+ return () => {
141
+ CompiledStateGraph.prototype.invoke = originalInvoke;
142
+ CompiledStateGraph.prototype.stream = originalStream;
143
+ };
144
+ }
145
+ // ---------------------------------------------------------------------------
146
+ // Private helpers
147
+ // ---------------------------------------------------------------------------
148
+ // Extract the set of node names from a compiled graph's internal node registry.
149
+ // LangGraph stores nodes in `this.nodes` (a Map or plain object keyed by name).
150
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
151
+ function _extractNodeNames(graph) {
152
+ try {
153
+ const nodes = graph.nodes;
154
+ if (nodes instanceof Map)
155
+ return Array.from(nodes.keys());
156
+ if (nodes && typeof nodes === 'object')
157
+ return Object.keys(nodes);
158
+ }
159
+ catch { /* ignore */ }
160
+ return [];
161
+ }
162
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
163
+ function _mergeCallbacks(config, cb) {
164
+ if (!config)
165
+ return { callbacks: [cb] };
166
+ const existing = Array.isArray(config.callbacks) ? config.callbacks : [];
167
+ return { ...config, callbacks: [...existing, cb] };
168
+ }