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