@mtharrison/loupe 1.3.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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.isTraceEnabled = isTraceEnabled;
4
37
  exports.getLocalLLMTracer = getLocalLLMTracer;
@@ -11,6 +44,7 @@ exports.__resetLocalLLMTracerForTests = __resetLocalLLMTracerForTests;
11
44
  exports.wrapChatModel = wrapChatModel;
12
45
  exports.wrapOpenAIClient = wrapOpenAIClient;
13
46
  const node_async_hooks_1 = require("node:async_hooks");
47
+ const childProcess = __importStar(require("node:child_process"));
14
48
  const server_1 = require("./server");
15
49
  const store_1 = require("./store");
16
50
  const ui_build_1 = require("./ui-build");
@@ -19,7 +53,10 @@ let singleton = null;
19
53
  const DEFAULT_TRACE_PORT = 4319;
20
54
  const activeSpanStorage = new node_async_hooks_1.AsyncLocalStorage();
21
55
  function isTraceEnabled() {
22
- return (0, utils_1.envFlag)('LLM_TRACE_ENABLED');
56
+ if (process.env.LLM_TRACE_ENABLED !== undefined) {
57
+ return (0, utils_1.envFlag)('LLM_TRACE_ENABLED');
58
+ }
59
+ return process.env.NODE_ENV === 'development';
23
60
  }
