@mtharrison/loupe 1.4.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/README.md CHANGED
@@ -40,16 +40,18 @@ npm install @mtharrison/loupe
40
40
 
41
41
  ### Requirements
42
42
 
43
- - Node.js 18 or newer
43
+ - Node.js 20 or newer
44
44
 
45
45
  ## Quick Start
46
46
 
47
- Enable tracing:
47
+ Enable tracing explicitly:
48
48
 
49
49
  ```bash
50
50
  export LLM_TRACE_ENABLED=1
51
51
  ```
52
52
 
53
+ Or just run your app with `NODE_ENV=development`. Loupe now enables tracing implicitly in development and opens the dashboard automatically on first start in an interactive local terminal.
54
+
53
55
  If your app already uses a higher-level model interface or the official OpenAI client, Loupe can wrap that directly instead of requiring manual `record*` calls.
54
56
 
55
57
  ### `wrapOpenAIClient(client, getContext, config?)`
@@ -95,6 +97,8 @@ When the server starts, Loupe prints the local URL:
95
97
  [llm-trace] dashboard: http://127.0.0.1:4319
96
98
  ```
97
99
 
100
+ In `NODE_ENV=development`, Loupe also opens that URL in your browser automatically unless `CI` is set, the terminal is non-interactive, or `LOUPE_OPEN_BROWSER=0`.
101
+
98
102
  If `4319` is already in use and you did not explicitly configure a port, Loupe falls back to another free local port and prints that URL instead.
99
103
 
100
104
  `wrapOpenAIClient()` is structurally typed, so Loupe's runtime API does not require the OpenAI SDK for normal library usage. The repo includes `openai` as a dev dependency for the bundled demo; if your own app instantiates `new OpenAI()` or runs the published example from a consumer install, install `openai` there too.
@@ -144,6 +148,25 @@ export LLM_TRACE_ENABLED=1
144
148
  node examples/nested-tool-call.js
145
149
  ```
146
150
 
151
+ ### Runnable Fully Featured Demo
152
+
153
+ `examples/fully-featured.js` is a credential-free demo that combines the main local tracing features in one session:
154
+
155
+ - a top-level input guardrail span recorded with the low-level lifecycle API
156
+ - a wrapped `invoke()` call with a nested stage span and child actor span
157
+ - a handled child-span error so the dashboard shows both success and failure states
158
+ - a wrapped `stream()` call with reconstructed output and usage
159
+
160
+ Run it with:
161
+
162
+ ```bash
163
+ npm install
164
+ export LLM_TRACE_ENABLED=1
165
+ node examples/fully-featured.js
166
+ ```
167
+
168
+ Supported demo environment variables: `LLM_TRACE_PORT`, `LOUPE_OPEN_BROWSER`.
169
+
147
170
  ## Low-Level Lifecycle API
148
171
 
149
172
  If you need full control over trace boundaries, Loupe exposes a lower-level span lifecycle API modeled on OpenTelemetry concepts: start a span, add events, end it, and record exceptions.
@@ -345,11 +368,12 @@ Environment variables:
345
368
 
346
369
  | Variable | Default | Description |
347
370
  | --- | --- | --- |
348
- | `LLM_TRACE_ENABLED` | `false` | Enables Loupe. |
371
+ | `LLM_TRACE_ENABLED` | `true` when `NODE_ENV=development`, otherwise `false` | Enables Loupe. |
349
372
  | `LLM_TRACE_HOST` | `127.0.0.1` | Host for the local dashboard server. |
350
373
  | `LLM_TRACE_PORT` | `4319` | Port for the local dashboard server. If unset, Loupe tries `4319` first and falls back to a free local port if it is already in use. |
351
374
  | `LLM_TRACE_MAX_TRACES` | `1000` | Maximum number of traces kept in memory. |
352
375
  | `LLM_TRACE_UI_HOT_RELOAD` | auto in local interactive dev | Enables UI rebuild + reload while developing the dashboard itself. |
376
+ | `LOUPE_OPEN_BROWSER` | `enabled` in local `NODE_ENV=development` sessions | Opens the dashboard in your browser on first server start. Set `0` to suppress it. |
353
377
 
354
378
  Programmatic configuration is also available through `getLocalLLMTracer(config)`.
355
379
 
@@ -29,22 +29,26 @@
29
29
  }
