@mtharrison/loupe 1.1.0 → 1.2.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.
@@ -0,0 +1,223 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.deriveSessionNavItems = deriveSessionNavItems;
4
+ exports.sortSessionNodesForNav = sortSessionNodesForNav;
5
+ exports.findSessionNodePath = findSessionNodePath;
6
+ exports.findSessionNodeById = findSessionNodeById;
7
+ exports.getNewestTraceIdForNode = getNewestTraceIdForNode;
8
+ exports.resolveSessionTreeSelection = resolveSessionTreeSelection;
9
+ exports.getDefaultExpandedSessionTreeNodeIds = getDefaultExpandedSessionTreeNodeIds;
10
+ function deriveSessionNavItems(sessionNodes, traceById) {
11
+ return sessionNodes
12
+ .map((node) => deriveSessionNavItem(node, traceById))
13
+ .sort(compareSessionNavItems);
14
+ }
15
+ function sortSessionNodesForNav(sessionNodes, traceById) {
16
+ const itemById = new Map(sessionNodes.map((node) => [node.id, deriveSessionNavItem(node, traceById)]));
17
+ return sessionNodes
18
+ .slice()
19
+ .sort((left, right) => compareSessionNavItems(itemById.get(left.id), itemById.get(right.id)));
20
+ }
21
+ function findSessionNodePath(nodes, id, trail = []) {
22
+ for (const node of nodes) {
23
+ const nextTrail = [...trail, node];
24
+ if (node.id === id) {
25
+ return nextTrail;
26
+ }
27
+ const childTrail = findSessionNodePath(node.children, id, nextTrail);
28
+ if (childTrail.length) {
29
+ return childTrail;
30
+ }
31
+ }
32
+ return [];
33
+ }
34
+ function findSessionNodeById(nodes, id) {
35
+ return findSessionNodePath(nodes, id).at(-1) ?? null;
36
+ }
37
+ function getNewestTraceIdForNode(node) {
38
+ if (!node?.traceIds.length) {
39
+ return null;
40
+ }
41
+ if (typeof node.meta?.traceId === "string" && node.meta.traceId) {
42
+ return node.meta.traceId;
43
+ }
44
+ return node.traceIds[0] || null;
45
+ }
46
+ function resolveSessionTreeSelection(sessionNodes, selectedNodeId, selectedTraceId) {
47
+ const selectedNode = selectedNodeId
48
+ ? findSessionNodeById(sessionNodes, selectedNodeId)
49
+ : null;
50
+ const selectedTraceNode = selectedTraceId
51
+ ? findSessionNodeById(sessionNodes, `trace:${selectedTraceId}`)
52
+ : null;
53
+ const fallbackNode = selectedNode ?? selectedTraceNode ?? sessionNodes[0] ?? null;
54
+ if (!fallbackNode) {
55
+ return {
56
+ selectedNodeId: null,
57
+ selectedTraceId: null,
58
+ };
59
+ }
60
+ const nextSelectedNodeId = selectedNode?.id ?? fallbackNode.id;
61
+ const nextSelectedTraceId = selectedTraceId && fallbackNode.traceIds.includes(selectedTraceId)
62
+ ? selectedTraceId
63
+ : getNewestTraceIdForNode(fallbackNode);
64
+ return {
65
+ selectedNodeId: nextSelectedNodeId,
66
+ selectedTraceId: nextSelectedTraceId,
67
+ };
68
+ }
69
+ function getDefaultExpandedSessionTreeNodeIds(sessionNodes, activeSessionId, selectedNodeId) {
70
+ const expanded = new Set();
71
+ const activeSession = (activeSessionId
72
+ ? sessionNodes.find((node) => node.id === activeSessionId) ?? null
73
+ : null) ?? sessionNodes[0] ?? null;
74
+ if (!activeSession) {
75
+ return expanded;
76
+ }
77
+ if (activeSession.children.length) {
78
+ expanded.add(activeSession.id);
79
+ }
80
+ visitSessionTree(activeSession.children, (node) => {
81
+ if (node.children.length && node.type === "actor") {
82
+ expanded.add(node.id);
83
+ }
84
+ });
85
+ if (selectedNodeId) {
86
+ for (const node of findSessionNodePath([activeSession], selectedNodeId)) {
87
+ if (node.children.length) {
88
+ expanded.add(node.id);
89
+ }
90
+ }
91
+ }
92
+ return expanded;
93
+ }
94
+ function deriveSessionNavItem(node, traceById) {
95
+ const traces = node.traceIds
96
+ .map((traceId) => traceById.get(traceId))
97
+ .filter((trace) => Boolean(trace));
98
+ const latestTrace = getLatestTrace(traces);
99
+ const sessionId = getSessionId(node) ?? latestTrace?.hierarchy.sessionId ?? "unknown";
100
+ const shortSessionId = shortId(sessionId);
101
+ const primaryActor = getPrimaryActor(node) ?? latestTrace?.hierarchy.rootActorId ?? shortSessionId;
102
+ return {
103
+ callCount: node.count,
104
+ costUsd: getCostUsd(node, traces),
105
+ hasHighlights: traces.some((trace) => Boolean(trace.flags?.hasHighlights)),
106
+ id: node.id,
107
+ latestStartedAt: latestTrace?.startedAt ?? null,
108
+ latestTimestamp: latestTrace?.startedAt
109
+ ? formatCompactTimestamp(latestTrace.startedAt)
110
+ : null,
111
+ primaryActor,
112
+ primaryLabel: primaryActor,
113
+ shortSessionId,
114
+ status: getAggregateStatus(traces),
115
+ };
116
+ }
117
+ function getCostUsd(node, traces) {
118
+ if (typeof node.meta?.costUsd === "number" &&
119
+ Number.isFinite(node.meta.costUsd)) {
120
+ return roundCostUsd(node.meta.costUsd);
121
+ }
122
+ const traceCosts = traces
123
+ .map((trace) => trace.costUsd)
124
+ .filter((value) => typeof value === "number" && Number.isFinite(value));
125
+ if (!traceCosts.length) {
126
+ return null;
127
+ }
128
+ return roundCostUsd(traceCosts.reduce((sum, value) => sum + value, 0));
129
+ }
130
+ function getLatestTrace(traces) {
131
+ let latestTrace = null;
132
+ let latestTimestamp = Number.NEGATIVE_INFINITY;
133
+ for (const trace of traces) {
134
+ const nextTimestamp = Date.parse(trace.startedAt);
135
+ if (!Number.isFinite(nextTimestamp)) {
136
+ continue;
137
+ }
138
+ if (nextTimestamp > latestTimestamp) {
139
+ latestTrace = trace;
140
+ latestTimestamp = nextTimestamp;
141
+ }
142
+ }
143
+ return latestTrace;
144
+ }
145
+ function getAggregateStatus(traces) {
146
+ if (traces.some((trace) => trace.status === "error")) {
147
+ return "error";
148
+ }
149
+ if (traces.some((trace) => trace.status === "pending")) {
150
+ return "pending";
151
+ }
152
+ return "ok";
153
+ }
154
+ function getSessionId(node) {
155
+ if (typeof node.meta?.sessionId === "string" && node.meta.sessionId) {
156
+ return node.meta.sessionId;
157
+ }
158
+ if (node.id.startsWith("session:")) {
159
+ return node.id.slice("session:".length);
160
+ }
161
+ return null;
162
+ }
163
+ function getPrimaryActor(node) {
164
+ const actorNode = node.children.find((child) => child.type === "actor");
165
+ return (actorNode?.meta?.actorId ??
166
+ actorNode?.meta?.rootActorId ??
167
+ node.meta?.rootActorId ??
168
+ null);
169
+ }
170
+ function compareSessionNavItems(left, right) {
171
+ const timestampDelta = toSortableTimestamp(right.latestStartedAt) -
172
+ toSortableTimestamp(left.latestStartedAt);
173
+ if (timestampDelta !== 0) {
174
+ return timestampDelta;
175
+ }
176
+ const statusDelta = getStatusRank(left.status) - getStatusRank(right.status);
177
+ if (statusDelta !== 0) {
178
+ return statusDelta;
179
+ }
180
+ const costDelta = (right.costUsd ?? 0) - (left.costUsd ?? 0);
181
+ if (costDelta !== 0) {
182
+ return costDelta;
183
+ }
184
+ return left.primaryLabel.localeCompare(right.primaryLabel);
185
+ }
186
+ function getStatusRank(status) {
187
+ switch (status) {
188
+ case "error":
189
+ return 0;
190
+ case "pending":
191
+ return 1;
192
+ default:
193
+ return 2;
194
+ }
195
+ }
196
+ function toSortableTimestamp(value) {
197
+ if (!value) {
198
+ return Number.NEGATIVE_INFINITY;
199
+ }
200
+ const timestamp = Date.parse(value);
201
+ return Number.isFinite(timestamp) ? timestamp : Number.NEGATIVE_INFINITY;
202
+ }
203
+ function roundCostUsd(value) {
204
+ return Math.round(value * 1e12) / 1e12;
205
+ }
206
+ function shortId(value) {
207
+ if (!value) {
208
+ return "unknown";
209
+ }
210
+ return value.length > 8 ? value.slice(0, 8) : value;
211
+ }
212
+ function formatCompactTimestamp(value) {
213
+ return new Date(value).toLocaleTimeString([], {
214
+ hour: "2-digit",
215
+ minute: "2-digit",
216
+ });
217
+ }
218
+ function visitSessionTree(nodes, visitor) {
219
+ for (const node of nodes) {
220
+ visitor(node);
221
+ visitSessionTree(node.children, visitor);
222
+ }
223
+ }
package/dist/store.js CHANGED
@@ -220,7 +220,10 @@ class TraceStore extends node_events_1.EventEmitter {
220
220
  }
