@warlock.js/ai-panoptic 4.3.0 → 4.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.
Files changed (114) hide show
  1. package/CHANGELOG.md +46 -30
  2. package/cjs/index.cjs +2636 -67
  3. package/cjs/index.cjs.map +1 -1
  4. package/esm/collector/collector.d.mts +20 -2
  5. package/esm/collector/collector.d.mts.map +1 -1
  6. package/esm/collector/collector.mjs +47 -7
  7. package/esm/collector/collector.mjs.map +1 -1
  8. package/esm/collector/content-capture.type.d.mts +52 -0
  9. package/esm/collector/content-capture.type.d.mts.map +1 -0
  10. package/esm/collector/extract-span-attributes.d.mts.map +1 -1
  11. package/esm/collector/extract-span-attributes.mjs +3 -0
  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 +7 -0
  15. package/esm/collector/normalize-error.d.mts.map +1 -1
  16. package/esm/collector/normalize-error.mjs +6 -3
  17. package/esm/collector/normalize-error.mjs.map +1 -1
  18. package/esm/collector/report-to-span.d.mts +8 -1
  19. package/esm/collector/report-to-span.d.mts.map +1 -1
  20. package/esm/collector/report-to-span.mjs +87 -2
  21. package/esm/collector/report-to-span.mjs.map +1 -1
  22. package/esm/collector/report-to-trace.d.mts +10 -6
  23. package/esm/collector/report-to-trace.d.mts.map +1 -1
  24. package/esm/collector/report-to-trace.mjs +10 -7
  25. package/esm/collector/report-to-trace.mjs.map +1 -1
  26. package/esm/config/apply-panoptic-config.d.mts +27 -0
  27. package/esm/config/apply-panoptic-config.d.mts.map +1 -0
  28. package/esm/config/apply-panoptic-config.mjs +89 -0
  29. package/esm/config/apply-panoptic-config.mjs.map +1 -0
  30. package/esm/config/index.d.mts +2 -0
  31. package/esm/config/index.mjs +3 -0
  32. package/esm/config/panoptic-config.type.d.mts +84 -0
  33. package/esm/config/panoptic-config.type.d.mts.map +1 -0
  34. package/esm/contracts/trace.type.d.mts +31 -5
  35. package/esm/contracts/trace.type.d.mts.map +1 -1
  36. package/esm/dashboard/dashboard.d.mts +32 -0
  37. package/esm/dashboard/dashboard.d.mts.map +1 -0
  38. package/esm/dashboard/dashboard.mjs +132 -0
  39. package/esm/dashboard/dashboard.mjs.map +1 -0
  40. package/esm/dashboard/dashboard.type.d.mts +63 -0
  41. package/esm/dashboard/dashboard.type.d.mts.map +1 -0
  42. package/esm/dashboard/index.d.mts +3 -0
  43. package/esm/dashboard/parse-query.d.mts +1 -0
  44. package/esm/dashboard/parse-query.mjs +54 -0
  45. package/esm/dashboard/parse-query.mjs.map +1 -0
  46. package/esm/dashboard/serve.d.mts +1 -0
  47. package/esm/dashboard/serve.mjs +111 -0
  48. package/esm/dashboard/serve.mjs.map +1 -0
  49. package/esm/dashboard/trace-filter.d.mts +187 -0
  50. package/esm/dashboard/trace-filter.d.mts.map +1 -0
  51. package/esm/dashboard/trace-filter.mjs +270 -0
  52. package/esm/dashboard/trace-filter.mjs.map +1 -0
  53. package/esm/dashboard/ui.html.mjs +1330 -0
  54. package/esm/dashboard/ui.html.mjs.map +1 -0
  55. package/esm/dashboard/warlock-logo.mjs +13 -0
  56. package/esm/dashboard/warlock-logo.mjs.map +1 -0
  57. package/esm/exporters/console/console-exporter.d.mts +11 -3
  58. package/esm/exporters/console/console-exporter.d.mts.map +1 -1
  59. package/esm/exporters/console/console-exporter.mjs +31 -13
  60. package/esm/exporters/console/console-exporter.mjs.map +1 -1
  61. package/esm/exporters/console/console-exporter.type.d.mts +15 -0
  62. package/esm/exporters/console/console-exporter.type.d.mts.map +1 -1
  63. package/esm/exporters/console/format-span-io.d.mts +27 -0
  64. package/esm/exporters/console/format-span-io.d.mts.map +1 -0
  65. package/esm/exporters/console/format-span-io.mjs +52 -0
  66. package/esm/exporters/console/format-span-io.mjs.map +1 -0
  67. package/esm/exporters/console/index.d.mts +2 -1
  68. package/esm/exporters/file/file-exporter.mjs +5 -2
  69. package/esm/exporters/file/file-exporter.mjs.map +1 -1
  70. package/esm/exporters/index.d.mts +1 -0
  71. package/esm/exporters/index.mjs +1 -0
  72. package/esm/exporters/langfuse/langfuse-exporter.d.mts +10 -6
  73. package/esm/exporters/langfuse/langfuse-exporter.d.mts.map +1 -1
  74. package/esm/exporters/langfuse/langfuse-exporter.mjs +80 -30
  75. package/esm/exporters/langfuse/langfuse-exporter.mjs.map +1 -1
  76. package/esm/exporters/langfuse/langfuse-exporter.type.d.mts +6 -2
  77. package/esm/exporters/langfuse/langfuse-exporter.type.d.mts.map +1 -1
  78. package/esm/exporters/otel/otel-api.shim.type.d.mts +60 -0
  79. package/esm/exporters/otel/otel-api.shim.type.d.mts.map +1 -0
  80. package/esm/exporters/otel/otel-exporter.d.mts.map +1 -1
  81. package/esm/exporters/otel/otel-exporter.mjs +15 -0
  82. package/esm/exporters/otel/otel-exporter.mjs.map +1 -1
  83. package/esm/exporters/otel/otel-exporter.type.d.mts +7 -3
  84. package/esm/exporters/otel/otel-exporter.type.d.mts.map +1 -1
  85. package/esm/exporters/utils/gen-ai-attributes.d.mts +3 -1
  86. package/esm/exporters/utils/gen-ai-attributes.d.mts.map +1 -1
  87. package/esm/exporters/utils/gen-ai-attributes.mjs +5 -1
  88. package/esm/exporters/utils/gen-ai-attributes.mjs.map +1 -1
  89. package/esm/index.d.mts +11 -2
  90. package/esm/index.mjs +10 -1
  91. package/esm/panoptic/index.mjs +4 -0
  92. package/esm/panoptic/panoptic.d.mts.map +1 -1
  93. package/esm/panoptic/panoptic.mjs +9 -1
  94. package/esm/panoptic/panoptic.mjs.map +1 -1
  95. package/esm/panoptic/panoptic.type.d.mts +32 -0
  96. package/esm/panoptic/panoptic.type.d.mts.map +1 -1
  97. package/esm/register.mjs +12 -0
  98. package/esm/register.mjs.map +1 -0
  99. package/esm/store/cache-trace-store.d.mts +81 -0
  100. package/esm/store/cache-trace-store.d.mts.map +1 -0
  101. package/esm/store/cache-trace-store.mjs +290 -0
  102. package/esm/store/cache-trace-store.mjs.map +1 -0
  103. package/esm/store/in-memory-trace-store.mjs +9 -2
  104. package/esm/store/in-memory-trace-store.mjs.map +1 -1
  105. package/esm/store/index.d.mts +1 -0
  106. package/esm/store/index.mjs +1 -0
  107. package/llms-full.txt +351 -4
  108. package/llms.txt +4 -3
  109. package/package.json +6 -2
  110. package/skills/README.md +7 -3
  111. package/skills/export-traces/SKILL.md +33 -1
  112. package/skills/observe-with-panoptic/SKILL.md +49 -2
  113. package/skills/query-traces/SKILL.md +52 -1
  114. package/skills/use-local-dashboard/SKILL.md +213 -0
