@warlock.js/ai-panoptic 4.3.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 (124) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/LICENSE +21 -0
  3. package/README.md +111 -0
  4. package/cjs/index.cjs +1370 -0
  5. package/cjs/index.cjs.map +1 -0
  6. package/esm/collector/collector.d.mts +19 -0
  7. package/esm/collector/collector.d.mts.map +1 -0
  8. package/esm/collector/collector.mjs +89 -0
  9. package/esm/collector/collector.mjs.map +1 -0
  10. package/esm/collector/extract-span-attributes.d.mts +29 -0
  11. package/esm/collector/extract-span-attributes.d.mts.map +1 -0
  12. package/esm/collector/extract-span-attributes.mjs +76 -0
  13. package/esm/collector/extract-span-attributes.mjs.map +1 -0
  14. package/esm/collector/index.d.mts +5 -0
  15. package/esm/collector/normalize-error.d.mts +27 -0
  16. package/esm/collector/normalize-error.d.mts.map +1 -0
  17. package/esm/collector/normalize-error.mjs +49 -0
  18. package/esm/collector/normalize-error.mjs.map +1 -0
  19. package/esm/collector/report-to-span.d.mts +31 -0
  20. package/esm/collector/report-to-span.d.mts.map +1 -0
  21. package/esm/collector/report-to-span.mjs +53 -0
  22. package/esm/collector/report-to-span.mjs.map +1 -0
  23. package/esm/collector/report-to-trace.d.mts +35 -0
  24. package/esm/collector/report-to-trace.d.mts.map +1 -0
  25. package/esm/collector/report-to-trace.mjs +53 -0
  26. package/esm/collector/report-to-trace.mjs.map +1 -0
  27. package/esm/contracts/collector.contract.d.mts +63 -0
  28. package/esm/contracts/collector.contract.d.mts.map +1 -0
  29. package/esm/contracts/exporter.contract.d.mts +76 -0
  30. package/esm/contracts/exporter.contract.d.mts.map +1 -0
  31. package/esm/contracts/index.d.mts +3 -0
  32. package/esm/contracts/trace.type.d.mts +122 -0
  33. package/esm/contracts/trace.type.d.mts.map +1 -0
  34. package/esm/exporters/console/console-exporter.d.mts +25 -0
  35. package/esm/exporters/console/console-exporter.d.mts.map +1 -0
  36. package/esm/exporters/console/console-exporter.mjs +79 -0
  37. package/esm/exporters/console/console-exporter.mjs.map +1 -0
  38. package/esm/exporters/console/console-exporter.type.d.mts +36 -0
  39. package/esm/exporters/console/console-exporter.type.d.mts.map +1 -0
  40. package/esm/exporters/console/format-span-line.d.mts +17 -0
  41. package/esm/exporters/console/format-span-line.d.mts.map +1 -0
  42. package/esm/exporters/console/format-span-line.mjs +41 -0
  43. package/esm/exporters/console/format-span-line.mjs.map +1 -0
  44. package/esm/exporters/console/index.d.mts +3 -0
  45. package/esm/exporters/file/file-exporter.d.mts +23 -0
  46. package/esm/exporters/file/file-exporter.d.mts.map +1 -0
  47. package/esm/exporters/file/file-exporter.mjs +93 -0
  48. package/esm/exporters/file/file-exporter.mjs.map +1 -0
  49. package/esm/exporters/file/file-exporter.type.d.mts +39 -0
  50. package/esm/exporters/file/file-exporter.type.d.mts.map +1 -0
  51. package/esm/exporters/file/index.d.mts +2 -0
  52. package/esm/exporters/index.d.mts +12 -0
  53. package/esm/exporters/index.mjs +12 -0
  54. package/esm/exporters/langfuse/index.d.mts +2 -0
  55. package/esm/exporters/langfuse/index.mjs +3 -0
  56. package/esm/exporters/langfuse/langfuse-exporter.d.mts +27 -0
  57. package/esm/exporters/langfuse/langfuse-exporter.d.mts.map +1 -0
  58. package/esm/exporters/langfuse/langfuse-exporter.mjs +158 -0
  59. package/esm/exporters/langfuse/langfuse-exporter.mjs.map +1 -0
  60. package/esm/exporters/langfuse/langfuse-exporter.type.d.mts +90 -0
  61. package/esm/exporters/langfuse/langfuse-exporter.type.d.mts.map +1 -0
  62. package/esm/exporters/otel/index.d.mts +2 -0
  63. package/esm/exporters/otel/index.mjs +3 -0
  64. package/esm/exporters/otel/otel-exporter.d.mts +29 -0
  65. package/esm/exporters/otel/otel-exporter.d.mts.map +1 -0
  66. package/esm/exporters/otel/otel-exporter.mjs +142 -0
  67. package/esm/exporters/otel/otel-exporter.mjs.map +1 -0
  68. package/esm/exporters/otel/otel-exporter.type.d.mts +39 -0
  69. package/esm/exporters/otel/otel-exporter.type.d.mts.map +1 -0
  70. package/esm/exporters/utils/gen-ai-attributes.d.mts +65 -0
  71. package/esm/exporters/utils/gen-ai-attributes.d.mts.map +1 -0
  72. package/esm/exporters/utils/gen-ai-attributes.mjs +88 -0
  73. package/esm/exporters/utils/gen-ai-attributes.mjs.map +1 -0
  74. package/esm/exporters/utils/index.d.mts +3 -0
  75. package/esm/exporters/utils/total-cost.d.mts +20 -0
  76. package/esm/exporters/utils/total-cost.d.mts.map +1 -0
  77. package/esm/exporters/utils/total-cost.mjs +23 -0
  78. package/esm/exporters/utils/total-cost.mjs.map +1 -0
  79. package/esm/exporters/utils/walk-spans.d.mts +18 -0
  80. package/esm/exporters/utils/walk-spans.d.mts.map +1 -0
  81. package/esm/exporters/utils/walk-spans.mjs +21 -0
  82. package/esm/exporters/utils/walk-spans.mjs.map +1 -0
  83. package/esm/index.d.mts +31 -0
  84. package/esm/index.mjs +22 -0
  85. package/esm/panoptic/index.d.mts +4 -0
  86. package/esm/panoptic/panoptic-middleware.d.mts +38 -0
  87. package/esm/panoptic/panoptic-middleware.d.mts.map +1 -0
  88. package/esm/panoptic/panoptic-middleware.mjs +68 -0
  89. package/esm/panoptic/panoptic-middleware.mjs.map +1 -0
  90. package/esm/panoptic/panoptic-target.type.d.mts +53 -0
  91. package/esm/panoptic/panoptic-target.type.d.mts.map +1 -0
  92. package/esm/panoptic/panoptic.d.mts +37 -0
  93. package/esm/panoptic/panoptic.d.mts.map +1 -0
  94. package/esm/panoptic/panoptic.mjs +134 -0
  95. package/esm/panoptic/panoptic.mjs.map +1 -0
  96. package/esm/panoptic/panoptic.type.d.mts +100 -0
  97. package/esm/panoptic/panoptic.type.d.mts.map +1 -0
  98. package/esm/store/in-memory-trace-store.d.mts +32 -0
  99. package/esm/store/in-memory-trace-store.d.mts.map +1 -0
  100. package/esm/store/in-memory-trace-store.mjs +134 -0
  101. package/esm/store/in-memory-trace-store.mjs.map +1 -0
  102. package/esm/store/index.d.mts +6 -0
  103. package/esm/store/index.mjs +5 -0
  104. package/esm/store/match-trace.d.mts +21 -0
  105. package/esm/store/match-trace.d.mts.map +1 -0
  106. package/esm/store/match-trace.mjs +44 -0
  107. package/esm/store/match-trace.mjs.map +1 -0
  108. package/esm/store/sum-usage.d.mts +34 -0
  109. package/esm/store/sum-usage.d.mts.map +1 -0
  110. package/esm/store/sum-usage.mjs +65 -0
  111. package/esm/store/sum-usage.mjs.map +1 -0
  112. package/esm/store/trace-aggregate.type.d.mts +51 -0
  113. package/esm/store/trace-aggregate.type.d.mts.map +1 -0
  114. package/esm/store/trace-query.type.d.mts +45 -0
  115. package/esm/store/trace-query.type.d.mts.map +1 -0
  116. package/esm/store/trace-store.contract.d.mts +55 -0
  117. package/esm/store/trace-store.contract.d.mts.map +1 -0
  118. package/llms-full.txt +450 -0
  119. package/llms.txt +11 -0
  120. package/package.json +40 -0
  121. package/skills/README.md +17 -0
  122. package/skills/export-traces/SKILL.md +152 -0
  123. package/skills/observe-with-panoptic/SKILL.md +128 -0
  124. package/skills/query-traces/SKILL.md +152 -0