24
61
  function getLocalLLMTracer(config = {}) {
25
62
  if (!singleton) {
@@ -180,6 +217,7 @@ function wrapOpenAIClient(client, getContext, config) {
180
217
  class LocalLLMTracerImpl {
181
218
  config;
182
219
  loggedUrl;
220
+ openedBrowser;
183
221
  portWasExplicit;
184
222
  server;
185
223
  serverFailed;
@@ -202,6 +240,7 @@ class LocalLLMTracerImpl {
202
240
  this.serverStartPromise = null;
203
241
  this.serverFailed = false;
204
242
  this.loggedUrl = false;
243
+ this.openedBrowser = false;
205
244
  this.uiWatcher = null;
206
245
  }
207
246
  configure(config = {}) {
@@ -278,6 +317,10 @@ class LocalLLMTracerImpl {
278
317
  this.loggedUrl = true;
279
318
  process.stdout.write(`[llm-trace] dashboard: ${this.serverInfo.url}\n`);
280
319
  }
320
+ if (!this.openedBrowser && this.serverInfo && shouldAutoOpenDashboard()) {
321
+ this.openedBrowser = true;
322
+ openBrowser(this.serverInfo.url);
323
+ }
281
324
  return this.serverInfo;
282
325
  }
283
326
  catch (error) {
@@ -292,6 +335,37 @@ class LocalLLMTracerImpl {
292
335
  return this.serverStartPromise;
293
336
  }
294
337
  }
338
+ function shouldAutoOpenDashboard() {
339
+ if (process.env.LOUPE_OPEN_BROWSER === '0') {
340
+ return false;
341
+ }
342
+ return (process.env.NODE_ENV === 'development'
343
+ && !process.env.CI
344
+ && !!process.stdout.isTTY);
345
+ }
346
+ function openBrowser(url) {
347
+ const command = process.platform === 'darwin'
348
+ ? ['open', [url]]
349
+ : process.platform === 'win32'
350
+ ? ['cmd', ['/c', 'start', '', url]]
351
+ : process.platform === 'linux'
352
+ ? ['xdg-open', [url]]
353
+ : null;
354
+ if (!command) {
355
+ return;
356
+ }
357
+ try {
358
+ const child = childProcess.spawn(command[0], command[1], {
359
+ detached: true,
360
+ stdio: 'ignore',
361
+ });
362
+ child.on('error', () => { });
363
+ child.unref();
364
+ }
365
+ catch (_error) {
366
+ // Ignore browser launch failures. The dashboard URL is already printed.
367
+ }
368
+ }
295
369
  function normaliseRequest(request) {
296
370
  return {
297
371
  input: (0, utils_1.safeClone)(request?.input),
@@ -41,4 +41,4 @@ export declare function findSessionNodePath(nodes: SessionNavHierarchyNode[], id
41
41
  export declare function findSessionNodeById(nodes: SessionNavHierarchyNode[], id: string): SessionNavHierarchyNode | null;
42
42
  export declare function getNewestTraceIdForNode(node: SessionNavHierarchyNode | null | undefined): string | null;
43
43
  export declare function resolveSessionTreeSelection(sessionNodes: SessionNavHierarchyNode[], selectedNodeId: string | null, selectedTraceId: string | null): SessionTreeSelection;
44
- export declare function getDefaultExpandedSessionTreeNodeIds(sessionNodes: SessionNavHierarchyNode[], activeSessionId: string | null, selectedNodeId: string | null): Set<string>;
44
+ export declare function getDefaultExpandedSessionTreeNodeIds(sessionNodes: SessionNavHierarchyNode[], activeSessionId: string | null, selectedNodeId: string | null, selectedTraceId?: string | null): Set<string>;
@@ -66,7 +66,7 @@ function resolveSessionTreeSelection(sessionNodes, selectedNodeId, selectedTrace
66
66
  selectedTraceId: nextSelectedTraceId,
67
67
  };
68
68
  }
69
- function getDefaultExpandedSessionTreeNodeIds(sessionNodes, activeSessionId, selectedNodeId) {
69
+ function getDefaultExpandedSessionTreeNodeIds(sessionNodes, activeSessionId, selectedNodeId, selectedTraceId = null) {
70
70
  const expanded = new Set();
71
71
  const activeSession = (activeSessionId
72
72
  ? sessionNodes.find((node) => node.id === activeSessionId) ?? null
@@ -89,6 +89,13 @@ function getDefaultExpandedSessionTreeNodeIds(sessionNodes, activeSessionId, sel
89
89
  }
90
90
  }
91
91
  }
92
+ if (selectedTraceId) {
93
+ for (const node of findSessionNodePath([activeSession], `trace:${selectedTraceId}`)) {
94
+ if (node.children.length) {
95
+ expanded.add(node.id);
96
+ }
97
+ }
98
+ }
92
99
  return expanded;
93
100
  }
94
101
  function deriveSessionNavItem(node, traceById) {
package/dist/store.d.ts CHANGED
@@ -17,6 +17,7 @@ export declare class TraceStore extends EventEmitter {
17
17
  clear(): void;
18
18
  hierarchy(filters?: TraceFilters): HierarchyResponse;
19
19
  private recordStart;
20
+ private findTraceBySpanReference;
20
21
  private evictIfNeeded;
21
22
  private cloneTrace;
22
23
  private filteredTraces;
package/dist/store.js CHANGED
@@ -141,51 +141,49 @@ class TraceStore extends node_events_1.EventEmitter {
141
141
  };
142
142
  }
143
143
  const roots = new Map();
144
+ const traceBySpanId = new Map();
145
+ const traceNodeByTraceId = new Map();
146
+ const traceSessionByTraceId = new Map();
147
+ const parentNodeById = new Map();
144
148
  for (const trace of traces) {
145
- const sessionId = trace.hierarchy.sessionId || 'unknown-session';
149
+ const sessionId = getTraceSessionId(trace);
146
150
  const sessionNode = getOrCreateNode(roots, `session:${sessionId}`, 'session', `Session ${sessionId}`, {
147
151
  sessionId,
148
152
  chatId: trace.hierarchy.chatId,
149
- });
150
- const lineage = [sessionNode];
151
- const rootActorId = trace.hierarchy.rootActorId || 'unknown-actor';
152
- const actorNode = getOrCreateNode(sessionNode.children, `actor:${sessionId}:${rootActorId}`, 'actor', rootActorId, {
153
- actorId: rootActorId,
154
- rootActorId,
155
- sessionId,
153
+ rootActorId: trace.hierarchy.rootActorId,
156
154
  topLevelAgentId: trace.hierarchy.topLevelAgentId,
157
155
  });
158
- lineage.push(actorNode);
159
- let currentNode = actorNode;
160
- if (trace.hierarchy.kind === 'guardrail') {
161
- const label = `${trace.hierarchy.guardrailPhase || 'guardrail'} guardrail`;
162
- currentNode = getOrCreateNode(currentNode.children, `guardrail:${sessionId}:${rootActorId}:${trace.context.guardrailType || label}`, 'guardrail', label, {
163
- guardrailPhase: trace.hierarchy.guardrailPhase || null,
164
- guardrailType: trace.context.guardrailType || null,
165
- systemType: trace.context.systemType || null,
166
- watchdogPhase: trace.hierarchy.watchdogPhase || null,
167
- });
168
- lineage.push(currentNode);
169
- }
170
- else if (trace.hierarchy.childActorId) {
171
- currentNode = getOrCreateNode(currentNode.children, `child-actor:${sessionId}:${rootActorId}:${trace.hierarchy.childActorId}`, 'child-actor', trace.hierarchy.childActorId, {
172
- actorId: trace.hierarchy.childActorId,
173
- childActorId: trace.hierarchy.childActorId,
174
- delegatedAgentId: trace.hierarchy.delegatedAgentId,
175
- });
176
- lineage.push(currentNode);
156
+ const traceNode = createTraceNode(trace);
157
+ traceBySpanId.set(trace.spanContext.spanId, trace);
158
+ traceNodeByTraceId.set(trace.id, traceNode);
159
+ traceSessionByTraceId.set(trace.id, sessionId);
160
+ }
161
+ for (const trace of traces) {
162
+ const sessionId = traceSessionByTraceId.get(trace.id) || 'unknown-session';
163
+ const sessionNode = roots.get(`session:${sessionId}`);
164
+ const traceNode = traceNodeByTraceId.get(trace.id);
165
+ if (!sessionNode || !traceNode) {
166
+ continue;
177
167
  }
178
- if (trace.hierarchy.stage) {
179
- currentNode = getOrCreateNode(currentNode.children, `stage:${sessionId}:${rootActorId}:${trace.hierarchy.childActorId || 'root'}:${trace.hierarchy.stage}`, 'stage', trace.hierarchy.stage, {
180
- stage: trace.hierarchy.stage,
181
- workflowState: trace.hierarchy.workflowState,
182
- });
183
- lineage.push(currentNode);
168
+ const parentTrace = trace.parentSpanId ? traceBySpanId.get(trace.parentSpanId) : null;
169
+ const parentTraceNode = parentTrace &&
170
+ traceSessionByTraceId.get(parentTrace.id) === sessionId &&
171
+ parentTrace.id !== trace.id
172
+ ? traceNodeByTraceId.get(parentTrace.id) || null
173
+ : null;
174
+ parentNodeById.set(traceNode.id, parentTraceNode || sessionNode);
175
+ }
176
+ for (const trace of traces) {
177
+ const traceNode = traceNodeByTraceId.get(trace.id);
178
+ const parentNode = traceNode ? parentNodeById.get(traceNode.id) || null : null;
179
+ if (!traceNode || !parentNode) {
180
+ continue;
184
181
  }
185
- const traceNode = createTraceNode(trace);
186
- currentNode.children.set(traceNode.id, traceNode);
187
- for (const node of new Set(lineage)) {
188
- applyTraceRollup(node, trace);
182
+ parentNode.children.set(traceNode.id, traceNode);
183
+ let currentNode = parentNode;
184
+ while (currentNode) {
185
+ applyTraceRollup(currentNode, trace);
186
+ currentNode = parentNodeById.get(currentNode.id) || null;
189
187
  }
190
188
  }
191
189
  return {
@@ -195,9 +193,9 @@ class TraceStore extends node_events_1.EventEmitter {
195
193
  };
196
194
  }
197
195
  recordStart(mode, context, request, options = {}) {
198
- const traceContext = (0, utils_1.normalizeTraceContext)(context, mode);
196
+ const traceContext = applyConversationIdToContext((0, utils_1.normalizeTraceContext)(context, mode), options.attributes);
199
197
  const traceId = randomId();
200
- const parentSpan = options.parentSpanId ? this.traces.get(options.parentSpanId) : null;
198
+ const parentSpan = this.findTraceBySpanReference(options.parentSpanId);
201
199
  const startedAt = new Date().toISOString();
202
200
  const trace = {
203
201
  attributes: buildSpanAttributes(traceContext, mode, request, options.attributes),
@@ -253,6 +251,21 @@ class TraceStore extends node_events_1.EventEmitter {
253
251
  this.publish('span:start', traceId, { trace: this.cloneTrace(trace) });
254
252
  return traceId;
255
253
  }
254
+ findTraceBySpanReference(spanReference) {
255
+ if (!spanReference) {
256
+ return null;
257
+ }
258
+ const byTraceId = this.traces.get(spanReference);
259
+ if (byTraceId) {
260
+ return byTraceId;
261
+ }
262
+ for (const trace of this.traces.values()) {
263
+ if (trace.spanContext.spanId === spanReference) {
264
+ return trace;
265
+ }
266
+ }
267
+ return null;
268
+ }
256
269
  evictIfNeeded() {
257
270
  while (this.order.length > this.maxTraces) {
258
271
  const oldest = this.order.shift();
@@ -420,6 +433,35 @@ function buildGroupHierarchy(traces, groupBy) {
420
433
  }
421
434
  return [...groups.values()].map(serialiseNode);
422
435
  }
436
+ function getTraceSessionId(trace) {
437
+ const conversationId = toNonEmptyString(trace.attributes?.['gen_ai.conversation.id']);
438
+ return conversationId || trace.hierarchy.sessionId || 'unknown-session';
439
+ }
440
+ function applyConversationIdToContext(context, extraAttributes) {
441
+ const conversationId = toNonEmptyString(extraAttributes?.['gen_ai.conversation.id']);
442
+ if (!conversationId || conversationId === context.sessionId) {
443
+ return context;
444
+ }
445
+ return {
446
+ ...context,
447
+ sessionId: conversationId,
448
+ chatId: conversationId,
449
+ rootSessionId: context.rootSessionId || conversationId,
450
+ rootChatId: context.rootChatId || conversationId,
451
+ tags: {
452
+ ...context.tags,
453
+ sessionId: conversationId,
454
+ chatId: conversationId,
455
+ rootSessionId: context.rootSessionId || conversationId,
456
+ rootChatId: context.rootChatId || conversationId,
457
+ },
458
+ hierarchy: {
459
+ ...context.hierarchy,
460
+ sessionId: conversationId,
461
+ chatId: conversationId,
462
+ },
463
+ };
464
+ }
423
465
  function randomId() {
424
466
  return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
425
467
  }
@@ -469,6 +511,9 @@ function getDefaultSpanName(context, mode) {
469
511
  const prefix = context.provider || 'llm';
470
512
  return `${prefix}.${mode}`;
471
513
  }
514
+ function toNonEmptyString(value) {
515
+ return typeof value === 'string' && value.trim() ? value : null;
516
+ }
472
517
  function buildSpanAttributes(context, mode, request, extraAttributes) {
473
518
  const base = {
474
519
  'gen_ai.conversation.id': context.sessionId || undefined,
package/dist/utils.js CHANGED
@@ -134,7 +134,9 @@ function normalizeTraceContext(context, mode) {
134
134
  const explicitKind = normalizeKind(raw.kind);
135
135
  let kind = explicitKind || 'actor';
136
136
  if (!explicitKind) {
137
- if (guardrailType || guardrailPhase) {
137
+ // Keep generic guardrail metadata from masking actual delegated-agent spans.
138
+ // Explicit guardrail spans or spans with a concrete phase still remain guardrails.
139
+ if (guardrailPhase || (guardrailType && !isChildActor)) {
138
140
  kind = 'guardrail';
139
141
  }
140
142
  else if (stage) {