@warlock.js/ai-panoptic 5.30.0 → 5.32.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.
Files changed (161) hide show
  1. package/CHANGELOG.md +25 -3
  2. package/README.md +2 -2
  3. package/cjs/index.cjs +38 -84
  4. package/cjs/index.cjs.map +1 -1
  5. package/esm/collector/collector.d.mts +3 -5
  6. package/esm/collector/collector.d.mts.map +1 -1
  7. package/esm/collector/content-capture.type.d.mts +4 -5
  8. package/esm/collector/content-capture.type.d.mts.map +1 -1
  9. package/esm/collector/extract-span-attributes.d.mts +1 -3
  10. package/esm/collector/extract-span-attributes.d.mts.map +1 -1
  11. package/esm/collector/extract-span-attributes.mjs +2 -4
  12. package/esm/collector/extract-span-attributes.mjs.map +1 -1
  13. package/esm/collector/index.d.mts +2 -1
  14. package/esm/collector/index.mjs +1 -1
  15. package/esm/collector/normalize-error.d.mts +1 -3
  16. package/esm/collector/normalize-error.d.mts.map +1 -1
  17. package/esm/collector/report-to-span.d.mts +1 -3
  18. package/esm/collector/report-to-span.d.mts.map +1 -1
  19. package/esm/collector/report-to-span.mjs.map +1 -1
  20. package/esm/collector/report-to-trace.d.mts +1 -3
  21. package/esm/collector/report-to-trace.d.mts.map +1 -1
  22. package/esm/config/apply-panoptic-config.d.mts +2 -3
  23. package/esm/config/apply-panoptic-config.d.mts.map +1 -1
  24. package/esm/config/apply-panoptic-config.mjs.map +1 -1
  25. package/esm/config/index.d.mts +2 -1
  26. package/esm/config/index.mjs +1 -1
  27. package/esm/config/panoptic-config.type.d.mts +2 -4
  28. package/esm/config/panoptic-config.type.d.mts.map +1 -1
  29. package/esm/contracts/collector.contract.d.mts +1 -3
  30. package/esm/contracts/collector.contract.d.mts.map +1 -1
  31. package/esm/contracts/exporter.contract.d.mts +1 -3
  32. package/esm/contracts/exporter.contract.d.mts.map +1 -1
  33. package/esm/contracts/index.d.mts +2 -1
  34. package/esm/contracts/trace.type.d.mts +35 -20
  35. package/esm/contracts/trace.type.d.mts.map +1 -1
  36. package/esm/dashboard/dashboard.d.mts +1 -3
  37. package/esm/dashboard/dashboard.d.mts.map +1 -1
  38. package/esm/dashboard/dashboard.mjs +3 -2
  39. package/esm/dashboard/dashboard.mjs.map +1 -1
  40. package/esm/dashboard/dashboard.type.d.mts +9 -7
  41. package/esm/dashboard/dashboard.type.d.mts.map +1 -1
  42. package/esm/dashboard/index.d.mts +4 -1
  43. package/esm/dashboard/index.mjs +1 -1
  44. package/esm/dashboard/parse-query.d.mts +1 -1
  45. package/esm/dashboard/parse-query.mjs +1 -0
  46. package/esm/dashboard/parse-query.mjs.map +1 -1
  47. package/esm/dashboard/serve.d.mts +2 -1
  48. package/esm/dashboard/serve.mjs +1 -1
  49. package/esm/dashboard/serve.mjs.map +1 -1
  50. package/esm/dashboard/trace-filter.d.mts +44 -32
  51. package/esm/dashboard/trace-filter.d.mts.map +1 -1
  52. package/esm/dashboard/trace-filter.mjs +1 -1
  53. package/esm/dashboard/trace-filter.mjs.map +1 -1
  54. package/esm/dashboard/ui.html.mjs +5 -4
  55. package/esm/dashboard/ui.html.mjs.map +1 -1
  56. package/esm/evaluate/evaluate-system-prompt.d.mts +1 -3
  57. package/esm/evaluate/evaluate-system-prompt.d.mts.map +1 -1
  58. package/esm/evaluate/evaluate-system-prompt.mjs +3 -1
  59. package/esm/evaluate/evaluate-system-prompt.mjs.map +1 -1
  60. package/esm/evaluate/evaluate.type.d.mts +2 -4
  61. package/esm/evaluate/evaluate.type.d.mts.map +1 -1
  62. package/esm/evaluate/extract-last-system-prompt.d.mts +1 -3
  63. package/esm/evaluate/extract-last-system-prompt.d.mts.map +1 -1
  64. package/esm/evaluate/find-span-by-id.d.mts +1 -3
  65. package/esm/evaluate/find-span-by-id.d.mts.map +1 -1
  66. package/esm/evaluate/index.d.mts +2 -1
  67. package/esm/evaluate/index.mjs +1 -1
  68. package/esm/exporters/console/console-exporter.d.mts +2 -3
  69. package/esm/exporters/console/console-exporter.d.mts.map +1 -1
  70. package/esm/exporters/console/console-exporter.mjs +4 -3
  71. package/esm/exporters/console/console-exporter.mjs.map +1 -1
  72. package/esm/exporters/console/console-exporter.type.d.mts +2 -3
  73. package/esm/exporters/console/console-exporter.type.d.mts.map +1 -1
  74. package/esm/exporters/console/format-span-io.d.mts +2 -2
  75. package/esm/exporters/console/format-span-io.d.mts.map +1 -1
  76. package/esm/exporters/console/format-span-io.mjs.map +1 -1
  77. package/esm/exporters/console/format-span-line.d.mts +2 -2
  78. package/esm/exporters/console/format-span-line.d.mts.map +1 -1
  79. package/esm/exporters/console/format-span-line.mjs +2 -0
  80. package/esm/exporters/console/format-span-line.mjs.map +1 -1
  81. package/esm/exporters/console/index.d.mts +2 -1
  82. package/esm/exporters/console/index.mjs +5 -0
  83. package/esm/exporters/file/file-exporter.d.mts +2 -3
  84. package/esm/exporters/file/file-exporter.d.mts.map +1 -1
  85. package/esm/exporters/file/file-exporter.mjs.map +1 -1
  86. package/esm/exporters/file/file-exporter.type.d.mts +8 -5
  87. package/esm/exporters/file/file-exporter.type.d.mts.map +1 -1
  88. package/esm/exporters/file/index.d.mts +2 -1
  89. package/esm/exporters/index.d.mts +7 -1
  90. package/esm/exporters/index.mjs +3 -1
  91. package/esm/exporters/langfuse/index.d.mts +2 -1
  92. package/esm/exporters/langfuse/index.mjs +1 -1
  93. package/esm/exporters/langfuse/langfuse-exporter.d.mts +2 -3
  94. package/esm/exporters/langfuse/langfuse-exporter.d.mts.map +1 -1
  95. package/esm/exporters/langfuse/langfuse-exporter.mjs +4 -3
  96. package/esm/exporters/langfuse/langfuse-exporter.mjs.map +1 -1
  97. package/esm/exporters/langfuse/langfuse-exporter.type.d.mts +23 -17
  98. package/esm/exporters/langfuse/langfuse-exporter.type.d.mts.map +1 -1
  99. package/esm/exporters/otel/index.d.mts +2 -1
  100. package/esm/exporters/otel/index.mjs +1 -1
  101. package/esm/exporters/otel/otel-api.shim.type.d.mts +6 -7
  102. package/esm/exporters/otel/otel-api.shim.type.d.mts.map +1 -1
  103. package/esm/exporters/otel/otel-exporter.d.mts +2 -3
  104. package/esm/exporters/otel/otel-exporter.d.mts.map +1 -1
  105. package/esm/exporters/otel/otel-exporter.mjs +2 -1
  106. package/esm/exporters/otel/otel-exporter.mjs.map +1 -1
  107. package/esm/exporters/otel/otel-exporter.type.d.mts +1 -3
  108. package/esm/exporters/otel/otel-exporter.type.d.mts.map +1 -1
  109. package/esm/exporters/utils/gen-ai-attributes.d.mts +9 -7
  110. package/esm/exporters/utils/gen-ai-attributes.d.mts.map +1 -1
  111. package/esm/exporters/utils/index.d.mts +2 -1
  112. package/esm/exporters/utils/index.mjs +5 -0
  113. package/esm/exporters/utils/total-cost.d.mts +3 -7
  114. package/esm/exporters/utils/total-cost.d.mts.map +1 -1
  115. package/esm/exporters/utils/total-cost.mjs +5 -7
  116. package/esm/exporters/utils/total-cost.mjs.map +1 -1
  117. package/esm/exporters/utils/walk-spans.d.mts +2 -2
  118. package/esm/exporters/utils/walk-spans.d.mts.map +1 -1
  119. package/esm/index.d.mts +8 -0
  120. package/esm/panoptic/index.d.mts +2 -1
  121. package/esm/panoptic/index.mjs +1 -1
  122. package/esm/panoptic/panoptic-middleware.d.mts +1 -3
  123. package/esm/panoptic/panoptic-middleware.d.mts.map +1 -1
  124. package/esm/panoptic/panoptic-middleware.mjs.map +1 -1
  125. package/esm/panoptic/panoptic-target.type.d.mts +4 -5
  126. package/esm/panoptic/panoptic-target.type.d.mts.map +1 -1
  127. package/esm/panoptic/panoptic.d.mts +1 -3
  128. package/esm/panoptic/panoptic.d.mts.map +1 -1
  129. package/esm/panoptic/panoptic.mjs.map +1 -1
  130. package/esm/panoptic/panoptic.type.d.mts +8 -7
  131. package/esm/panoptic/panoptic.type.d.mts.map +1 -1
  132. package/esm/store/cache-trace-store.d.mts +4 -6
  133. package/esm/store/cache-trace-store.d.mts.map +1 -1
  134. package/esm/store/cache-trace-store.mjs +2 -4
  135. package/esm/store/cache-trace-store.mjs.map +1 -1
  136. package/esm/store/in-memory-trace-store.d.mts +2 -4
  137. package/esm/store/in-memory-trace-store.d.mts.map +1 -1
  138. package/esm/store/in-memory-trace-store.mjs +4 -5
  139. package/esm/store/in-memory-trace-store.mjs.map +1 -1
  140. package/esm/store/index.d.mts +2 -1
  141. package/esm/store/index.mjs +1 -1
  142. package/esm/store/match-trace.d.mts +1 -3
  143. package/esm/store/match-trace.d.mts.map +1 -1
  144. package/esm/store/sum-usage.d.mts +4 -30
  145. package/esm/store/sum-usage.d.mts.map +1 -1
  146. package/esm/store/sum-usage.mjs +6 -49
  147. package/esm/store/sum-usage.mjs.map +1 -1
  148. package/esm/store/trace-aggregate.type.d.mts +13 -14
  149. package/esm/store/trace-aggregate.type.d.mts.map +1 -1
  150. package/esm/store/trace-query.type.d.mts +5 -5
  151. package/esm/store/trace-query.type.d.mts.map +1 -1
  152. package/esm/store/trace-store.contract.d.mts +1 -3
  153. package/esm/store/trace-store.contract.d.mts.map +1 -1
  154. package/llms-full.txt +148 -117
  155. package/package.json +4 -4
  156. package/skills/evaluate-system-prompt/SKILL.md +21 -17
  157. package/skills/export-traces/SKILL.md +32 -24
  158. package/skills/index.md +3 -0
  159. package/skills/observe-with-panoptic/SKILL.md +18 -18
  160. package/skills/query-traces/SKILL.md +45 -33
  161. package/skills/use-local-dashboard/SKILL.md +33 -26