@@ -0,0 +1,152 @@
1
+ ---
2
+ name: export-traces
3
+ description: 'Send @warlock.js/ai-panoptic traces to an observability backend via pluggable exporters. Triggers: `consoleExporter`, `fileExporter`, `otelExporter`, `langfuseExporter`, `ExporterContract`, `toGenAiAttributes`, `walkSpans`, `totalCostUsd`, `GEN_AI_ATTRIBUTES`, `WARLOCK_ATTRIBUTES`, `reportToTrace`, `reportToSpan`, `extractSpanAttributes`, `normalizeError`, `formatSpanLine`, `createPanopticMiddleware`; ''export AI traces to OpenTelemetry'', ''send traces to Langfuse'', ''log traces to console / a file'', ''gen_ai semantic conventions'', ''write a custom exporter''; typical import `import { otelExporter, langfuseExporter, consoleExporter, fileExporter } from "@warlock.js/ai-panoptic"`. Skip: wiring the subscriber/collector into a run — `@warlock.js/ai-panoptic/observe-with-panoptic/SKILL.md`; the core report shape — `@warlock.js/ai/run-ai-agent/SKILL.md`.'
4
+ ---
5
+
6
+ # Export traces — the sink end of Panoptic
7
+
8
+ An **exporter** translates a vendor-neutral `Trace` (produced by the collector) into a backend's wire format. The collector fans every completed trace out to each registered exporter via the `ExporterContract`:
9
+
10
+ ```ts
11
+ export interface ExporterContract {
12
+ readonly name: string;
13
+ export(trace: Trace): void | Promise<void>;
14
+ exportSpan?(span: TraceSpan): void | Promise<void>; // optional live streaming
15
+ flush?(): void | Promise<void>;
16
+ shutdown?(): void | Promise<void>;
17
+ }
18
+ ```
19
+
20
+ Four exporters ship today. Each is a **factory** returning an `ExporterContract` — callers never `new`.
21
+
22
+ ## Pick an exporter
23
+
24
+ | Exporter | Backend | Deps | Use it for |
25
+ |---|---|---|---|
26
+ | `consoleExporter()` | stdout/stderr | none | local dev, CI logs |
27
+ | `fileExporter()` | JSON-Lines file | none | durable local sink, replay, out-of-band shipping |
28
+ | `otelExporter()` | OpenTelemetry | `@opentelemetry/api` (optional peer) | OTel-native stacks, GenAI semantic conventions |
29
+ | `langfuseExporter()` | Langfuse | `langfuse` (optional peer) | LLM-specific dashboards, generations |
30
+
31
+ > `@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.
32
+
33
+ ## Console
34
+
35
+ ```ts
36
+ import { consoleExporter } from "@warlock.js/ai-panoptic";
37
+
38
+ collector.use(consoleExporter()); // one summary line per trace
39
+ collector.use(consoleExporter({ tree: true })); // full indented span tree
40
+ collector.use(consoleExporter({ streaming: true })); // also print each span as it finalizes
41
+ ```
42
+
43
+ Failed/cancelled spans route to `console.error`; everything else to `console.log`. Inject a sink (`{ console: mySink }`) to redirect or to capture in tests.
44
+
45
+ ## File
46
+
47
+ ```ts
48
+ import { fileExporter } from "@warlock.js/ai-panoptic";
49
+
50
+ collector.use(fileExporter({ path: "storage/traces.jsonl" })); // write every trace
51
+ collector.use(fileExporter({ path: "...", flushEvery: 50 })); // batch 50 traces per write
52
+ collector.use(fileExporter({ path: "...", pretty: true })); // multi-line records
53
+ ```
54
+
55
+ 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.
56
+
57
+ ## OpenTelemetry
58
+
59
+ The exporter emits spans onto a `Tracer`; **the host app owns the SDK wiring** (`TracerProvider`, processors, span exporters), exactly as with any OTel instrumentation.
60
+
61
+ ```ts
62
+ import { otelExporter } from "@warlock.js/ai-panoptic";
63
+
64
+ // app already configured @opentelemetry/sdk-trace-base + a global provider
65
+ collector.use(otelExporter({ tracerName: "my-app", system: "openai" }));
66
+
67
+ // or pass a Tracer directly
68
+ collector.use(otelExporter({ tracer: myTracer }));
69
+ ```
70
+
71
+ Each `TraceSpan` becomes one OTel span with the source start/end times and parent relationship reconstructed, so the emitted tree matches the original execution tree. Attributes follow the [GenAI semantic conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/):
72
+
73
+ - `gen_ai.usage.input_tokens` / `gen_ai.usage.output_tokens` / `gen_ai.usage.total_tokens` (from the span's typed `usage` rollup)
74
+ - `gen_ai.usage.cached_tokens` / `gen_ai.usage.reasoning_tokens` (when the span reported them)
75
+ - `gen_ai.conversation.id` (from the span's `sessionId`)
76
+ - `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).
77
+ - `warlock.report.type` / `warlock.version` / `warlock.duration_ms` / `warlock.cost.usd`
78
+ - 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.
79
+
80
+ Failed/cancelled spans get OTel `ERROR` status plus a recorded exception.
81
+
82
+ ## Langfuse
83
+
84
+ ```ts
85
+ import { langfuseExporter } from "@warlock.js/ai-panoptic";
86
+
87
+ collector.use(langfuseExporter({ publicKey: "pk-...", secretKey: "sk-...", baseUrl: "..." }));
88
+ // or reuse an existing Langfuse client
89
+ collector.use(langfuseExporter({ client: myLangfuse }));
90
+ ```
91
+
92
+ - The root span becomes a Langfuse **trace**.
93
+ - A token-producing span (`usage.total > 0`) becomes a **generation** (carries `usage`); a pure tool/callback becomes a plain **span**.
94
+ - Children nest under their parent observation; timing, `version`, and status (failed → `ERROR` level) are mapped.
95
+ - `flush()` / `shutdown()` delegate to the client's `flushAsync()` / `shutdownAsync()` — call `collector.shutdown()` on teardown so buffered observations are sent.
96
+
97
+ ## Shared utilities
98
+
99
+ Reusable when authoring a **custom exporter**:
100
+
101
+ ```ts
102
+ import { walkSpans, toGenAiAttributes, totalCostUsd, GEN_AI_ATTRIBUTES, WARLOCK_ATTRIBUTES } from "@warlock.js/ai-panoptic";
103
+
104
+ for (const span of walkSpans(trace.root)) { // depth-first pre-order
105
+ const attributes = toGenAiAttributes(span); // gen_ai.* / warlock.* attribute map
106
+ const cost = totalCostUsd(span.usage); // single USD scalar, or undefined
107
+ }
108
+ ```
109
+
110
+ `toGenAiAttributes` copies only **scalar** entries from a span's free-form `attributes` bag (objects/arrays are skipped — backend attribute values must be primitives) and lets an explicit collector attribute override a derived one.
111
+
112
+ ## Write a custom exporter
113
+
114
+ Implement `ExporterContract` directly — `name` + `export` are the only required members:
115
+
116
+ ```ts
117
+ import type { ExporterContract, Trace } from "@warlock.js/ai-panoptic";
118
+
119
+ const myExporter: ExporterContract = {
120
+ name: "my-backend",
121
+ async export(trace: Trace) {
122
+ await sendSomewhere(trace);
123
+ },
124
+ };
125
+
126
+ collector.use(myExporter);
127
+ ```
128
+
129
+ Wrapping a third-party SDK? Lazily `await import(...)` it **inside** the export path (never at module top) so it stays an optional peer — copy the loader pattern from `otelExporter` / `langfuseExporter`. The collector isolates a throwing exporter, so one backend's failure never crashes the originating AI run.
130
+
131
+ ## Lower-level building blocks
132
+
133
+ Most apps only need `panoptic(...)`, an exporter, and the store. These lower-level exports exist for **custom collectors / exporters and bespoke pipelines** — each is a pure function over the report tree:
134
+
135
+ ```ts
136
+ import {
137
+ reportToTrace, // (report: BaseReport) => Trace — project a finished report into a vendor-neutral Trace
138
+ reportToSpan, // (report: BaseReport) => TraceSpan — project one report node into a span (recurses into children)
139
+ extractSpanAttributes, // (report: BaseReport) => Record<string, unknown> | undefined — the per-primitive attribute bag (agent.model.name, workflow.steps, …)
140
+ normalizeError, // (error: unknown) => TraceSpanError | undefined — coerce any thrown value into the typed span-error shape
141
+ formatSpanLine, // (span: TraceSpan, depth?: number) => string — render one indented console line (consoleExporter's tree mode uses this)
142
+ createPanopticMiddleware, // (collector: CollectorContract, name?: string) => AgentMiddleware — build a middleware from a collector you already hold (panoptic(...) wires this for you)
143
+ } from "@warlock.js/ai-panoptic";
144
+ ```
145
+
146
+ Reach for `reportToTrace` when you already hold a finished `result.report` and want a `Trace` without the live subscriber; `formatSpanLine` to build a custom console renderer; `createPanopticMiddleware` when you assemble the agent's `middleware` array by hand instead of letting `panoptic(...)` do it.
147
+
148
+ ## See also
149
+
150
+ - [`@warlock.js/ai-panoptic/observe-with-panoptic/SKILL.md`](@warlock.js/ai-panoptic/observe-with-panoptic/SKILL.md) — the source end: wiring the `panoptic(...)` subscriber into a run (`attach` / `middleware` / `collect`). The `collector` these exporters register on comes from there.
151
+ - [`@warlock.js/ai-panoptic/query-traces/SKILL.md`](@warlock.js/ai-panoptic/query-traces/SKILL.md) — 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.
152
+ - [`@warlock.js/ai/run-ai-agent/SKILL.md`](@warlock.js/ai/run-ai-agent/SKILL.md) — running the agent and the `BaseReport` shape Panoptic projects into a `Trace`.
@@ -0,0 +1,128 @@
1
+ ---
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`.'
4
+ ---
5
+
6
+ # Observe a run with Panoptic — the one-call subscriber
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.
9
+
10
+ ```ts
11
+ import { panoptic } from "@warlock.js/ai-panoptic";
12
+ import { consoleExporter, otelExporter } from "@warlock.js/ai-panoptic";
13
+
14
+ const observe = panoptic({
15
+ exporters: [consoleExporter(), otelExporter({ tracerName: "my-app" })],
16
+ });
17
+ ```
18
+
19
+ ## Three feed paths
20
+
21
+ | Path | Use when | API |
22
+ |---|---|---|
23
+ | **Events** | You want every run captured automatically and the primitive emits a `*.completed` event (agent / workflow / supervisor). | `observe.attach(target)` |
24
+ | **Middleware** | The app already composes cross-cutting concerns through the agent middleware pipeline. | `panoptic().middleware()` |
25
+ | **Direct** | The primitive carries no result-bearing event (the orchestrator), or you captured a report out of band. | `observe.collect(report)` |
26
+
27
+ ### 1. Attach to the event stream
28
+
29
+ `attach` subscribes to the terminal `*.completed` events and returns a **detach** function:
30
+
31
+ ```ts
32
+ const agent = ai.agent({ model });
33
+ const detach = observe.attach(agent); // agent | workflow | supervisor
34
+
35
+ await agent.execute("Summarize this");
36
+
37
+ // ...later, on teardown:
38
+ detach();
39
+ await observe.shutdown();
40
+ ```
41
+
42
+ By default Panoptic subscribes to `agent.completed`, `workflow.completed`, and `supervisor.completed` — the terminal events that carry the finalized `result.report`. These fire **once per run regardless of outcome** (the matching `*.error` fires first on failure, then `*.completed` still fires), so attaching captures completed, failed, and cancelled runs alike. Panoptic silently ignores the names a given target doesn't emit, so the same subscriber attaches to any of the three primitives.
43
+
44
+ Narrow or widen the surface with `completedEvents`:
45
+
46
+ ```ts
47
+ panoptic({ exporters, completedEvents: ["agent.completed"] }); // agents only
48
+ ```
49
+
50
+ ### 2. Wire it as agent middleware
51
+
52
+ ```ts
53
+ const observe = panoptic({ exporters: [langfuseExporter({ /* ... */ })] });
54
+
55
+ const agent = ai.agent({
56
+ model,
57
+ middleware: [observe.middleware()],
58
+ });
59
+ ```
60
+
61
+ The middleware feeds the collector from the `execute`-level `after` (success) and `onError` (failure) hooks. It never returns a value, so it never mutates the agent's result. Register more than one Panoptic middleware on the same agent by passing a distinct `middlewareName`:
62
+
63
+ ```ts
64
+ panoptic({ middlewareName: "panoptic-secondary" }).middleware();
65
+ ```
66
+
67
+ ### 3. Collect a report directly
68
+
69
+ The **orchestrator** emits `orchestrator.turn.*` events that carry only session identity — no result. Feed its report straight in:
70
+
71
+ ```ts
72
+ const result = await orchestrator.execute(input, { sessionId });
73
+ await observe.collect(result.report);
74
+ ```
75
+
76
+ `collect(report)` also covers any report captured out of band (e.g. replayed from a store).
77
+
78
+ ## Failure isolation
79
+
80
+ An observability fault never escapes onto the AI run's hot path:
81
+
82
+ - The collector wraps every exporter invocation in `Promise.allSettled`, so a throwing exporter can't crash the run or block its siblings.
83
+ - The event handler and the middleware additionally swallow the `collect` rejection, so even a collector-level fault stays contained.
84
+
85
+ A broken backend degrades to *missing telemetry*, never a failed agent run.
86
+
87
+ ## Lifecycle
88
+
89
+ ```ts
90
+ await observe.flush(); // drain buffered exporters (between batches / checkpoints)
91
+ await observe.shutdown(); // flush + release every exporter (process teardown)
92
+ ```
93
+
94
+ Call `shutdown()` on teardown so buffered exporters (`fileExporter` batches, the Langfuse client) send their last records before exit.
95
+
96
+ ## Bring your own collector or store
97
+
98
+ The in-memory trace store doubles as an `ExporterContract`, so the simplest way to retain runs for later querying is to pass it as an exporter — it then receives every trace alongside your other sinks:
99
+
100
+ ```ts
101
+ import { panoptic, createInMemoryTraceStore } from "@warlock.js/ai-panoptic";
102
+
103
+ const store = createInMemoryTraceStore(); // an ExporterContract + queryable store
104
+ const observe = panoptic({ exporters: [store, consoleExporter()] });
105
+
106
+ // store now receives every trace — query/aggregate it later:
107
+ store.query({ status: "failed" });
108
+ store.aggregate({ sessionId });
109
+ ```
110
+
111
+ Pass a pre-built `collector` instead when you want to share one collector across several subscribers (each subscriber's `attach` / `middleware` feeds the same exporters):
112
+
113
+ ```ts
114
+ import { panoptic, createCollector } from "@warlock.js/ai-panoptic";
115
+
116
+ const collector = createCollector().use(store);
117
+ const observe = panoptic({ collector, exporters: [consoleExporter()] });
118
+ ```
119
+
120
+ Add exporters after construction with `observe.use(exporter)` (chainable, deduped by `ExporterContract.name`). `observe.toTrace(report)` projects a report into a `Trace` **without** dispatching — handy for inspection or tests.
121
+
122
+ > The store's full query / aggregate / capacity surface is its own skill — see [`query-traces/SKILL.md`](@warlock.js/ai-panoptic/query-traces/SKILL.md).
123
+
124
+ ## See also
125
+
126
+ - [`@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
+ - [`@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
+ - [`@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.
@@ -0,0 +1,152 @@
1
+ ---
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`.'
4
+ ---
5
+
6
+ # Query traces — the in-memory queryable store
7
+
8
+ 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.
9
+
10
+ `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.
11
+
12
+ ```ts
13
+ import { panoptic, createInMemoryTraceStore } from "@warlock.js/ai-panoptic";
14
+
15
+ const store = createInMemoryTraceStore();
16
+
17
+ // The store doubles as an exporter — register it like any other sink.
18
+ const observe = panoptic({ exporters: [store] });
19
+
20
+ const agent = ai.agent({ model });
21
+ observe.attach(agent);
22
+
23
+ await agent.execute("Summarize this");
24
+
25
+ // ...later, off the hot path:
26
+ const failed = store.query({ status: "failed" });
27
+ const spend = store.aggregate({ sessionId: "session-42" });
28
+ ```
29
+
30
+ > The store is also a valid `collector.use(...)` target directly — `panoptic({ collector: createCollector().use(store) })` — see [`observe-with-panoptic/SKILL.md`](@warlock.js/ai-panoptic/observe-with-panoptic/SKILL.md) for bring-your-own-collector wiring.
31
+
32
+ ## The store contract
33
+
34
+ `TraceStoreContract` is the queryable surface; the in-memory implementation adds the `ExporterContract` `name` + `export` so it can be a sink.
35
+
36
+ ```ts
37
+ interface TraceStoreContract {
38
+ add(trace: Trace): void; // ingest; overwrites same traceId
39
+ get(traceId: string): Trace | undefined; // one run by id
40
+ query(filter?: TraceQuery): Trace[]; // matching traces, newest-started first
41
+ aggregate(filter?: TraceQuery): TraceAggregate; // usage + cost + status rollup
42
+ readonly size: number; // retained count
43
+ clear(): void; // drop everything
44
+ }
45
+ ```
46
+
47
+ - **`add` overwrites** any existing trace with the same `traceId` — re-collecting a run replaces rather than duplicates it.
48
+ - **`export(trace)` is an alias for `add`** — that is what makes the store a drop-in exporter (`store.name === "in-memory-trace-store"`).
49
+ - **`query` returns newest-started first** (sorted by root `startedAt`, descending). An empty / omitted filter returns every retained trace.
50
+
51
+ ## Filtering with `TraceQuery`
52
+
53
+ Every field is optional and **ANDed** together — an absent field is "don't care", so `{}` matches everything. The same filter type drives both `query` and `aggregate`.
54
+
55
+ ```ts
56
+ type TraceQuery = {
57
+ traceId?: string; // exact one run
58
+ sessionId?: string; // every run for a session
59
+ status?: ReportStatus | ReportStatus[]; // single status or membership set
60
+ startedAfter?: string | Date; // inclusive lower bound on root startedAt
61
+ startedBefore?: string | Date; // inclusive upper bound on root startedAt
62
+ };
63
+ ```
64
+
65
+ ```ts
66
+ // One run by id:
67
+ store.query({ traceId: "run-abc" });
68
+
69
+ // Every run for a conversation:
70
+ store.query({ sessionId: "session-42" });
71
+
72
+ // Failed or cancelled runs since midnight (status accepts an array):
73
+ store.query({
74
+ status: ["failed", "cancelled"],
75
+ startedAfter: "2026-06-18T00:00:00.000Z",
76
+ });
77
+
78
+ // A Date works anywhere a time bound is accepted:
79
+ store.query({ startedAfter: new Date(Date.now() - 3_600_000) }); // last hour
80
+ ```
81
+
82
+ Time bounds compare against each trace's root `startedAt`, **inclusive on both ends**, accepting either an ISO-8601 string or a `Date`. Status is exact equality (or array membership). The pure matcher behind this is `matchTrace(trace, filter)` — exported for testing or filtering a trace list you hold yourself.
83
+
84
+ ## Rolling up with `aggregate`
85
+
86
+ `aggregate(filter?)` answers the headline questions — how many runs, how many tokens, how much did it cost, how many failed — for whatever slice the filter selects:
87
+
88
+ ```ts
89
+ type TraceAggregate = {
90
+ traces: number; // matched count
91
+ completed: number; // root status === "completed"
92
+ failed: number; // root status === "failed"
93
+ cancelled: number; // root status === "cancelled"
94
+ usage: Usage; // summed token usage (input/output/total always present)
95
+ cost?: ModelPricing; // merged per-channel USD, or undefined if no trace was priced
96
+ totalDuration: number; // summed wall-clock ms
97
+ };
98
+ ```
99
+
100
+ ```ts
101
+ const stats = store.aggregate({ sessionId: "session-42" });
102
+
103
+ console.log(`${stats.traces} runs, ${stats.usage.total} tokens, ${stats.failed} failed`);
104
+
105
+ const totalUsd =
106
+ (stats.cost?.input ?? 0) +
107
+ (stats.cost?.output ?? 0) +
108
+ (stats.cost?.cachedInput ?? 0) +
109
+ (stats.cost?.cachedOutput ?? 0);
110
+ ```
111
+
112
+ Notes that matter when reading the numbers:
113
+
114
+ - **`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.
115
+ - **`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.
116
+ - **`completed` + `failed` + `cancelled` need not equal `traces`** — non-terminal statuses (`awaiting-input`, `max-iterations`) count toward `traces` but none of the three headline counters.
117
+ - An empty match returns a zeroed aggregate (`usage` all `0`, `cost` `undefined`, `totalDuration` `0`).
118
+
119
+ ## Bounding retention with `capacity`
120
+
121
+ Unbounded by default (keep everything until `clear()`). For a long-lived process, cap it — the oldest-ingested trace is evicted FIFO once the cap is exceeded:
122
+
123
+ ```ts
124
+ type InMemoryTraceStoreOptions = { capacity?: number }; // absent / 0 = unbounded
125
+
126
+ const store = createInMemoryTraceStore({ capacity: 1000 });
127
+ ```
128
+
129
+ Overwriting an existing `traceId` refreshes its insertion position, so a re-collected run is treated as newest for eviction — eviction stays honest under overwrite.
130
+
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
+
133
+ ## Summing usage yourself
134
+
135
+ `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):
136
+
137
+ ```ts
138
+ import { emptyUsage, sumUsage } from "@warlock.js/ai-panoptic";
139
+
140
+ let total = emptyUsage(); // { input: 0, output: 0, total: 0 } — optional channels absent
141
+ for (const trace of store.query({ sessionId })) {
142
+ total = sumUsage(total, trace.usage);
143
+ }
144
+ ```
145
+
146
+ `sumUsage` is pure (returns a fresh `Usage`, mutates neither argument): token channels always sum; the optional cache/reasoning channels sum only when a side reported them (so a never-metered channel stays absent rather than fabricating a `0`); `cost` merges with the framework's `accumulateCost` so an unpriced contributor never erases a priced one.
147
+
148
+ ## See also
149
+
150
+ - [`@warlock.js/ai-panoptic/observe-with-panoptic/SKILL.md`](@warlock.js/ai-panoptic/observe-with-panoptic/SKILL.md) — wiring the `panoptic(...)` subscriber and bring-your-own-collector so the store fills as runs complete.
151
+ - [`@warlock.js/ai-panoptic/export-traces/SKILL.md`](@warlock.js/ai-panoptic/export-traces/SKILL.md) — sending traces to an external backend instead of (or alongside) retaining them in memory; writing a custom sink.
152
+ - [`@warlock.js/ai/run-ai-agent/SKILL.md`](@warlock.js/ai/run-ai-agent/SKILL.md) — the `BaseReport` the collector projects into the `Trace` this store retains.