221
221
  }
222
222
  cloneTrace(trace) {
223
- return (0, utils_1.safeClone)(trace);
223
+ return {
224
+ ...(0, utils_1.safeClone)(trace),
225
+ insights: (0, utils_1.getTraceInsights)(trace),
226
+ };
224
227
  }
225
228
  filteredTraces(filters = {}) {
226
229
  const tagFilters = normalizeTagFilters(filters.tags || filters.tagFilters);
package/dist/types.d.ts CHANGED
@@ -79,6 +79,27 @@ export type TraceRequest = {
79
79
  input?: Record<string, any>;
80
80
  options?: Record<string, any>;
81
81
  };
82
+ export type TraceStructuredInputInsight = {
83
+ format: 'xml';
84
+ role: string;
85
+ snippet: string;
86
+ tags: string[];
87
+ };
88
+ export type TraceHighlightInsight = {
89
+ description: string;
90
+ kind: string;
91
+ snippet: string;
92
+ source: string;
93
+ title: string;
94
+ };
95
+ export type TraceInsights = {
96
+ highlights: TraceHighlightInsight[];
97
+ structuredInputs: TraceStructuredInputInsight[];
98
+ };
99
+ export type TraceSummaryFlags = {
100
+ hasHighlights: boolean;
101
+ hasStructuredInput: boolean;
102
+ };
82
103
  export type TraceRecord = {
83
104
  context: NormalizedTraceContext;
84
105
  endedAt: string | null;
@@ -106,6 +127,7 @@ export type TraceRecord = {
106
127
  usage: Record<string, any> | null;
107
128
  };
108
129
  };
130
+ insights?: TraceInsights;
109
131
  tags: TraceTags;
110
132
  usage: Record<string, any> | null;
111
133
  };