30
30
  :root[data-theme=dark] {
31
31
  color-scheme: dark;
32
- --background: #111b29;
33
- --background-strong: #09111c;
34
- --panel: rgba(22, 32, 47, 0.96);
35
- --panel-strong: rgba(28, 40, 59, 0.98);
36
- --panel-soft: rgba(115, 170, 255, 0.16);
37
- --foreground: #f3f7ff;
38
- --muted-foreground: #b6c5d9;
39
- --line: rgba(140, 164, 197, 0.24);
40
- --line-strong: rgba(145, 193, 255, 0.52);
41
- --primary: #9dc8ff;
42
- --primary-strong: #f4f8ff;
43
- --secondary: rgba(120, 155, 212, 0.18);
44
- --warning: #f2bf67;
45
- --danger: #f39bac;
46
- --success: #7ad1aa;
47
- --shadow: 0 30px 90px rgba(1, 8, 20, 0.56);
32
+ --background: #0b1523;
33
+ --background-strong: #040913;
34
+ --panel: rgba(16, 27, 41, 0.9);
35
+ --panel-strong: rgba(12, 22, 35, 0.94);
36
+ --panel-soft: rgba(91, 186, 255, 0.14);
37
+ --surface-elevated: rgba(20, 34, 52, 0.96);
38
+ --surface-overlay: rgba(15, 25, 39, 0.94);
39
+ --surface-highlight: rgba(33, 57, 87, 0.98);
40
+ --surface-code: rgba(7, 13, 23, 0.96);
41
+ --foreground: #ecf4ff;
42
+ --muted-foreground: #9bb0c8;
43
+ --line: rgba(117, 146, 180, 0.24);
44
+ --line-strong: rgba(126, 196, 255, 0.5);
45
+ --primary: #88d0ff;
46
+ --primary-strong: #f5fbff;
47
+ --secondary: rgba(91, 138, 200, 0.18);
48
+ --warning: #ffc57b;
49
+ --danger: #ff9eae;
50
+ --success: #72ddb7;
51
+ --shadow: 0 34px 96px rgba(1, 8, 20, 0.62);
48
52
  }