package/llms-full.txt CHANGED
@@ -39,10 +39,10 @@ Absent `evaluate` ⇒ the drawer never renders the button and a `POST` to its ro
39
39
 
40
40
  ### `EvaluateConfig`
41
41
 
42
- | Field | Required | Notes |
43
- |---|---|---|
44
- | `model` | yes | A `ModelContract`, or a `() => ModelContract \| Promise<ModelContract>` factory — mirrors `panoptic.cache`'s literal-or-factory ergonomics. Resolved fresh on every evaluate click (a deliberate, infrequent, human-triggered action — not the collector's hot path — so no memoization, unlike the cache store's driver). |
45
- | `instructions` | no | Default grading rubric/criteria, verbatim — the same shape `ai.prompts().validate({ criteria })` accepts. Seeds the dashboard's editable textarea (baked into the served page at boot, so the operator sees and can extend the actual default, not a blank box); the built-in prompt-quality rubric is used when neither this nor a per-run override is supplied. |
42
+ | Field | Required | Notes |
43
+ | -------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
44
+ | `model` | yes | A `ModelContract`, or a `() => ModelContract \| Promise<ModelContract>` factory — mirrors `panoptic.cache`'s literal-or-factory ergonomics. Resolved fresh on every evaluate click (a deliberate, infrequent, human-triggered action — not the collector's hot path — so no memoization, unlike the cache store's driver). |
45
+ | `instructions` | no | Default grading rubric/criteria, verbatim — the same shape `ai.prompts().validate({ criteria })` accepts. Seeds the dashboard's editable textarea (baked into the served page at boot, so the operator sees and can extend the actual default, not a blank box); the built-in prompt-quality rubric is used when neither this nor a per-run override is supplied. |
46
46
 
47
47
  ## The UI
48
48
 
@@ -53,10 +53,14 @@ Click it to open an inline panel: a textarea pre-filled with the configured `ins
53
53
  ## The building blocks (for embedding elsewhere)
54
54
 
55
55
  ```ts
56
- import { evaluateSystemPrompt, extractLastSystemPrompt, findSpanById } from "@warlock.js/ai-panoptic";
56
+ import {
57
+ evaluateSystemPrompt,
58
+ extractLastSystemPrompt,
59
+ findSpanById,
60
+ } from "@warlock.js/ai-panoptic";
57
61
 