@@ -1,11 +1,57 @@
1
1
  ---
2
2
  name: observe-with-panoptic
3
- description: 'Wire @warlock.js/ai-panoptic into an agent/workflow/supervisor/orchestrator with the one-call `panoptic({ exporters })` subscriber. Triggers: `panoptic`, `Panoptic`, `PanopticOptions`, `.attach`, `.middleware`, `.collect`, `.toTrace`, `observe.attach(agent)`, `panoptic().middleware()`, `completedEvents`, `middlewareName`; ''observe a warlock agent'', ''trace an agent run'', ''attach observability to a workflow/supervisor'', ''add a tracing middleware'', ''collect an orchestrator turn report'', ''wire OTel/Langfuse into my agent''; typical import `import { panoptic } from "@warlock.js/ai-panoptic"`. Skip: writing/choosing an exporter (the sink end) — `@warlock.js/ai-panoptic/export-traces/SKILL.md`; the core report shape / running the agent itself — `@warlock.js/ai/run-ai-agent/SKILL.md`.'
3
+ description: 'Wire @warlock.js/ai-panoptic into an agent/workflow/supervisor/orchestrator — declaratively via `ai.config({ panoptic })` (registers panoptic on core''s Observer seam) or with the one-call `panoptic({ exporters })` subscriber. Triggers: `panoptic`, `Panoptic`, `PanopticOptions`, `PanopticConfig`, `ai.config({ panoptic })`, `observeAll`, per-flow `observe`, `FlowObserveOption`, `registerObserver`, `Observer`, `.attach`, `.middleware`, `.collect`, `.toTrace`, `observe.attach(agent)`, `panoptic().middleware()`, `completedEvents`, `middlewareName`; ''observe a warlock agent'', ''trace an agent run'', ''attach observability to a workflow/supervisor'', ''observe every flow by default'', ''add a tracing middleware'', ''collect an orchestrator turn report'', ''wire OTel/Langfuse into my agent''; typical import `import { ai } from "@warlock.js/ai"` + `import "@warlock.js/ai-panoptic"` (or `import { panoptic } from "@warlock.js/ai-panoptic"`). Skip: the zero-setup local dashboard — `@warlock.js/ai-panoptic/use-local-dashboard/SKILL.md`; writing/choosing an exporter (the sink end) — `@warlock.js/ai-panoptic/export-traces/SKILL.md`; the core report shape / running the agent itself — `@warlock.js/ai/run-ai-agent/SKILL.md`.'
4
4
  ---