49
53
  * {
50
54
  box-sizing: border-box;
@@ -90,18 +94,22 @@ body {
90
94
  :root[data-theme=dark] body {
91
95
  background:
92
96
  radial-gradient(
93
- circle at top left,
94
- rgba(108, 158, 235, 0.2),
95
- transparent 34rem),
97
+ circle at 12% 0%,
98
+ rgba(86, 166, 255, 0.28),
99
+ transparent 30rem),
96
100
  radial-gradient(
97
- circle at top right,
98
- rgba(138, 164, 207, 0.12),
99
- transparent 28rem),
101
+ circle at 88% 8%,
102
+ rgba(52, 197, 182, 0.14),
103
+ transparent 26rem),
104
+ radial-gradient(
105
+ circle at 50% 115%,
106
+ rgba(33, 86, 146, 0.4),
107
+ transparent 38rem),
100
108
  linear-gradient(
101
109
  180deg,
102
- #1a2638 0%,
103
- #101a28 45%,
104
- #09111c 100%);
110
+ #162436 0%,
111
+ #0a1320 46%,
112
+ #030811 100%);
105
113
  }
106
114
  button,
107
115
  input,
@@ -1183,6 +1191,12 @@ pre {
1183
1191
  overflow: auto;
1184
1192
  padding-right: 0.2rem;
1185
1193
  }
1194
+ .hierarchy-timeline-empty-state {
1195
+ padding: 0.8rem 0.55rem 0.4rem;
1196
+ color: var(--muted-foreground);
1197
+ font-size: 0.78rem;
1198
+ line-height: 1.45;
1199
+ }
1186
1200
  .hierarchy-timeline-row {
1187
1201
  --timeline-time-column: 4.75rem;
1188
1202
  --timeline-column-gap: 0.45rem;
@@ -1295,6 +1309,13 @@ pre {
1295
1309
  .hierarchy-timeline-row.is-guardrail {
1296
1310
  --timeline-row-color: rgba(184, 129, 44, 0.92);
1297
1311
  }
1312
+ .hierarchy-timeline-row.is-guardrail-trace {
1313
+ --timeline-row-color: rgba(184, 129, 44, 0.92);
1314
+ }
1315
+ .hierarchy-timeline-row.is-guardrail-group::before {
1316
+ border-color: color-mix(in srgb, var(--timeline-row-color) 18%, rgba(84, 100, 125, 0.08));
1317
+ background: color-mix(in srgb, var(--timeline-row-color) 10%, rgba(255, 255, 255, 0.72));
1318
+ }
1298
1319
  .hierarchy-timeline-row.is-trace {
1299
1320
  --timeline-row-color: rgba(92, 121, 171, 0.9);
1300
1321
  }
@@ -1396,6 +1417,20 @@ pre {
1396
1417
  min-width: 0;
1397
1418
  flex-wrap: wrap;
1398
1419
  }
1420
+ .hierarchy-timeline-row-toggle-label {
1421
+ display: inline-flex;
1422
+ align-items: center;
1423
+ min-height: 1.3rem;
1424
+ padding: 0.12rem 0.46rem;
1425
+ border-radius: 999px;
1426
+ border: 1px solid rgba(184, 129, 44, 0.16);
1427
+ background: rgba(184, 129, 44, 0.08);
1428
+ color: #865815;
1429
+ font-size: 0.67rem;
1430
+ font-weight: 700;
1431
+ letter-spacing: 0.04em;
1432
+ text-transform: uppercase;
1433
+ }
1399
1434
  .hierarchy-timeline-row-flag {
1400
1435
  display: inline-flex;
1401
1436
  align-items: center;
@@ -1499,6 +1534,25 @@ pre {
1499
1534
  text-align: right;
1500
1535
  white-space: nowrap;
1501
1536
  }
1537
+ .hierarchy-timeline-row.is-guardrail-group .hierarchy-timeline-row-labels,
1538
+ .hierarchy-timeline-row.is-guardrail-compact .hierarchy-timeline-row-labels {
1539
+ gap: 0.14rem;
1540
+ padding-top: 0.42rem;
1541
+ padding-bottom: 0.42rem;
1542
+ }
1543
+ .hierarchy-timeline-row.is-guardrail-group .hierarchy-timeline-row-bars,
1544
+ .hierarchy-timeline-row.is-guardrail-compact .hierarchy-timeline-row-bars {
1545
+ padding-top: 0.42rem;
1546
+ padding-bottom: 0.42rem;
1547
+ }
1548
+ .hierarchy-timeline-row.is-guardrail-group .hierarchy-timeline-row-title-text,
1549
+ .hierarchy-timeline-row.is-guardrail-compact .hierarchy-timeline-row-title-text {
1550
+ font-size: 0.84rem;
1551
+ }
1552
+ .hierarchy-timeline-row.is-guardrail-group .hierarchy-timeline-row-meta,
1553
+ .hierarchy-timeline-row.is-guardrail-compact .hierarchy-timeline-row-meta {
1554
+ font-size: 0.73rem;
1555
+ }
1502
1556
  .trace-detail-header {
1503
1557
  display: flex;
1504
1558
  align-items: flex-start;
@@ -1917,6 +1971,19 @@ pre {
1917
1971
  .session-tree-timeline-shell {
1918
1972
  padding: 0.08rem 0.1rem 0.24rem 0.5rem;
1919
1973
  }
1974
+ .session-tree-timeline-toolbar {
1975
+ display: flex;
1976
+ align-items: center;
1977
+ justify-content: space-between;
1978
+ gap: 0.65rem;
1979
+ padding: 0.1rem 0.2rem 0.45rem 0.08rem;
1980
+ }
1981
+ .session-tree-timeline-toolbar-copy {
1982
+ color: var(--muted-foreground);
1983
+ font-size: 0.72rem;
1984
+ font-weight: 600;
1985
+ letter-spacing: 0.01em;
1986
+ }
1920
1987
  .session-tree-timeline-list {
1921
1988
  display: flex;
1922
1989
  flex-direction: column;
@@ -1977,6 +2044,23 @@ pre {
1977
2044
  min-height: 0;
1978
2045
  padding-top: 2.05rem;
1979
2046
  }
2047
+ .session-tree-timeline-list .hierarchy-timeline-row.is-guardrail-group .hierarchy-timeline-row-labels,
2048
+ .session-tree-timeline-list .hierarchy-timeline-row.is-guardrail-compact .hierarchy-timeline-row-labels {
2049
+ padding-top: 0.62rem;
2050
+ padding-bottom: 0.62rem;
2051
+ }
2052
+ .session-tree-timeline-list .hierarchy-timeline-row.is-guardrail-group .hierarchy-timeline-row-title,
2053
+ .session-tree-timeline-list .hierarchy-timeline-row.is-guardrail-compact .hierarchy-timeline-row-title {
2054
+ padding-top: 1.7rem;
2055
+ }
2056
+ .session-tree-timeline-list .hierarchy-timeline-row.is-guardrail-group .hierarchy-timeline-row-bars,
2057
+ .session-tree-timeline-list .hierarchy-timeline-row.is-guardrail-compact .hierarchy-timeline-row-bars {
2058
+ top: 0.62rem;
2059
+ }
2060
+ .session-tree-timeline-list .hierarchy-timeline-row.has-semantic-indent .hierarchy-timeline-row-ancestor,
2061
+ .session-tree-timeline-list .hierarchy-timeline-row.has-semantic-indent .hierarchy-timeline-row-connector {
2062
+ display: none;
2063
+ }
1980
2064
  .session-tree-timeline-list .hierarchy-timeline-row-title-text,
1981
2065
  .session-tree-timeline-list .hierarchy-timeline-row-meta {
1982
2066
  overflow: hidden;
@@ -2479,86 +2563,11 @@ pre {
2479
2563
  padding: 0.9rem 1rem;
2480
2564
  box-shadow: inset 3px 0 0 var(--message-accent), inset 0 1px 0 rgba(255, 255, 255, 0.72);
2481
2565
  }
2482
- .message-insight-banner {
2483
- display: flex;
2484
- flex-direction: column;
2485
- gap: 0.55rem;
2486
- margin-bottom: 0.85rem;
2487
- }
2488
- .message-insight-card {
2489
- display: flex;
2490
- flex-direction: column;
2491
- gap: 0.35rem;
2492
- padding: 0.7rem 0.8rem;
2493
- border: 1px solid rgba(184, 129, 44, 0.16);
2494
- border-radius: 12px;
2495
- background: rgba(255, 248, 236, 0.9);
2496
- }
2497
- .message-insight-card.is-highlight {
2498
- border-color: rgba(86, 107, 147, 0.16);
2499
- background: rgba(241, 246, 255, 0.88);
2500
- }
2501
- .message-insight-card-header {
2502
- display: flex;
2503
- align-items: center;
2504
- justify-content: space-between;
2505
- gap: 0.5rem;
2506
- flex-wrap: wrap;
2507
- }
2508
- .message-insight-card-tags,
2509
2566
  .structured-markup-tags {
2510
2567
  display: flex;
2511
2568
  flex-wrap: wrap;
2512
2569
  gap: 0.35rem;
2513
2570
  }
2514
- .message-insight-card-copy {
2515
- color: var(--foreground);
2516
- font-size: 0.82rem;
2517
- line-height: 1.45;
2518
- }
2519
- .trace-insights-content {
2520
- display: flex;
2521
- flex-direction: column;
2522
- gap: 0.7rem;
2523
- }
2524
- .trace-insight-item {
2525
- display: flex;
2526
- flex-direction: column;
2527
- gap: 0.35rem;
2528
- padding: 0.78rem 0.85rem;
2529
- border: 1px solid rgba(86, 107, 147, 0.14);
2530
- border-radius: 12px;
2531
- background: rgba(243, 247, 255, 0.84);
2532
- }
2533
- .trace-insight-item.is-structured {
2534
- border-color: rgba(34, 154, 161, 0.16);
2535
- background: rgba(240, 252, 252, 0.9);
2536
- }
2537
- .trace-insight-item-header,
2538
- .trace-insight-item-tags {
2539
- display: flex;
2540
- align-items: center;
2541
- gap: 0.45rem;
2542
- flex-wrap: wrap;
2543
- }
2544
- .trace-insight-item-header {
2545
- justify-content: space-between;
2546
- }
2547
- .trace-insight-item-source {
2548
- color: var(--muted-foreground);
2549
- font-size: 0.74rem;
2550
- font-family:
2551
- ui-monospace,
2552
- SFMono-Regular,
2553
- Menlo,
2554
- Consolas,
2555
- monospace;
2556
- }
2557
- .trace-insight-item-copy {
2558
- color: var(--foreground);
2559
- font-size: 0.83rem;
2560
- line-height: 1.5;
2561
- }
2562
2571
  .structured-markup-block {
2563
2572
  display: flex;
2564
2573
  flex-direction: column;
@@ -3234,7 +3243,6 @@ pre {
3234
3243
  :root[data-theme=dark] .trace-detail-subtitle,
3235
3244
  :root[data-theme=dark] .trace-detail-json-toggle,
3236
3245
  :root[data-theme=dark] .message-card-hint,
3237
- :root[data-theme=dark] .trace-insight-item-source,
3238
3246
  :root[data-theme=dark] .tool-call-label,
3239
3247
  :root[data-theme=dark] .metadata-grid dt,
3240
3248
  :root[data-theme=dark] .metadata-secondary,
@@ -3476,6 +3484,10 @@ pre {
3476
3484
  background: rgba(36, 52, 77, 0.96);
3477
3485
  box-shadow: inset 3px 0 0 rgba(134, 184, 255, 0.82), 0 10px 22px rgba(2, 10, 24, 0.18);
3478
3486
  }
3487
+ :root[data-theme=dark] .hierarchy-timeline-row.is-guardrail-group::before {
3488
+ border-color: color-mix(in srgb, var(--timeline-row-color) 24%, rgba(128, 153, 189, 0.12));
3489
+ background: color-mix(in srgb, var(--timeline-row-color) 12%, rgba(20, 29, 43, 0.94));
3490
+ }
3479
3491
  :root[data-theme=dark] .hierarchy-timeline-row-gutter::before,
3480
3492
  :root[data-theme=dark] .hierarchy-timeline-row-gutter::after {
3481
3493
  background: color-mix(in srgb, var(--timeline-row-color) 24%, rgba(128, 153, 189, 0.14));
@@ -3497,6 +3509,11 @@ pre {
3497
3509
  background: rgba(184, 129, 44, 0.18);
3498
3510
  color: #f0c780;
3499
3511
  }
3512
+ :root[data-theme=dark] .hierarchy-timeline-row-toggle-label {
3513
+ border-color: rgba(240, 199, 128, 0.18);
3514
+ background: rgba(184, 129, 44, 0.16);
3515
+ color: #f0c780;
3516
+ }
3500
3517
  :root[data-theme=dark] .trace-detail-breadcrumb {
3501
3518
  color: rgba(182, 197, 219, 0.74);
3502
3519
  }
@@ -3644,22 +3661,6 @@ pre {
3644
3661
  :root[data-theme=dark] .tool-call-bubble {
3645
3662
  --expandable-fade-color: rgba(44, 31, 16, 0.99);
3646
3663
  }
3647
- :root[data-theme=dark] .message-insight-card {
3648
- border-color: rgba(184, 129, 44, 0.24);
3649
- background: rgba(61, 45, 18, 0.8);
3650
- }
3651
- :root[data-theme=dark] .message-insight-card.is-highlight {
3652
- border-color: rgba(86, 107, 147, 0.24);
3653
- background: rgba(27, 38, 57, 0.82);
3654
- }
3655
- :root[data-theme=dark] .trace-insight-item {
3656
- border-color: rgba(86, 107, 147, 0.24);
3657
- background: rgba(27, 38, 57, 0.82);
3658
- }
3659
- :root[data-theme=dark] .trace-insight-item.is-structured {
3660
- border-color: rgba(34, 154, 161, 0.22);
3661
- background: rgba(14, 43, 46, 0.58);
3662
- }
3663
3664
  :root[data-theme=dark] .structured-markup-pre {
3664
3665
  border-color: rgba(34, 154, 161, 0.22);
3665
3666
  background: rgba(14, 43, 46, 0.58);
@@ -3967,6 +3968,10 @@ pre {
3967
3968
  .session-tree-timeline-meta {
3968
3969
  justify-content: flex-start;
3969
3970
  }
3971
+ .session-tree-timeline-toolbar {
3972
+ flex-direction: column;
3973
+ align-items: stretch;
3974
+ }
3970
3975
  .trace-detail-secondary .hierarchy-timeline-axis,
3971
3976
  .trace-detail-secondary .hierarchy-timeline-row {
3972
3977
  grid-template-columns: minmax(0, 1fr);