58
- const span = findSpanById(trace.root, spanId); // walk the span tree
59
- const prompt = extractLastSystemPrompt(span); // undefined if none captured
62
+ const span = findSpanById(trace.root, spanId); // walk the span tree
63
+ const prompt = extractLastSystemPrompt(span); // undefined if none captured
60
64
  if (prompt) {
61
65
  const verdict = await evaluateSystemPrompt(prompt, evaluateConfig, "Per-run override, optional");
62
66
  // verdict: { score?: number; issues: string[] }
@@ -71,15 +75,15 @@ if (prompt) {
71
75
 
72
76
  `POST {basePath}api/traces/:traceId/spans/:spanId/evaluate`, body `{ instructions?: string }` (all optional — an empty body is valid). Gated by the same `authToken` / `allowedHosts` checks as every other route (S4) — off-loopback still requires a token, exactly like the rest of the dashboard. Body reads are capped at 64 KB (`413` past that) so a hostile/oversized payload can't hold the connection open.
73
77
 
74
- | Status | Meaning |
75
- |---|---|
76
- | `200` | `EvaluateVerdict` — `{ score?: number; issues: string[] }` |
77
- | `404` | trace or span not found |
78
- | `422` | the span carried no captured system prompt to grade |
79
- | `413` | request body over 64 KB |
80
- | `400` | request body isn't valid JSON |
81
- | `502` | `config.model` (a factory) threw resolving the judge model |
82
- | `405` | POST hit this path but `evaluate` isn't configured |
78
+ | Status | Meaning |
79
+ | ------ | ---------------------------------------------------------- |
80
+ | `200` | `EvaluateVerdict` — `{ score?: number; issues: string[] }` |
81
+ | `404` | trace or span not found |
82
+ | `422` | the span carried no captured system prompt to grade |
83
+ | `413` | request body over 64 KB |
84
+ | `400` | request body isn't valid JSON |
85
+ | `502` | `config.model` (a factory) threw resolving the judge model |
86
+ | `405` | POST hit this path but `evaluate` isn't configured |
83
87
 
84
88
  ```sh
85
89
  curl -X POST 'http://127.0.0.1:4319/api/traces/tr_1/spans/sp_1/evaluate' \
@@ -119,35 +123,35 @@ Four exporters ship today. Each is a **factory** returning an `ExporterContract`
119
123
 
120
124
  ## Pick an exporter
121
125
 
122
- | Exporter | Backend | Deps | Use it for |
123
- |---|---|---|---|
124
- | `consoleExporter()` | stdout/stderr | none | local dev, CI logs |
125
- | `fileExporter()` | JSON-Lines file | none | durable local sink, replay, out-of-band shipping |
126
- | `otelExporter()` | OpenTelemetry | `@opentelemetry/api` (optional peer) | OTel-native stacks, GenAI semantic conventions |
127
- | `langfuseExporter()` | Langfuse | `langfuse` (optional peer) | LLM-specific dashboards, generations |
126
+ | Exporter | Backend | Deps | Use it for |
127
+ | -------------------- | --------------- | ------------------------------------ | ------------------------------------------------ |
128
+ | `consoleExporter()` | stdout/stderr | none | local dev, CI logs |
129
+ | `fileExporter()` | JSON-Lines file | none | durable local sink, replay, out-of-band shipping |
130
+ | `otelExporter()` | OpenTelemetry | `@opentelemetry/api` (optional peer) | OTel-native stacks, GenAI semantic conventions |
131
+ | `langfuseExporter()` | Langfuse | `langfuse` (optional peer) | LLM-specific dashboards, generations |
128
132
 
129
- > `@opentelemetry/api` and `langfuse` are **OPTIONAL peer dependencies**, lazily imported by their exporter. Importing `@warlock.js/ai-panoptic` never pulls them in; a missing SDK surfaces as a curated *"install this"* error on first export, never a boot-time stack trace.
133
+ > `@opentelemetry/api` and `langfuse` are **OPTIONAL peer dependencies**, lazily imported by their exporter. Importing `@warlock.js/ai-panoptic` never pulls them in; a missing SDK surfaces as a curated _"install this"_ error on first export, never a boot-time stack trace.
130
134
 
131
135
  ## Console
132
136
 
133
137
  ```ts
134
138
  import { consoleExporter } from "@warlock.js/ai-panoptic";
135
139
 
136
- collector.use(consoleExporter()); // one summary line per trace
137
- collector.use(consoleExporter({ tree: true })); // full indented span tree
140
+ collector.use(consoleExporter()); // one summary line per trace
141
+ collector.use(consoleExporter({ tree: true })); // full indented span tree
138
142
  collector.use(consoleExporter({ streaming: true })); // also print each span as it finalizes
139
143
  ```
140
144
 
141
- Failed/cancelled spans route to `console.error`; everything else to `console.log`. Inject a sink (`{ console: mySink }`) to redirect or to capture in tests.
145
+ Failed/cancelled/incomplete spans route to `console.error`; everything else to `console.log`. Inject a sink (`{ console: mySink }`) to redirect or to capture in tests.
142
146
 
143
147
  ## File
144
148
 
145
149
  ```ts
146
150
  import { fileExporter } from "@warlock.js/ai-panoptic";
147
151
 
148
- collector.use(fileExporter({ path: "storage/traces.jsonl" })); // write every trace
149
- collector.use(fileExporter({ path: "...", flushEvery: 50 })); // batch 50 traces per write
150
- collector.use(fileExporter({ path: "...", pretty: true })); // multi-line records
152
+ collector.use(fileExporter({ path: "storage/traces.jsonl" })); // write every trace
153
+ collector.use(fileExporter({ path: "...", flushEvery: 50 })); // batch 50 traces per write
154
+ collector.use(fileExporter({ path: "...", pretty: true })); // multi-line records
151
155
  ```
152
156
 
153
157
  Appends one JSON record per line (a `TraceRecord` = `{ type: "trace", exportedAt, trace }`). Buffers in memory and drains on `flush()` / `shutdown()` — call `collector.shutdown()` on teardown so the last batch lands. The parent directory is created on first write.
@@ -173,9 +177,9 @@ Each `TraceSpan` becomes one OTel span with the source start/end times and paren
173
177
  - `gen_ai.conversation.id` (from the span's `sessionId`)
174
178
  - `gen_ai.system` / `gen_ai.request.model` / `gen_ai.operation.name` — **never invented**: these appear only when the span's free-form `attributes` bag already carried that exact key. `otelExporter`'s `system` option backfills `gen_ai.system` when the span supplied none (and never overrides one it did).
175
179
  - `warlock.report.type` / `warlock.version` / `warlock.duration_ms` / `warlock.cost.usd`
176
- - the collector's own per-primitive keys ride along verbatim — `agent.model.name` / `agent.model.provider` / `agent.trips`, `workflow.steps`, `supervisor.iterations` / `supervisor.terminatedBy`, `tool.tripIndex`, `retries`, … — since `toGenAiAttributes` copies every scalar entry of the span's `attributes` bag.
180
+ - the collector's own per-primitive keys ride along verbatim — `agent.model.name` / `agent.model.provider` / `agent.trips`, `workflow.steps`, `supervisor.iterations` / `supervisor.terminatedBy`, `tool.tripIndex`, `tool.callId`, `retries`, … — since `toGenAiAttributes` copies every scalar entry of the span's `attributes` bag.
177
181
 
178
- Failed/cancelled spans get OTel `ERROR` status plus a recorded exception.
182
+ Failed/cancelled/incomplete spans get OTel `ERROR` status plus a recorded exception.
179
183
 
180
184
  ## Langfuse
181
185
 
@@ -198,11 +202,12 @@ Exporters surface `span.input` / `span.output` (console `in:` / `out:`, file JSO
198
202
 
199
203
  ```ts
200
204
  panoptic({
201
- exporters: [langfuseExporter({ /* ... */ })],
202
- captureContent: true, // copy prompts/responses + tool args/results onto spans
203
- redactContent: (value, { field }) => // optional masker, per value
205
+ exporters: [langfuseExporter({/* ... */})],
206
+ captureContent: true, // copy prompts/responses + tool args/results onto spans
207
+ redactContent: (value, { field }) =>
208
+ // optional masker, per value
204
209
  field === "input" ? "[redacted]" : value,
205
- fullHistory: true, // emit the WHOLE conversation as span.input (see below)
210
+ fullHistory: true, // emit the WHOLE conversation as span.input (see below)
206
211
  });
207
212
  ```
208
213
 
@@ -229,11 +234,18 @@ Reach for `fullHistory` when a backend (Langfuse) should show the full multi-tur
229
234
  Reusable when authoring a **custom exporter**:
230
235
 
231
236
  ```ts
232
- import { walkSpans, toGenAiAttributes, totalCostUsd, GEN_AI_ATTRIBUTES, WARLOCK_ATTRIBUTES } from "@warlock.js/ai-panoptic";
237
+ import {
238
+ walkSpans,
239
+ toGenAiAttributes,
240
+ totalCostUsd,
241
+ GEN_AI_ATTRIBUTES,
242
+ WARLOCK_ATTRIBUTES,
243
+ } from "@warlock.js/ai-panoptic";
233
244
 
234
- for (const span of walkSpans(trace.root)) { // depth-first pre-order
235
- const attributes = toGenAiAttributes(span); // gen_ai.* / warlock.* attribute map
236
- const cost = totalCostUsd(span.usage); // single USD scalar, or undefined
245
+ for (const span of walkSpans(trace.root)) {
246
+ // depth-first pre-order
247
+ const attributes = toGenAiAttributes(span); // gen_ai.* / warlock.* attribute map
248
+ const cost = totalCostUsd(span.usage); // single USD scalar, or undefined
237
249
  }
238
250
  ```
239
251
 
@@ -278,7 +290,7 @@ Reach for `reportToTrace` when you already hold a finished `result.report` and w
278
290
  ## See also
279
291
 
280
292
  - The `observe-with-panoptic` topic — the source end: wiring the `panoptic(...)` subscriber into a run (`attach` / `middleware` / `collect`). The `collector` these exporters register on comes from there.
281
- - The `query-traces` topic — the in-memory trace store: a sibling sink that *retains* traces (instead of shipping them out) so you can `query` / `aggregate` them after the fact.
293
+ - The `query-traces` topic — the in-memory trace store: a sibling sink that _retains_ traces (instead of shipping them out) so you can `query` / `aggregate` them after the fact.
282
294
  - The `run-ai-agent` topic of the `warlock-js-ai` skill — running the agent and the `BaseReport` shape Panoptic projects into a `Trace`.
283
295
 
284
296
 
@@ -303,9 +315,9 @@ import "@warlock.js/ai-panoptic"; // side-effect wiring — do this once at boot
303
315
 
304
316
  ai.config({
305
317
  panoptic: {
306
- exporters: [consoleExporter()], // optional; a store-shaped exporter also feeds the dashboard
307
- observeAll: true, // observe every flow by default
308
- dashboard: true, // optional local viewer — see use-local-dashboard
318
+ exporters: [consoleExporter()], // optional; a store-shaped exporter also feeds the dashboard
319
+ observeAll: true, // observe every flow by default
320
+ dashboard: true, // optional local viewer — see use-local-dashboard
309
321
  },
310
322
  });
311
323
  ```
@@ -316,13 +328,13 @@ ai.config({
316
328
 
317
329
  Once a collector is registered, which flows actually route to it is controlled by core's `observe` seam:
318
330
 
319
- | Setting | Effect |
320
- |---|---|
321
- | `observeAll: true` (config) | Every `ai.agent` / `ai.workflow` / `ai.supervisor` / `ai.team` that left `observe` unset routes its completed report to the registered observers. |
322
- | `observe: true` (per-flow) | Force-observe **this** flow even when `observeAll` is off → the globally registered observers. |
323
- | `observe: false` (per-flow) | Opt **this** flow out entirely, even when `observeAll` is on. |
331
+ | Setting | Effect |
332
+ | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
333
+ | `observeAll: true` (config) | Every `ai.agent` / `ai.workflow` / `ai.supervisor` / `ai.team` that left `observe` unset routes its completed report to the registered observers. |
334
+ | `observe: true` (per-flow) | Force-observe **this** flow even when `observeAll` is off → the globally registered observers. |
335
+ | `observe: false` (per-flow) | Opt **this** flow out entirely, even when `observeAll` is on. |
324
336
  | `observe: <Observer>` (per-flow) | A **flow-local** collector — only this flow's report, routed only to it (the global observers are skipped). A panoptic flow-local collector implements `Observer`, so pass it directly. |
325
- | (unset, `observeAll: false`) | Not observed — opt-in default, identical to legacy behavior. |
337
+ | (unset, `observeAll: false`) | Not observed — opt-in default, identical to legacy behavior. |
326
338
 
327
339
  ```ts
328
340
  ai.agent({ model, observe: true }); // always traced
@@ -350,11 +362,11 @@ const observe = panoptic({
350
362
 
351
363
  ## Three feed paths
352
364
 
353
- | Path | Use when | API |
354
- |---|---|---|
355
- | **Events** | You want every run captured automatically and the primitive emits a `*.completed` event (agent / workflow / supervisor). | `observe.attach(target)` |
356
- | **Middleware** | The app already composes cross-cutting concerns through the agent middleware pipeline. | `panoptic().middleware()` |
357
- | **Direct** | The primitive carries no result-bearing event (the orchestrator), or you captured a report out of band. | `observe.collect(report)` |
365
+ | Path | Use when | API |
366
+ | -------------- | ------------------------------------------------------------------------------------------------------------------------ | ------------------------- |
367
+ | **Events** | You want every run captured automatically and the primitive emits a `*.completed` event (agent / workflow / supervisor). | `observe.attach(target)` |
368
+ | **Middleware** | The app already composes cross-cutting concerns through the agent middleware pipeline. | `panoptic().middleware()` |
369
+ | **Direct** | The primitive carries no result-bearing event (the orchestrator), or you captured a report out of band. | `observe.collect(report)` |
358
370
 
359
371
  ### 1. Attach to the event stream
360
372
 
@@ -382,7 +394,7 @@ panoptic({ exporters, completedEvents: ["agent.completed"] }); // agents only
382
394
  ### 2. Wire it as agent middleware
383
395
 
384
396
  ```ts
385
- const observe = panoptic({ exporters: [langfuseExporter({ /* ... */ })] });
397
+ const observe = panoptic({ exporters: [langfuseExporter({/* ... */})] });
386
398
 
387
399
  const agent = ai.agent({
388
400
  model,
@@ -414,12 +426,12 @@ An observability fault never escapes onto the AI run's hot path:
414
426
  - The collector wraps every exporter invocation in `Promise.allSettled`, so a throwing exporter can't crash the run or block its siblings.
415
427
  - The event handler and the middleware additionally swallow the `collect` rejection, so even a collector-level fault stays contained.
416
428
 
417
- A broken backend degrades to *missing telemetry*, never a failed agent run.
429
+ A broken backend degrades to _missing telemetry_, never a failed agent run.
418
430
 
419
431
  ## Lifecycle
420
432
 
421
433
  ```ts
422
- await observe.flush(); // drain buffered exporters (between batches / checkpoints)
434
+ await observe.flush(); // drain buffered exporters (between batches / checkpoints)
423
435
  await observe.shutdown(); // flush + release every exporter (process teardown)
424
436
  ```
425
437
 
@@ -432,7 +444,7 @@ The in-memory trace store doubles as an `ExporterContract`, so the simplest way
432
444
  ```ts
433
445
  import { panoptic, createInMemoryTraceStore } from "@warlock.js/ai-panoptic";
434
446
 
435
- const store = createInMemoryTraceStore(); // an ExporterContract + queryable store
447
+ const store = createInMemoryTraceStore(); // an ExporterContract + queryable store
436
448
  const observe = panoptic({ exporters: [store, consoleExporter()] });
437
449
 
438
450
  // store now receives every trace — query/aggregate it later:
@@ -470,7 +482,7 @@ description: 'Retain @warlock.js/ai-panoptic traces in a queryable store — in-
470
482
 
471
483
  # Query traces — the in-memory queryable store
472
484
 
473
- The collector fans every completed `Trace` out to its exporters. A **trace store** is one such sink that *retains* traces so you can answer observability questions after the fact: pull one run by id, list every run for a session, slice failed runs in a time window, or roll usage + cost across any of those slices.
485
+ The collector fans every completed `Trace` out to its exporters. A **trace store** is one such sink that _retains_ traces so you can answer observability questions after the fact: pull one run by id, list every run for a session, slice failed runs in a time window, or roll usage + cost across any of those slices.
474
486
 
475
487
  `createInMemoryTraceStore()` returns an object that satisfies **both** `TraceStoreContract` and `ExporterContract` — so wiring it into a collector is a single `collector.use(store)` and the store fills as traces complete.
476
488
 
@@ -500,12 +512,12 @@ const spend = store.aggregate({ sessionId: "session-42" });
500
512
 
501
513
  ```ts
502
514
  interface TraceStoreContract {
503
- add(trace: Trace): void; // ingest; overwrites same traceId
515
+ add(trace: Trace): void; // ingest; overwrites same traceId
504
516
  get(traceId: string): Trace | undefined; // one run by id
505
- query(filter?: TraceQuery): Trace[]; // matching traces, newest-started first
517
+ query(filter?: TraceQuery): Trace[]; // matching traces, newest-started first
506
518
  aggregate(filter?: TraceQuery): TraceAggregate; // usage + cost + status rollup
507
- readonly size: number; // retained count
508
- clear(): void; // drop everything
519
+ readonly size: number; // retained count
520
+ clear(): void; // drop everything
509
521
  }
510
522
  ```
511
523
 
@@ -519,11 +531,11 @@ Every field is optional and **ANDed** together — an absent field is "don't car
519
531
 
520
532
  ```ts
521
533
  type TraceQuery = {
522
- traceId?: string; // exact one run
523
- sessionId?: string; // every run for a session
524
- status?: ReportStatus | ReportStatus[]; // single status or membership set
525
- startedAfter?: string | Date; // inclusive lower bound on root startedAt
526
- startedBefore?: string | Date; // inclusive upper bound on root startedAt
534
+ traceId?: string; // exact one run
535
+ sessionId?: string; // every run for a session
536
+ status?: ReportStatus | ReportStatus[]; // single status or membership set
537
+ startedAfter?: string | Date; // inclusive lower bound on root startedAt
538
+ startedBefore?: string | Date; // inclusive upper bound on root startedAt
527
539
  };
528
540
  ```
529
541
 
@@ -552,12 +564,12 @@ Time bounds compare against each trace's root `startedAt`, **inclusive on both e
552
564
 
553
565
  ```ts
554
566
  type TraceAggregate = {
555
- traces: number; // matched count
556
- completed: number; // root status === "completed"
557
- failed: number; // root status === "failed"
558
- cancelled: number; // root status === "cancelled"
559
- usage: Usage; // summed token usage (input/output/total always present)
560
- cost?: ModelPricing; // merged per-channel USD, or undefined if no trace was priced
567
+ traces: number; // matched count
568
+ completed: number; // root status === "completed"
569
+ failed: number; // root status === "failed" or "incomplete"
570
+ cancelled: number; // root status === "cancelled"
571
+ usage: Usage; // summed token usage (input/output/total always present)
572
+ cost?: ModelPricing; // merged per-channel USD, or undefined if no trace was priced
561
573
  totalDuration: number; // summed wall-clock ms
562
574
  };
563
575
  ```
@@ -567,17 +579,14 @@ const stats = store.aggregate({ sessionId: "session-42" });
567
579
 
568
580
  console.log(`${stats.traces} runs, ${stats.usage.total} tokens, ${stats.failed} failed`);
569
581
 
570
- const totalUsd =
571
- (stats.cost?.input ?? 0) +
572
- (stats.cost?.output ?? 0) +
573
- (stats.cost?.cachedInput ?? 0) +
574
- (stats.cost?.cachedOutput ?? 0);
582
+ // totalCost from @warlock.js/ai sums every channel (cache reads + writes included).
583
+ const totalUsd = totalCost(stats.cost) ?? 0;
575
584
  ```
576
585
 
577
586
  Notes that matter when reading the numbers:
578
587
 
579
- - **`usage`** sums each trace's *root* `Usage` (already a rollup of own cost + children), so the totals reflect the whole run tree without re-walking spans. `input` / `output` / `total` are always present (zero when nothing matched); the optional `cachedTokens` / `cacheWriteTokens` / `reasoningTokens` channels are summed only when at least one matched trace reported them.
580
- - **`cost`** is a per-channel `ModelPricing` breakdown (not one opaque number), merged with the framework's own cost-rollup logic — an unpriced trace never erases the cost of priced ones. It stays `undefined` only when **no** matched trace carried pricing.
588
+ - **`usage`** sums each trace's _root_ `Usage` (already a rollup of own cost + children), so the totals reflect the whole run tree without re-walking spans. `input` / `output` / `total` are always present (zero when nothing matched); the optional `cachedTokens` / `cacheWriteTokens` / `reasoningTokens` channels are summed only when at least one matched trace reported them.
589
+ - **`cost`** is a per-channel `ModelPricing` breakdown (not one opaque number; `cacheWrite` holds cache-write spend), merged with the framework's own cost-rollup logic — an unpriced trace never erases the cost of priced ones. It stays `undefined` only when **no** matched trace carried pricing.
581
590
  - **`completed` + `failed` + `cancelled` need not equal `traces`** — non-terminal statuses (`awaiting-input`, `max-iterations`) count toward `traces` but none of the three headline counters.
582
591
  - An empty match returns a zeroed aggregate (`usage` all `0`, `cost` `undefined`, `totalDuration` `0`).
583
592
 
@@ -612,13 +621,13 @@ const store = createCacheTraceStore(async () => {
612
621
  return driver;
613
622
  });
614
623
 
615
- await store.ready(); // hydrate the in-memory mirror from a prior run
616
- collector.use(store); // fills as traces complete; reads stay synchronous
624
+ await store.ready(); // hydrate the in-memory mirror from a prior run
625
+ collector.use(store); // fills as traces complete; reads stay synchronous
617
626
 
618
627
  const failed = store.query({ status: "failed" }); // queryable like any store
619
628
  ```
620
629
 
621
- **Write-through over a sync contract.** The store keeps the same insertion-ordered in-memory **mirror** the in-memory store uses and serves every read (`get` / `query` / `aggregate` / `size`) from it synchronously — the dashboard polls these and can't await a cache. Writes go **through**: `add` updates the mirror immediately, then *fire-and-forget* persists the trace + a newest-first index to the cache. So:
630
+ **Write-through over a sync contract.** The store keeps the same insertion-ordered in-memory **mirror** the in-memory store uses and serves every read (`get` / `query` / `aggregate` / `size`) from it synchronously — the dashboard polls these and can't await a cache. Writes go **through**: `add` updates the mirror immediately, then _fire-and-forget_ persists the trace + a newest-first index to the cache. So:
622
631
 
623
632
  - **Reads never wait on the cache** — the mirror is the runtime source of truth; the cache is the durable backing store.
624
633
  - **A flaky cache never throws into the hot path** — a rejected write is routed to the optional `onError` hook and swallowed; the trace stays visible in the mirror for the life of the process, it just won't survive a restart.
@@ -626,20 +635,21 @@ const failed = store.query({ status: "failed" }); // queryable like any store
626
635
 
627
636
  ```ts
628
637
  type CacheDriverInput =
629
- | CacheDriver // a live driver
638
+ | CacheDriver // a live driver
630
639
  | (() => CacheDriver | Promise<CacheDriver>); // lazy (sync or async) factory
631
640
 
632
641
  type CacheTraceStoreOptions = {
633
- prefix?: string; // key namespace; per-trace keys `${prefix}:trace:${id}`,
634
- // index at `${prefix}:index`. Default "panoptic".
642
+ prefix?: string; // key namespace; per-trace keys `${prefix}:trace:${id}`,
643
+ // index at `${prefix}:index`. Default "panoptic".
635
644
  capacity?: number; // FIFO cap (oldest-ingested evicted from cache + mirror);
636
- // absent / 0 = unbounded.
645
+ // absent / 0 = unbounded.
637
646
  onError?: (error: unknown) => void; // swallowed cache-write failures land here.
638
647
  };
639
648
 
640
- type CacheTraceStoreHandle = TraceStoreContract & ExporterContract & {
641
- ready(): Promise<void>; // hydrate the mirror from the cache on startup
642
- };
649
+ type CacheTraceStoreHandle = TraceStoreContract &
650
+ ExporterContract & {
651
+ ready(): Promise<void>; // hydrate the mirror from the cache on startup
652
+ };
643
653
  ```
644
654
 
645
655
  Ordering uses an **internal monotonic counter**, not `Date.now()`, so the persisted index stays a stable total order even when many traces land in the same millisecond — FIFO eviction stays honest across a restart.
@@ -667,6 +677,20 @@ for (const trace of store.query({ sessionId })) {
667
677
  - The `export-traces` topic — sending traces to an external backend instead of (or alongside) retaining them in memory; writing a custom sink.
668
678
  - The `run-ai-agent` topic of the `warlock-js-ai` skill — the `BaseReport` the collector projects into the `Trace` this store retains.
669
679
 
680
+ ## Hosted-tool rollups
681
+
682
+ `sumUsage` uses the framework's all-channel `mergeUsage`, so traces retain
683
+ `usage.serverToolUse` and `usage.cost.serverTools` alongside cache lifetime
684
+ counts and mixed-provider cache semantics. Request counts remain absent when
685
+ unreported; unpriced requests do not erase priced siblings.
686
+
687
+ ```ts
688
+ const stats = store.aggregate({ sessionId });
689
+ console.log(stats.usage.serverToolUse?.webSearchRequests);
690
+ console.log(stats.cost?.serverTools?.webSearch); // historical USD
691
+ console.log(totalCost(stats.cost)); // includes hosted fees
692
+ ```
693
+
670
694
 
671
695
  ## use-local-dashboard `@warlock.js/ai-panoptic/use-local-dashboard/SKILL.md`
672
696
 
@@ -689,8 +713,8 @@ import "@warlock.js/ai-panoptic"; // side-effect: subscribes panoptic to the con
689
713
 
690
714
  ai.config({
691
715
  panoptic: {
692
- dashboard: true, // serve on http://127.0.0.1:4319/
693
- observeAll: true, // route every workflow/supervisor/team run to the collector
716
+ dashboard: true, // serve on http://127.0.0.1:4319/
717
+ observeAll: true, // route every workflow/supervisor/team run to the collector
694
718
  },
695
719
  });
696
720
  ```
@@ -721,7 +745,7 @@ So `dashboard: true` with no exporters Just Works — you get a store for free.
721
745
  The page is one self-contained, dependency-free vanilla-JS file (no bundler, no framework, works offline). It polls the JSON API every 2s and renders:
722
746
 
723
747
  - **Header** — the Warlock logo + your `title`, the `TraceAggregate` headline counts (traces / completed / failed / cancelled), tokens as **`↓input · ↑output · total`**, total cost, an **errors-only** toggle, a **search** box, and a **light / dark / system** theme switch (persisted to `localStorage`, so the choice survives a reload; `system` follows the OS `prefers-color-scheme`).
724
- - **Trace list** — newest-first clickable master rows, each led by a **colour-coded Title-case type label** (`Supervisor` / `Team` / `Workflow` / `Agent` / `Tool` / …; `ai.team` reports its own `team` type rather than `supervisor`, so it groups, filters, and labels separately). An optional **group-by-session** view collapses runs under collapsible session headers; a **group-by-prompt** view does the same over the `name@version` prompt key (see *Prompt-version linkage* below); a **group-by-type** view buckets the list by root type under collapsible `Type (N)` headers (`Agent (12)`, `Team (3)`, …). The three groupings are mutually exclusive.
748
+ - **Trace list** — newest-first clickable master rows, each led by a **colour-coded Title-case type label** (`Supervisor` / `Team` / `Workflow` / `Agent` / `Tool` / …; `ai.team` reports its own `team` type rather than `supervisor`, so it groups, filters, and labels separately). An optional **group-by-session** view collapses runs under collapsible session headers; a **group-by-prompt** view does the same over the `name@version` prompt key (see _Prompt-version linkage_ below); a **group-by-type** view buckets the list by root type under collapsible `Type (N)` headers (`Agent (12)`, `Team (3)`, …). The three groupings are mutually exclusive.
725
749
  - **Two-pane drawer** (click a row) — a collapsible **nested call tree** on the left and the **selected node's detail** on the right: rich structured input/output (chat bubbles / key-value / Markdown), a per-node **token breakdown** (`↓in · ↑out · total`), the node's **rollup cost** (its own cost, else the sum of its children — so wrapper nodes that carry tokens but no cost don't double-count), and a **metadata panel** with humanized keys (`Supervisor Terminated By`, `Span ID` — not raw `supervisor.terminatedBy`), and **started** / **ended** wall-clock timestamps (`28 Jun 2026 03:16 PM` — named month, 12-hour, minute precision) alongside the elapsed duration. Zero-valued token/cost rows are hidden so the panel stays readable.
726
750
  - **Cost heatmap** — every tree node and trace row carries a left accent tinted by its rollup cost relative to the trace's most expensive node, with a small legend, so the spend hot-spots jump out at a glance.
727
751
  - **Timeline view** — the drawer's left pane toggles between the nested call **Tree** and a Gantt **Timeline** (each span drawn at its offset from the root start, width = duration, critical path highlighted).
@@ -743,8 +767,14 @@ The pure matching/grouping rules are unit-tested in `trace-filter.ts` and mirror
743
767
 
744
768
  ```ts
745
769
  import {
746
- filterTraces, matchesFilter, groupBySession, rollupCost, maxNodeCost, heatIntensity,
747
- type TraceFilter, type SessionGroup,
770
+ filterTraces,
771
+ matchesFilter,
772
+ groupBySession,
773
+ rollupCost,
774
+ maxNodeCost,
775
+ heatIntensity,
776
+ type TraceFilter,
777
+ type SessionGroup,
748
778
  } from "@warlock.js/ai-panoptic"; // dashboard helpers, re-exported
749
779
 
750
780
  const failed = filterTraces(traces, { errorsOnly: true });
@@ -759,7 +789,7 @@ The open trace + span are reflected in the URL hash (`#trace=<id>&span=<id>`) an
759
789
 
760
790
  ### Prompt-version linkage
761
791
 
762
- When an agent ran against a *named* `ai.prompts` builder, the collector stamps `agent.promptName` / `agent.promptVersion` onto the root span. The dashboard derives a `name@version` key from them (`tracePromptKey(trace)`) and offers **filter-by-prompt** and **group-by-prompt** beside session — so a reviewer sees every run of `support@2` together and compares its cost / failure rate against `support@3`. Runs with no named prompt bucket under `"(no prompt)"`.
792
+ When an agent ran against a _named_ `ai.prompts` builder, the collector stamps `agent.promptName` / `agent.promptVersion` onto the root span. The dashboard derives a `name@version` key from them (`tracePromptKey(trace)`) and offers **filter-by-prompt** and **group-by-prompt** beside session — so a reviewer sees every run of `support@2` together and compares its cost / failure rate against `support@3`. Runs with no named prompt bucket under `"(no prompt)"`.
763
793
 
764
794
  ```ts
765
795
  import { tracePromptKey, groupByPrompt, type PromptGroup } from "@warlock.js/ai-panoptic";
@@ -803,7 +833,7 @@ import { dashboard, createInMemoryTraceStore } from "@warlock.js/ai-panoptic";
803
833
  const store = createInMemoryTraceStore();
804
834
  const handle = await dashboard(store, { port: 4319, open: true });
805
835
 
806
- console.log(handle.url); // http://127.0.0.1:4319/
836
+ console.log(handle.url); // http://127.0.0.1:4319/
807
837
  console.log(handle.port); // 4319 (the REAL OS port when port: 0 was passed)
808
838
 
809
839
  // ...on teardown:
@@ -816,23 +846,23 @@ The store is the same object the collector writes to — wire the collector to i
816
846
 
817
847
  Every field optional, every default safe:
818
848
 
819
- | Option | Default | Notes |
820
- |---|---|---|
821
- | `port` | `4319` | Pass `0` for an ephemeral port — the resolved port comes back on `handle.port`. A busy port rejects with a clear `Error` (`port N in use; pass { port: 0 }`), not raw `EADDRINUSE`. |
822
- | `host` | `"127.0.0.1"` | **Loopback only** by default, so prompt content is never exposed to the LAN. Binding a non-loopback interface is gated — it requires `authToken` (see below) or `dashboard()` rejects. |
823
- | `basePath` | `"/"` | Mount-path prefix for both the page and every `/api/...` route, e.g. `"/panoptic"`. Normalized to leading + trailing slash. |
824
- | `open` | `false` | Open the default browser at the resolved URL once listening. Best-effort and fully swallowed — failing to open a browser never rejects the start. |
825
- | `title` | `"Panoptic"` | Header title baked into the served page. |
826
- | `authToken` | _unset_ | Bearer token required on every request. Compared with `crypto.timingSafeEqual` (constant-time, length-checked first) against both the header and query-string forms — never a plain `===` — so a network-reachable dashboard doesn't leak the token to a byte-at-a-time timing attack. `Authorization: Bearer <token>` is accepted on every route; `?token=` is accepted **only on the HTML page route** (the JSON API routes are header-only), since the page load is the one request that structurally can't carry a header — this narrows the token's footprint in access/proxy logs and browser history. Missing/wrong → `401`. **Required** when `host` is non-loopback. |
827
- | `allowedHosts` | loopback names | `Host`-header allowlist (DNS-rebind guard); an unlisted `Host` → `403`. Defaults to `localhost` / `127.0.0.1` / `[::1]` for a loopback bind. |
849
+ | Option | Default | Notes |
850
+ | -------------- | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
851
+ | `port` | `4319` | Pass `0` for an ephemeral port — the resolved port comes back on `handle.port`. A busy port rejects with a clear `Error` (`port N in use; pass { port: 0 }`), not raw `EADDRINUSE`. |
852
+ | `host` | `"127.0.0.1"` | **Loopback only** by default, so prompt content is never exposed to the LAN. Binding a non-loopback interface is gated — it requires `authToken` (see below) or `dashboard()` rejects. |
853
+ | `basePath` | `"/"` | Mount-path prefix for both the page and every `/api/...` route, e.g. `"/panoptic"`. Normalized to leading + trailing slash. |
854
+ | `open` | `false` | Open the default browser at the resolved URL once listening. Best-effort and fully swallowed — failing to open a browser never rejects the start. |
855
+ | `title` | `"Panoptic"` | Header title baked into the served page. |
856
+ | `authToken` | _unset_ | Bearer token required on every request. Compared with `crypto.timingSafeEqual` (constant-time, length-checked first) against both the header and query-string forms — never a plain `===` — so a network-reachable dashboard doesn't leak the token to a byte-at-a-time timing attack. `Authorization: Bearer <token>` is accepted on every route; `?token=` is accepted **only on the HTML page route** (the JSON API routes are header-only), since the page load is the one request that structurally can't carry a header — this narrows the token's footprint in access/proxy logs and browser history. Missing/wrong → `401`. **Required** when `host` is non-loopback. |
857
+ | `allowedHosts` | loopback names | `Host`-header allowlist (DNS-rebind guard); an unlisted `Host` → `403`. Defaults to `localhost` / `127.0.0.1` / `[::1]` for a loopback bind. |
828
858
 
829
859
  ### `DashboardHandle`
830
860
 
831
861
  ```ts
832
862
  type DashboardHandle = {
833
- readonly url: string; // http://host:port{basePath}
834
- readonly port: number; // real OS port (when port: 0 was requested)
835
- close(): Promise<void>; // stop the server, release the port
863
+ readonly url: string; // http://host:port{basePath}
864
+ readonly port: number; // real OS port (when port: 0 was requested)
865
+ close(): Promise<void>; // stop the server, release the port
836
866
  };
837
867
  ```
838
868
 
@@ -840,13 +870,13 @@ type DashboardHandle = {
840
870
 
841
871
  Every route is **`GET`-only and read-only** (non-`GET` → `405`), mounted under `basePath` — EXCEPT the one opt-in `POST .../spans/:spanId/evaluate` route `DashboardOptions.evaluate` enables (see the `evaluate-system-prompt` topic); absent that config, it 405s like everything else. The page polls the `GET` routes; you can curl them too. Store shapes are already JSON-safe, so responses are a plain `JSON.stringify`.
842
872
 
843
- | Route | Returns |
844
- |---|---|
845
- | `GET /api/traces` | `store.query(...)` — the trace list, filtered by the query string (`id` / `sessionId` / `status` / time window, parsed by `parseQuery`). |
846
- | `GET /api/traces/:id` | `store.get(id)` — one full trace, or `404 { error: "trace_not_found" }`. |
847
- | `GET /api/aggregate` | `store.aggregate(...)` — the usage + cost + status rollup over the same query filter. |
848
- | `GET /` (basePath) | the self-contained HTML page. |
849
- | `POST /api/traces/:traceId/spans/:spanId/evaluate` | grades the span's last captured system prompt — only when `evaluate` is configured. |
873
+ | Route | Returns |
874
+ | -------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
875
+ | `GET /api/traces` | `store.query(...)` — the trace list, filtered by the query string (`id` / `sessionId` / `status` / time window, parsed by `parseQuery`). |
876
+ | `GET /api/traces/:id` | `store.get(id)` — one full trace, or `404 { error: "trace_not_found" }`. |
877
+ | `GET /api/aggregate` | `store.aggregate(...)` — the usage + cost + status rollup over the same query filter. |
878
+ | `GET /` (basePath) | the self-contained HTML page. |
879
+ | `POST /api/traces/:traceId/spans/:spanId/evaluate` | grades the span's last captured system prompt — only when `evaluate` is configured. |
850
880
 
851
881
  Query-string filters map 1:1 onto `TraceQuery`:
852
882
 
@@ -880,6 +910,7 @@ createServer(handler).listen(4319, "127.0.0.1");
880
910
  The dashboard surfaces whatever the store holds — including captured prompt/response content when content capture is on (`captureContent`). It binds **loopback-only** by default precisely so that content never leaves the machine. Binding a non-loopback host is gated — it requires an `authToken` (and checks a `Host` allowlist, sending `nosniff` / CSP / `X-Frame-Options: DENY` on every response) — but still prefer loopback for dev, and for production ship to a real backend via an exporter rather than exposing the dashboard. The dashboard has exactly one write-capable route (`evaluate`, off by default) — everything else stays read-only regardless of configuration.
881
911
 
882
912
  **Trace content is untrusted input to the dashboard's own UI, and is treated that way:**
913
+
883
914
  - **Markdown links in captured content are sanitized.** `span.input`/`span.output` can contain model output or attacker-controlled tool results — i.e. prompt-injected content — and the drawer renders it as Markdown. Link URLs go through `sanitizeHref` (a scheme allowlist: `http:`, `https:`, `mailto:`, plus relative/anchor URLs) after normalizing the way a browser will — attribute-entity decode, strip of ignored control chars/whitespace, lowercase — so `javascript:`/`data:`/`vbscript:` payloads (including case/whitespace/entity-obfuscated variants, and scheme-relative `//host` links) never become a live `href`; a rejected link renders its label as plain text instead. This closes a stored-XSS path that could otherwise reach the in-page bearer token.
884
915
  - **The bearer token is no longer a page-global.** The client script's `TOKEN` is sealed inside its own inner IIFE that exposes only the `fetchAuthed` helper, so other code in the page's closure can't read the raw token by name.
885
916
 
package/package.json CHANGED
@@ -19,9 +19,9 @@
19
19
  "peerDependencies": {
20
20
  "@opentelemetry/api": "*",
21
21
  "@opentelemetry/sdk-trace-base": "*",
22
- "@warlock.js/ai": "5.30.0",
23
- "@warlock.js/cache": "5.30.0",
24
- "@warlock.js/logger": "5.30.0",
22
+ "@warlock.js/ai": "5.32.0",
23
+ "@warlock.js/cache": "5.32.0",
24
+ "@warlock.js/logger": "5.32.0",
25
25
  "langfuse": "*"
26
26
  },
27
27
  "peerDependenciesMeta": {
@@ -41,7 +41,7 @@
41
41
  "dependencies": {
42
42
  "langfuse": "^3.38.20"
43
43
  },
44
- "version": "5.30.0",
44
+ "version": "5.32.0",
45
45
  "main": "./cjs/index.cjs",
46
46
  "module": "./esm/index.mjs",
47
47
  "types": "./esm/index.d.mts",