5
5
 
6
6
  # Observe a run with Panoptic — the one-call subscriber
7
7
 
8
- `panoptic(...)` is the entry point. It builds a collector, registers your exporters on it, and hands back a subscriber with **three ways to feed traces** in. All three converge on the same collector, so each run reaches every exporter exactly once.
8
+ There are two levels. **Declarative** (`ai.config({ panoptic })`) is the high-level path: panoptic registers itself on core's generic `Observer` seam and every observed flow routes its report in automatically — no per-target wiring. **Imperative** (`panoptic(...)`) is the underlying subscriber, with three explicit feed paths. The declarative path is sugar over the imperative one; both reuse the same collection pipeline.
9
+
10
+ ## Declarative — `ai.config({ panoptic })` + the Observer seam
11
+
12
+ The simplest wiring. Importing `@warlock.js/ai-panoptic` (even bare, for side effects) subscribes panoptic to core's config seam; a later `ai.config({ panoptic })` then builds the collector and **registers it once on core's `Observer` registry** via `registerObserver`. Core itself never imports panoptic — it depends only on the structural `Observer` interface (`{ collect(report) }`), the dependency inversion that keeps the two sides decoupled.
13
+
14
+ ```ts
15
+ import { ai } from "@warlock.js/ai";
16
+ import "@warlock.js/ai-panoptic"; // side-effect wiring — do this once at boot
17
+
18
+ ai.config({
19
+ panoptic: {
20
+ exporters: [consoleExporter()], // optional; a store-shaped exporter also feeds the dashboard
21
+ observeAll: true, // observe every flow by default
22
+ dashboard: true, // optional local viewer — see use-local-dashboard
23
+ },
24
+ });
25
+ ```
26
+
27
+ `PanopticConfig`: `exporters?` (the collector's sinks), `observeAll?` (default `false`), `dashboard?` (`boolean | DashboardOptions`). Applying it is **idempotent** — the observer registers once, the dashboard starts at most once; repeat `ai.config(...)` merges only refresh `observeAll`.
28
+
29
+ ### `observeAll` + per-flow `observe`
30
+
31
+ Once a collector is registered, which flows actually route to it is controlled by core's `observe` seam:
32
+
33
+ | Setting | Effect |
34
+ |---|---|
35
+ | `observeAll: true` (config) | Every `ai.agent` / `ai.workflow` / `ai.supervisor` / `ai.team` that left `observe` unset routes its completed report to the registered observers. |
36
+ | `observe: true` (per-flow) | Force-observe **this** flow even when `observeAll` is off → the globally registered observers. |
37
+ | `observe: false` (per-flow) | Opt **this** flow out entirely, even when `observeAll` is on. |
38
+ | `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. |
39
+ | (unset, `observeAll: false`) | Not observed — opt-in default, identical to legacy behavior. |
40
+
41
+ ```ts
42
+ ai.agent({ model, observe: true }); // always traced
43
+ ai.workflow({ name: "billing", steps, observe: true }); // always traced
44
+ ai.supervisor({ name: "triage", ..., observe: false }); // never traced, even under observeAll
45
+
46
+ const flowLocal = panoptic({ exporters: [fileExporter({ path: "billing.jsonl" })] });
47
+ ai.team({ name: "research", ..., observe: flowLocal }); // only this flow, only this collector
48
+ ```
49
+
50
+ The flow awaits each observer's `collect`, and a throwing observer is **swallowed** — observability never breaks the run (mirrors `onUsage` / `onComplete`). `observe` lives on **all four** flow configs — `ai.agent`, `ai.workflow`, `ai.supervisor`, and `ai.team` (the agent additionally supports the imperative `attach()` / `middleware()` paths below).
51
+
52
+ ## Imperative — `panoptic(...)`, the one-call subscriber
53
+
54
+ `panoptic(...)` builds a collector, registers your exporters on it, and hands back a subscriber with **three ways to feed traces** in. All three converge on the same collector, so each run reaches every exporter exactly once. Use it when you want explicit control, a flow-local collector, or to wire a target the declarative seam doesn't cover (the orchestrator).
9
55
 
10
56
  ```ts
11
57
  import { panoptic } from "@warlock.js/ai-panoptic";
@@ -123,6 +169,7 @@ Add exporters after construction with `observe.use(exporter)` (chainable, dedupe
123
169
 
124
170
  ## See also
125
171
 
172
+ - [`@warlock.js/ai-panoptic/use-local-dashboard/SKILL.md`](@warlock.js/ai-panoptic/use-local-dashboard/SKILL.md) — the zero-setup local viewer the `ai.config({ panoptic: { dashboard } })` switch starts over the collector's store.
126
173
  - [`@warlock.js/ai-panoptic/export-traces/SKILL.md`](@warlock.js/ai-panoptic/export-traces/SKILL.md) — the sink end: choosing/writing an exporter, the `gen_ai.*` / `warlock.*` attribute mapping.
127
174
  - [`@warlock.js/ai-panoptic/query-traces/SKILL.md`](@warlock.js/ai-panoptic/query-traces/SKILL.md) — retaining traces in the queryable in-memory store and slicing them (`query` / `aggregate` / `capacity`).
128
175
  - [`@warlock.js/ai/run-ai-agent/SKILL.md`](@warlock.js/ai/run-ai-agent/SKILL.md) — running the agent and the `BaseReport` shape Panoptic consumes.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: query-traces
3
- description: 'Retain @warlock.js/ai-panoptic traces in a queryable in-memory store and slice them after the fact get one run by id, list a session, filter failed runs in a window, roll up usage + cost. Triggers: `createInMemoryTraceStore`, `TraceStoreContract`, `TraceQuery`, `TraceAggregate`, `InMemoryTraceStoreOptions`, `store.query`, `store.aggregate`, `store.get`, `store.add`, `store.clear`, `store.size`, `capacity`, `sumUsage`, `emptyUsage`, `matchTrace`; ''query collected traces'', ''aggregate AI cost per session'', ''how much did this session spend'', ''list failed runs'', ''retain traces in memory'', ''roll up token usage''; typical import `import { createInMemoryTraceStore } from "@warlock.js/ai-panoptic"`. Skip: sending traces to an external backend (OTel/Langfuse/console/file) — `@warlock.js/ai-panoptic/export-traces/SKILL.md`; wiring the subscriber into a run — `@warlock.js/ai-panoptic/observe-with-panoptic/SKILL.md`.'
3
+ description: 'Retain @warlock.js/ai-panoptic traces in a queryable store — in-memory, or cache-backed so they survive a process restart — and slice them after the fact: get one run by id, list a session, filter failed runs in a window, roll up usage + cost. Triggers: `createInMemoryTraceStore`, `createCacheTraceStore`, `CacheTraceStoreHandle`, `CacheTraceStoreOptions`, `CacheDriverInput`, `store.ready`, `TraceStoreContract`, `TraceQuery`, `TraceAggregate`, `InMemoryTraceStoreOptions`, `store.query`, `store.aggregate`, `store.get`, `store.add`, `store.clear`, `store.size`, `capacity`, `prefix`, `onError`, `sumUsage`, `emptyUsage`, `matchTrace`; ''query collected traces'', ''aggregate AI cost per session'', ''how much did this session spend'', ''list failed runs'', ''retain traces in memory'', ''persist traces across a restart'', ''cache-backed trace store'', ''Redis trace store'', ''roll up token usage''; typical import `import { createInMemoryTraceStore, createCacheTraceStore } from "@warlock.js/ai-panoptic"`. Skip: sending traces to an external backend (OTel/Langfuse/console/file) — `@warlock.js/ai-panoptic/export-traces/SKILL.md`; wiring the subscriber into a run — `@warlock.js/ai-panoptic/observe-with-panoptic/SKILL.md`.'
4
4
  ---
5
5
 
6
6
  # Query traces — the in-memory queryable store
@@ -130,6 +130,57 @@ Overwriting an existing `traceId` refreshes its insertion position, so a re-coll
130
130
 
131
131
  > The in-memory store has no secondary indexes: `get` / `add` are O(1), `query` / `aggregate` are O(n) scans. That is the right trade-off for dev/test and modest-volume runtime use. For high-volume or durable retention, write a custom `TraceStoreContract` (or export to a backend with [`export-traces`](@warlock.js/ai-panoptic/export-traces/SKILL.md)) backed by a real datastore.
132
132
 
133
+ ## Persisting across a restart — `createCacheTraceStore`
134
+
135
+ The in-memory store loses everything when the process exits. `createCacheTraceStore(cache, options?)` is a **durable** `TraceStoreContract` (and `ExporterContract`) backed by any `@warlock.js/cache` `CacheDriver` — Redis, file, Postgres, … — so traces survive a restart and the dashboard re-opens with history intact.
136
+
137
+ ```ts
138
+ import { createCacheTraceStore } from "@warlock.js/ai-panoptic";
139
+ import { RedisCacheDriver } from "@warlock.js/cache";
140
+
141
+ // A driver instance, or a (possibly async) FACTORY resolved lazily on first
142
+ // use — the factory form defers the Redis connect until the first trace is
143
+ // collected, so module import stays connection-free.
144
+ const store = createCacheTraceStore(async () => {
145
+ const driver = new RedisCacheDriver();
146
+ await driver.connect();
147
+ return driver;
148
+ });
149
+
150
+ await store.ready(); // hydrate the in-memory mirror from a prior run
151
+ collector.use(store); // fills as traces complete; reads stay synchronous
152
+
153
+ const failed = store.query({ status: "failed" }); // queryable like any store
154
+ ```
155
+
156
+ **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:
157
+
158
+ - **Reads never wait on the cache** — the mirror is the runtime source of truth; the cache is the durable backing store.
159
+ - **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.
160
+ - **`ready()` re-hydrates** the mirror from the persisted index on startup (await it once — `ai.config({ panoptic: { cache } })` does this for you before serving the dashboard).
161
+
162
+ ```ts
163
+ type CacheDriverInput =
164
+ | CacheDriver // a live driver
165
+ | (() => CacheDriver | Promise<CacheDriver>); // lazy (sync or async) factory
166
+
167
+ type CacheTraceStoreOptions = {
168
+ prefix?: string; // key namespace; per-trace keys `${prefix}:trace:${id}`,
169
+ // index at `${prefix}:index`. Default "panoptic".
170
+ capacity?: number; // FIFO cap (oldest-ingested evicted from cache + mirror);
171
+ // absent / 0 = unbounded.
172
+ onError?: (error: unknown) => void; // swallowed cache-write failures land here.
173
+ };
174
+
175
+ type CacheTraceStoreHandle = TraceStoreContract & ExporterContract & {
176
+ ready(): Promise<void>; // hydrate the mirror from the cache on startup
177
+ };
178
+ ```
179
+
180
+ 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.
181
+
182
+ > The usual path is declarative: `ai.config({ panoptic: { cache, dashboard: true } })` constructs this store, awaits `ready()`, and wires it under the dashboard for you — see [`use-local-dashboard/SKILL.md`](@warlock.js/ai-panoptic/use-local-dashboard/SKILL.md). Reach for `createCacheTraceStore` directly only when you manage the store / collector yourself.
183
+
133
184
  ## Summing usage yourself
134
185
 
135
186
  `sumUsage` / `emptyUsage` are the pure folds `aggregate` is built on — reuse them to roll a `Usage` set the store didn't produce (e.g. a list of traces you filtered by hand):
@@ -0,0 +1,213 @@
1
+ ---
2
+ name: use-local-dashboard
3
+ description: 'Run the zero-setup local Panoptic dashboard — a loopback `node:http` server over a trace store served from `ai.config({ panoptic: { dashboard } })`, with a two-pane call-tree + metadata UI (light/dark/system theme, search/filter, session + prompt-version grouping, cost heatmap, Gantt timeline, errors filter, hash deep-links) and an optional cache-backed persistent store. Triggers: `dashboard`, `DashboardOptions`, `DashboardHandle`, `ai.config({ panoptic })`, `panoptic.dashboard`, `panoptic.cache`, `port: 4319`, `basePath`, `open`, `/api/traces`, `/api/aggregate`, `/api/traces/:id`, `createRequestHandler`, `dashboardHtml`, `ServeConfig`, `TraceStoreContract`, `createCacheTraceStore`, theme, dark mode, call tree, metadata panel, cost heatmap, timeline / Gantt, errors only, deep-link, group by session, prompt version, group by type, type stats panel, started / ended timestamps, team type; ''open a local AI trace dashboard'', ''view agent traces in the browser'', ''self-hosted panoptic UI'', ''inspect traces without Langfuse'', ''spin up a dev observability server'', ''dark mode dashboard'', ''persist traces across restart'', ''group traces by session / prompt version / type'', ''cost heatmap'', ''timeline / waterfall view'', ''filter failed runs in the dashboard'', ''per-type stats panel''; typical import `import { ai } from "@warlock.js/ai"` + `import "@warlock.js/ai-panoptic"`. Skip: shipping traces to a backend (OTel / Langfuse / file) — `@warlock.js/ai-panoptic/export-traces/SKILL.md`; the queryable store API the dashboard reads — `@warlock.js/ai-panoptic/query-traces/SKILL.md`; wiring the subscriber by hand — `@warlock.js/ai-panoptic/observe-with-panoptic/SKILL.md`.'
4
+ ---
5
+
6
+ # The local Panoptic dashboard — zero-setup trace viewing
7
+
8
+ A loopback-only `node:http` server that serves one self-contained HTML page polling a read-only JSON API over a trace store. No external backend and no build step; it binds `127.0.0.1` with no auth by default, and gains optional bearer-token auth + a Host allowlist (`authToken` / `allowedHosts`, **required** off-loopback). The store is the live object the collector fills, so each poll reflects the latest completed traces.
9
+
10
+ ## The documented path — `ai.config({ panoptic: { dashboard } })`
11
+
12
+ You almost never call `dashboard(...)` directly. Set it declaratively and the side-effect import wires the store, the collector, and the server for you:
13
+
14
+ ```ts
15
+ import { ai } from "@warlock.js/ai";
16
+ import "@warlock.js/ai-panoptic"; // side-effect: subscribes panoptic to the config seam
17
+
18
+ ai.config({
19
+ panoptic: {
20
+ dashboard: true, // serve on http://127.0.0.1:4319/
21
+ observeAll: true, // route every workflow/supervisor/team run to the collector
22
+ },
23
+ });
24
+ ```
25
+
26
+ `dashboard: true` uses the defaults. Pass an object to override:
27
+
28
+ ```ts
29
+ ai.config({
30
+ panoptic: {
31
+ exporters: [createInMemoryTraceStore({ capacity: 5000 })],
32
+ dashboard: { port: 4319, open: true, title: "My App — AI traces" },
33
+ observeAll: true,
34
+ },
35
+ });
36
+ ```
37
+
38
+ **Where the dashboard reads its traces from.** It needs a queryable store (a `TraceStoreContract`). Panoptic resolves one in this order:
39
+
40
+ 1. The first **store-shaped exporter** in `config.exporters` — `createInMemoryTraceStore()` doubles as an `ExporterContract`, so a store passed in `exporters` is reused (captured AND served from the same object).
41
+ 2. If none was supplied but `dashboard` is set, panoptic creates a fresh in-memory store and registers it as an exporter so the collector fills it.
42
+
43
+ So `dashboard: true` with no exporters Just Works — you get a store for free. Supply your own when you want a capacity cap or want to `query` / `aggregate` it from app code too.
44
+
45
+ **Idempotent.** Applying the config is safe on every `ai.config(...)` merge: the collector is registered as a core `Observer` exactly once and the dashboard is started at most once. Repeat calls only refresh the `observeAll` flag (latest config wins). `panoptic` undefined in the config is a no-op.
46
+
47
+ ## What the UI shows
48
+
49
+ 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:
50
+
51
+ - **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`).
52
+ - **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.
53
+ - **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.
54
+ - **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.
55
+ - **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).
56
+ - **Stats panel** — a header **Stats** toggle shows a per-type aggregate table above the trace list, one row per root type (`Agent` / `Team` / `Workflow` / …) with Count, Failed (rate), p50 / p95 latency, total Tokens, and total Cost, computed over the active filtered set. Distinct from the headline counts in the header.
57
+
58
+ All UI state (selected trace, selected span, collapsed nodes, theme, filters) lives in JS, **not** the DOM, so the 2s poll never disturbs an open drawer.
59
+
60
+ ### Filtering & grouping (all client-side over the polled list)
61
+
62
+ The list narrows entirely in the browser — no server round-trip per keystroke:
63
+
64
+ - **Search** — free-text, case-insensitive, matched against root span name + session id.
65
+ - **Status / type chips** — restrict to selected terminal statuses or executable types. Chip, badge, drawer, and group-header labels are **Title-cased** everywhere (`Completed`, `Workflow`, `Team`); the underlying filter keys stay lowercase (`completed`, `workflow`, `team`).
66
+ - **Errors-only** — header toggle that keeps just `failed` / `cancelled` runs.
67
+ - **Session chip** — exact `sessionId`; **prompt chip** — exact `name@version` key.
68
+ - **Group-by-type** — buckets the list by root type under `Type (N)` headers; mutually exclusive with group-by-session and group-by-prompt.
69
+
70
+ The pure matching/grouping rules are unit-tested in `trace-filter.ts` and mirrored 1:1 by the inlined dashboard JS, exported for reuse / your own list views:
71
+
72
+ ```ts
73
+ import {
74
+ filterTraces, matchesFilter, groupBySession, rollupCost, maxNodeCost, heatIntensity,
75
+ type TraceFilter, type SessionGroup,
76
+ } from "@warlock.js/ai-panoptic"; // dashboard helpers, re-exported
77
+
78
+ const failed = filterTraces(traces, { errorsOnly: true });
79
+ const bySession = groupBySession(failed); // SessionGroup[], sessionless → "(no session)"
80
+ ```
81
+
82
+ `TraceFilter` is `{ text?, statuses?, types?, sessionId?, promptKey?, errorsOnly? }` — every field optional and ANDed, so `{}` matches everything. `rollupCost(span)` / `maxNodeCost(root)` / `heatIntensity(nodeCost, maxCost)` are the pure heatmap folds (intensity in `[0,1]`, `0` for a free trace rather than dividing by zero).
83
+
84
+ ### Deep-links
85
+
86
+ The open trace + span are reflected in the URL hash (`#trace=<id>&span=<id>`) and re-opened from it on load — so a drawer view is **shareable / bookmarkable**. (A live socket tail is a noted follow-up; this pass stays on the 2s poll.)
87
+
88
+ ### Prompt-version linkage
89
+
90
+ 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)"`.
91
+
92
+ ```ts
93
+ import { tracePromptKey, groupByPrompt, type PromptGroup } from "@warlock.js/ai-panoptic";
94
+
95
+ const byPrompt = groupByPrompt(store.query()); // PromptGroup[] keyed by name@version
96
+ ```
97
+
98
+ ## Persistent traces — `cache` (survive a restart)
99
+
100
+ By default the dashboard reads a fresh **in-memory** store, so traces vanish on restart. Point `panoptic.cache` at a `@warlock.js/cache` `CacheDriver` (Redis, file, …) and panoptic serves the dashboard from a **cache-backed** store instead — traces persist and re-hydrate on the next boot:
101
+
102
+ ```ts
103
+ import { ai } from "@warlock.js/ai";
104
+ import "@warlock.js/ai-panoptic";
105
+ import { RedisCacheDriver } from "@warlock.js/cache";
106
+
107
+ ai.config({
108
+ panoptic: {
109
+ dashboard: true,
110
+ observeAll: true,
111
+ // A driver, or a (possibly async) factory resolved lazily on first use —
112
+ // the factory form defers the Redis connect until the first trace lands.
113
+ cache: async () => {
114
+ const driver = new RedisCacheDriver();
115
+ await driver.connect();
116
+ return driver;
117
+ },
118
+ },
119
+ });
120
+ ```
121
+
122
+ Panoptic awaits the store's `ready()` hydration **before** the server starts serving, so a restart surfaces previously-persisted traces immediately. Reads stay synchronous (served from an in-memory mirror); writes go through to the cache fire-and-forget, so a flaky cache never throws into the collector's hot path. A store-shaped exporter you pass via `exporters` still wins over `cache`. The store building block (`createCacheTraceStore`) and its durability contract live in [`query-traces/SKILL.md`](@warlock.js/ai-panoptic/query-traces/SKILL.md).
123
+
124
+ ## The low-level building block — `dashboard(store, options)`
125
+
126
+ Reach for this directly only when you manage the store yourself (e.g. a long-lived store you also query elsewhere, or a non-default lifecycle). It returns a `Promise<DashboardHandle>`:
127
+
128
+ ```ts
129
+ import { dashboard, createInMemoryTraceStore } from "@warlock.js/ai-panoptic";
130
+
131
+ const store = createInMemoryTraceStore();
132
+ const handle = await dashboard(store, { port: 4319, open: true });
133
+
134
+ console.log(handle.url); // http://127.0.0.1:4319/
135
+ console.log(handle.port); // 4319 (the REAL OS port when port: 0 was passed)
136
+
137
+ // ...on teardown:
138
+ await handle.close();
139
+ ```
140
+
141
+ The store is the same object the collector writes to — wire the collector to it (pass it as an exporter to `panoptic({ exporters: [store] })` or via `ai.config`) and the dashboard reflects new traces on its next poll.
142
+
143
+ ### `DashboardOptions`
144
+
145
+ Every field optional, every default safe:
146
+
147
+ | Option | Default | Notes |
148
+ |---|---|---|
149
+ | `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`. |
150
+ | `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. |
151
+ | `basePath` | `"/"` | Mount-path prefix for both the page and every `/api/...` route, e.g. `"/panoptic"`. Normalized to leading + trailing slash. |
152
+ | `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. |
153
+ | `title` | `"Panoptic"` | Header title baked into the served page. |
154
+ | `authToken` | _unset_ | Bearer token required on every request (`Authorization: Bearer <token>` or `?token=`); missing/wrong → `401`. **Required** when `host` is non-loopback. |
155
+ | `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. |
156
+
157
+ ### `DashboardHandle`
158
+
159
+ ```ts
160
+ type DashboardHandle = {
161
+ readonly url: string; // http://host:port{basePath}
162
+ readonly port: number; // real OS port (when port: 0 was requested)
163
+ close(): Promise<void>; // stop the server, release the port
164
+ };
165
+ ```
166
+
167
+ ## The JSON API
168
+
169
+ All routes are **`GET`-only and read-only** (non-`GET` → `405`), mounted under `basePath`. The page polls them; you can curl them too. Store shapes are already JSON-safe, so responses are a plain `JSON.stringify`.
170
+
171
+ | Route | Returns |
172
+ |---|---|
173
+ | `GET /api/traces` | `store.query(...)` — the trace list, filtered by the query string (`id` / `sessionId` / `status` / time window, parsed by `parseQuery`). |
174
+ | `GET /api/traces/:id` | `store.get(id)` — one full trace, or `404 { error: "trace_not_found" }`. |
175
+ | `GET /api/aggregate` | `store.aggregate(...)` — the usage + cost + status rollup over the same query filter. |
176
+ | `GET /` (basePath) | the self-contained HTML page. |
177
+
178
+ Query-string filters map 1:1 onto `TraceQuery`:
179
+
180
+ ```sh
181
+ curl 'http://127.0.0.1:4319/api/traces?status=failed'
182
+ curl 'http://127.0.0.1:4319/api/aggregate?sessionId=sess_42'
183
+ ```
184
+
185
+ (The exact filter keys and the aggregate shape live in [`query-traces/SKILL.md`](@warlock.js/ai-panoptic/query-traces/SKILL.md).)
186
+
187
+ ## Advanced — build the handler yourself
188
+
189
+ `createRequestHandler(store, config)` is the bare `(req, res) => void` the server closes over — split out from the lifecycle so it unit-tests without binding a port, and so you can mount the same routes inside an existing server:
190
+
191
+ ```ts
192
+ import { createRequestHandler } from "@warlock.js/ai-panoptic";
193
+ import { createServer } from "node:http";
194
+
195
+ const handler = createRequestHandler(store, {
196
+ basePath: "/",
197
+ title: "Panoptic",
198
+ allowedHosts: ["127.0.0.1", "localhost"],
199
+ });
200
+ createServer(handler).listen(4319, "127.0.0.1");
201
+ ```
202
+
203
+ `dashboardHtml(basePath, title)` returns the served page as a string if you embed it elsewhere. `ServeConfig` is `{ basePath; title; allowedHosts; authToken? }`.
204
+
205
+ ## Security note
206
+
207
+ 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.
208
+
209
+ ## See also
210
+
211
+ - [`@warlock.js/ai-panoptic/observe-with-panoptic/SKILL.md`](@warlock.js/ai-panoptic/observe-with-panoptic/SKILL.md) — the `ai.config({ panoptic })` wiring and the underlying `panoptic(...)` subscriber that fills the store.
212
+ - [`@warlock.js/ai-panoptic/query-traces/SKILL.md`](@warlock.js/ai-panoptic/query-traces/SKILL.md) — the `TraceStoreContract` (`query` / `get` / `aggregate`) the dashboard reads, the `TraceQuery` filter, and capacity eviction.
213
+ - [`@warlock.js/ai-panoptic/export-traces/SKILL.md`](@warlock.js/ai-panoptic/export-traces/SKILL.md) — shipping traces to OTel / Langfuse / a file instead of (or alongside) the local dashboard.