@@ -113,6 +135,7 @@ export type TraceSummary = {
113
135
  costUsd: number | null;
114
136
  durationMs: number | null;
115
137
  endedAt: string | null;
138
+ flags?: TraceSummaryFlags;
116
139
  hierarchy: TraceHierarchy;
117
140
  id: string;
118
141
  kind: string;
@@ -179,6 +202,24 @@ export interface ChatModelLike<TInput = any, TOptions = any, TValue = any, TChun
179
202
  invoke(input: TInput, options?: TOptions): Promise<TValue>;
180
203
  stream(input: TInput, options?: TOptions): AsyncGenerator<TChunk>;
181
204
  }
205
+ export type OpenAIChatCompletionCreateParamsLike = Record<string, any> & {
206
+ messages?: Record<string, any>[];
207
+ model?: string | null;
208
+ stream?: boolean | null;
209
+ };
210
+ export interface OpenAIChatCompletionStreamLike<TChunk = any> extends AsyncIterable<TChunk> {
211
+ [Symbol.asyncIterator](): AsyncIterator<TChunk>;
212
+ }
213
+ export interface OpenAIChatCompletionsLike<TParams = OpenAIChatCompletionCreateParamsLike, TOptions = Record<string, any>, TResponse = any, TChunk = any> {
214
+ create(params: TParams, options?: TOptions): Promise<TResponse> | Promise<OpenAIChatCompletionStreamLike<TChunk>> | OpenAIChatCompletionStreamLike<TChunk>;
215
+ }
216
+ export interface OpenAIClientLike<TParams = OpenAIChatCompletionCreateParamsLike, TOptions = Record<string, any>, TResponse = any, TChunk = any> {
217
+ chat: {
218
+ completions: OpenAIChatCompletionsLike<TParams, TOptions, TResponse, TChunk>;
219
+ [key: string]: any;
220
+ };
221
+ [key: string]: any;
222
+ }
182
223
  export type TraceServer = {
183
224
  broadcast(event: TraceEvent | UIReloadEvent): void;
184
225
  close(): void;
package/dist/ui-build.js CHANGED
@@ -24,7 +24,7 @@ function createBuildOptions() {
24
24
  loader: {
25
25
  '.svg': 'dataurl',
26
26
  },
27
- splitting: false,
27
+ splitting: true,
28
28
  minify: false,
29
29
  sourcemap: false,
30
30
  logLevel: 'silent',
package/dist/utils.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type NormalizedTraceContext, type TraceContext, type TraceMode, type TraceRecord, type TraceSummary } from './types';
1
+ import { type NormalizedTraceContext, type TraceContext, type TraceInsights, type TraceMode, type TraceRecord, type TraceSummary } from './types';
2
2
  export declare function safeClone<T>(value: T): T;
3
3
  export declare function toErrorPayload(error: any): Record<string, any> | null;
4
4
  export declare function sanitizeHeaders(headers: Record<string, any> | undefined): Record<string, any>;
@@ -7,4 +7,5 @@ export declare function stringifyRecord(record: Record<string, any>): Record<str
7
7
  export declare function normalizeTraceContext(context: TraceContext | undefined, mode: TraceMode): NormalizedTraceContext;
8
8
  export declare function summariseValue(value: unknown, maxLength?: number): string;
9
9
  export declare function getUsageCostUsd(usage: Record<string, any> | null | undefined): number | null;
10
+ export declare function getTraceInsights(trace: TraceRecord): TraceInsights;
10
11
  export declare function toSummary(trace: TraceRecord): TraceSummary;
package/dist/utils.js CHANGED
@@ -8,6 +8,7 @@ exports.stringifyRecord = stringifyRecord;
8
8
  exports.normalizeTraceContext = normalizeTraceContext;
9
9
  exports.summariseValue = summariseValue;
10
10
  exports.getUsageCostUsd = getUsageCostUsd;
11
+ exports.getTraceInsights = getTraceInsights;
11
12
  exports.toSummary = toSummary;
12
13
  function safeClone(value) {
13
14
  if (value === undefined) {
@@ -253,12 +254,84 @@ function extractResponsePreview(trace) {
253
254
  }
254
255
  return '';
255
256
  }
257
+ function getTraceInsights(trace) {
258
+ const structuredInputs = [];
259
+ const highlights = [];
260
+ const seenStructured = new Set();
261
+ const seenHighlights = new Set();
262
+ for (const message of collectTraceMessages(trace)) {
263
+ const text = toInsightText(message.content);
264
+ if (!text) {
265
+ continue;
266
+ }
267
+ const structuredMarkup = detectStructuredMarkup(text);
268
+ if (structuredMarkup) {
269
+ const structuredKey = `${message.source}:${structuredMarkup.tags.join('|')}:${structuredMarkup.snippet}`;
270
+ if (!seenStructured.has(structuredKey)) {
271
+ structuredInputs.push({
272
+ format: 'xml',
273
+ role: message.role,
274
+ snippet: structuredMarkup.snippet,
275
+ tags: structuredMarkup.tags,
276
+ });
277
+ seenStructured.add(structuredKey);
278
+ }
279
+ const highlightKey = `structured:${message.source}:${structuredMarkup.tags.join('|')}`;
280
+ if (!seenHighlights.has(highlightKey)) {
281
+ highlights.push({
282
+ kind: 'structured-input',
283
+ title: `Structured ${message.role} input`,
284
+ description: `Contains XML-like markup (${structuredMarkup.tags.slice(0, 3).join(', ')}) that may influence guardrail behavior.`,
285
+ source: message.source,
286
+ snippet: structuredMarkup.snippet,
287
+ });
288
+ seenHighlights.add(highlightKey);
289
+ }
290
+ }
291
+ if ((message.role === 'user' || message.role === 'system') && looksLikeLongPrompt(text)) {
292
+ const longKey = `long:${message.source}`;
293
+ if (!seenHighlights.has(longKey)) {
294
+ highlights.push({
295
+ kind: 'long-message',
296
+ title: `Long ${message.role} message`,
297
+ description: 'Large prompt payloads often hide embedded instructions, policies, or contextual data worth inspecting.',
298
+ source: message.source,
299
+ snippet: createSnippet(text, 240),
300
+ });
301
+ seenHighlights.add(longKey);
302
+ }
303
+ }
304
+ }
305
+ if (trace.kind === 'guardrail') {
306
+ const contextSummary = [trace.context.guardrailPhase, trace.context.guardrailType, trace.context.systemType]
307
+ .filter(Boolean)
308
+ .join(' / ');
309
+ if (contextSummary) {
310
+ highlights.push({
311
+ kind: 'guardrail-context',
312
+ title: `${capitalize(trace.context.guardrailPhase || 'guardrail')} guardrail context`,
313
+ description: `This trace ran inside ${contextSummary}.`,
314
+ source: 'trace:guardrail',
315
+ snippet: createSnippet(contextSummary, 180),
316
+ });
317
+ }
318
+ }
319
+ return {
320
+ structuredInputs,
321
+ highlights,
322
+ };
323
+ }
256
324
  function toSummary(trace) {
257
325
  const durationMs = trace.endedAt ? Math.max(0, Date.parse(trace.endedAt) - Date.parse(trace.startedAt)) : null;
326
+ const insights = trace.insights || getTraceInsights(trace);
258
327
  return {
259
328
  costUsd: getUsageCostUsd(trace.usage),
260
329
  durationMs,
261
330
  endedAt: trace.endedAt,
331
+ flags: {
332
+ hasHighlights: insights.highlights.length > 0,
333
+ hasStructuredInput: insights.structuredInputs.length > 0,
334
+ },
262
335
  hierarchy: safeClone(trace.hierarchy),
263
336
  id: trace.id,
264
337
  kind: trace.kind,
@@ -278,3 +351,103 @@ function toSummary(trace) {
278
351
  tags: safeClone(trace.tags),
279
352
  };
280
353
  }
354
+ function collectTraceMessages(trace) {
355
+ const entries = [];
356
+ const requestMessages = trace.request?.input?.messages;
357
+ if (Array.isArray(requestMessages)) {
358
+ for (const [index, message] of requestMessages.entries()) {
359
+ entries.push({
360
+ content: message?.content,
361
+ role: typeof message?.role === 'string' ? message.role : 'unknown',
362
+ source: `request:${index}`,
363
+ });
364
+ }
365
+ }
366
+ const responseMessage = trace.response?.message || trace.stream?.reconstructed?.message;
367
+ if (responseMessage?.content !== undefined) {
368
+ entries.push({
369
+ content: responseMessage.content,
370
+ role: typeof responseMessage.role === 'string' ? responseMessage.role : 'assistant',
371
+ source: 'response:0',
372
+ });
373
+ }
374
+ return entries;
375
+ }
376
+ function toInsightText(content) {
377
+ if (typeof content === 'string') {
378
+ const trimmed = content.trim();
379
+ return trimmed ? trimmed : null;
380
+ }
381
+ if (Array.isArray(content)) {
382
+ const parts = content
383
+ .map((item) => {
384
+ if (typeof item === 'string') {
385
+ return item;
386
+ }
387
+ if (typeof item?.text === 'string') {
388
+ return item.text;
389
+ }
390
+ if (typeof item?.text?.value === 'string') {
391
+ return item.text.value;
392
+ }
393
+ if (typeof item?.content === 'string') {
394
+ return item.content;
395
+ }
396
+ return '';
397
+ })
398
+ .filter(Boolean);
399
+ return parts.length ? parts.join('\n\n').trim() : null;
400
+ }
401
+ if (typeof content?.text === 'string') {
402
+ return content.text.trim() || null;
403
+ }
404
+ if (typeof content?.text?.value === 'string') {
405
+ return content.text.value.trim() || null;
406
+ }
407
+ if (typeof content?.content === 'string') {
408
+ return content.content.trim() || null;
409
+ }
410
+ return null;
411
+ }
412
+ function detectStructuredMarkup(text) {
413
+ const tagRegex = /<\/?([A-Za-z][\w:-]*)\b[^>]*>/g;
414
+ const tagNames = [];
415
+ let match = tagRegex.exec(text);
416
+ while (match) {
417
+ tagNames.push(match[1].toLowerCase());
418
+ match = tagRegex.exec(text);
419
+ }
420
+ const uniqueTags = [...new Set(tagNames)];
421
+ if (!uniqueTags.length) {
422
+ return null;
423
+ }
424
+ const hasInstructionalTag = uniqueTags.some((tag) => ['assistant', 'instruction', 'option', 'policy', 'system', 'user'].includes(tag));
425
+ const hasMultipleTags = uniqueTags.length >= 2;
426
+ const hasClosingTag = /<\/[A-Za-z][\w:-]*>/.test(text);
427
+ if (!hasClosingTag && !hasInstructionalTag && !hasMultipleTags) {
428
+ return null;
429
+ }
430
+ return {
431
+ snippet: createSnippet(text, 220),
432
+ tags: uniqueTags.slice(0, 6),
433
+ };
434
+ }
435
+ function looksLikeLongPrompt(text) {
436
+ return text.length >= 1200 || countLines(text) >= 20;
437
+ }
438
+ function countLines(text) {
439
+ return text.split(/\r?\n/).length;
440
+ }
441
+ function createSnippet(text, maxLength) {
442
+ const trimmed = text.trim().replace(/\s+/g, ' ');
443
+ if (trimmed.length <= maxLength) {
444
+ return trimmed;
445
+ }
446
+ return `${trimmed.slice(0, maxLength - 1)}...`;
447
+ }
448
+ function capitalize(value) {
449
+ if (!value) {
450
+ return '';
451
+ }
452
+ return `${value.charAt(0).toUpperCase()}${value.slice(1)}`;
453
+ }