@warlock.js/ai-panoptic 5.24.0 → 5.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/llms-full.txt +869 -869
- package/package.json +4 -4
- package/skills/evaluate-system-prompt/SKILL.md +86 -86
- package/skills/export-traces/SKILL.md +184 -184
- package/skills/observe-with-panoptic/SKILL.md +175 -175
- package/skills/query-traces/SKILL.md +203 -203
- package/skills/use-local-dashboard/SKILL.md +221 -221
package/package.json
CHANGED
|
@@ -18,9 +18,9 @@
|
|
|
18
18
|
"peerDependencies": {
|
|
19
19
|
"@opentelemetry/api": "*",
|
|
20
20
|
"@opentelemetry/sdk-trace-base": "*",
|
|
21
|
-
"@warlock.js/ai": "5.
|
|
22
|
-
"@warlock.js/cache": "5.
|
|
23
|
-
"@warlock.js/logger": "5.
|
|
21
|
+
"@warlock.js/ai": "5.25.0",
|
|
22
|
+
"@warlock.js/cache": "5.25.0",
|
|
23
|
+
"@warlock.js/logger": "5.25.0",
|
|
24
24
|
"langfuse": "*"
|
|
25
25
|
},
|
|
26
26
|
"peerDependenciesMeta": {
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"langfuse": "^3.38.20"
|
|
42
42
|
},
|
|
43
|
-
"version": "5.
|
|
43
|
+
"version": "5.25.0",
|
|
44
44
|
"main": "./cjs/index.cjs",
|
|
45
45
|
"module": "./esm/index.mjs",
|
|
46
46
|
"types": "./esm/index.d.mts",
|
|
@@ -1,86 +1,86 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: evaluate-system-prompt
|
|
3
|
-
description: 'Grade a single trace''s LAST captured system prompt from the dashboard drawer — the dashboard''s only write-capable feature, config-gated via `DashboardOptions.evaluate` (`ai.config({ panoptic: { dashboard: { evaluate: { model, instructions } } } })`). Triggers: `evaluate`, `EvaluateConfig`, `EvaluateVerdict`, `evaluateSystemPrompt`, `extractLastSystemPrompt`, `findSpanById`, "Evaluate system prompt" button, `POST .../spans/:spanId/evaluate`, grading a prompt from a real trace, judging a captured system prompt, per-run instructions override, judge model factory; ''evaluate this trace''s prompt'', ''grade the system prompt from the dashboard'', ''judge a captured run'', ''score a live system prompt''. Skip: grading raw prompt TEXT you already have in code (no trace involved) — `@warlock.js/ai`''s `ai.prompts().validate({ criteria })`, the same judge machinery this wraps; a DATASET of test cases run through an agent — `@warlock.js/ai`''s `ai.dataset` + `agent.eval`; viewing/filtering/grouping traces without grading them — `@warlock.js/ai-panoptic/use-local-dashboard/SKILL.md`; capturing the content this feature reads — `@warlock.js/ai-panoptic/export-traces/SKILL.md` (`captureContent`).'
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Evaluate a trace's system prompt from the dashboard
|
|
7
|
-
|
|
8
|
-
A drawer button that grades the LAST `{role: "system"}` message captured on the selected span — not the whole conversation, not the tool code, just the system prompt that was actually in effect for that run. It's the dashboard's only write-capable feature: every other route is `GET`-only and read-only by design (see `use-local-dashboard/SKILL.md`'s security note); this one POSTs, and it's off unless you explicitly opt in.
|
|
9
|
-
|
|
10
|
-
## Enable it
|
|
11
|
-
|
|
12
|
-
```ts
|
|
13
|
-
import { ai } from "@warlock.js/ai";
|
|
14
|
-
import "@warlock.js/ai-panoptic";
|
|
15
|
-
|
|
16
|
-
ai.config({
|
|
17
|
-
panoptic: {
|
|
18
|
-
dashboard: {
|
|
19
|
-
evaluate: {
|
|
20
|
-
model: openai.model("gpt-4o-mini"),
|
|
21
|
-
instructions: "Must address the user by name. Never invent prices.",
|
|
22
|
-
},
|
|
23
|
-
},
|
|
24
|
-
observeAll: true,
|
|
25
|
-
captureContent: true, // required — evaluate reads the captured span.input
|
|
26
|
-
},
|
|
27
|
-
});
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
Absent `evaluate` ⇒ the drawer never renders the button and a `POST` to its route 405s like any other route would (the path is never even pattern-matched) — the dashboard stays fully read-only unless you opt in.
|
|
31
|
-
|
|
32
|
-
### `EvaluateConfig`
|
|
33
|
-
|
|
34
|
-
| Field | Required | Notes |
|
|
35
|
-
|---|---|---|
|
|
36
|
-
| `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). |
|
|
37
|
-
| `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. |
|
|
38
|
-
|
|
39
|
-
## The UI
|
|
40
|
-
|
|
41
|
-
Open a trace, select the span whose prompt you want graded (any span carrying a captured `[system, user]` pair or `fullHistory` message array — an agent span, not a tool span). An "Evaluate system prompt" button appears under the input/output block **only when that span actually has a system prompt to grade** (`extractLastSystemPrompt` finds one) — no dead button on tool spans or system-prompt-less runs.
|
|
42
|
-
|
|
43
|
-
Click it to open an inline panel: a textarea pre-filled with the configured `instructions` (fully editable — type over it, append to it, or clear it to fall back to the built-in rubric) and a "Run" button. The result — `score` (0–1, shown as a percentage) and `issues` (the judge's reasoning, as a list) — renders inline once it lands. State (open/closed, typed text, last result) is kept per-span in client memory, so switching to a sibling span and back doesn't lose it; it does NOT persist to the trace store or survive a page reload.
|
|
44
|
-
|
|
45
|
-
## The building blocks (for embedding elsewhere)
|
|
46
|
-
|
|
47
|
-
```ts
|
|
48
|
-
import { evaluateSystemPrompt, extractLastSystemPrompt, findSpanById } from "@warlock.js/ai-panoptic";
|
|
49
|
-
|
|
50
|
-
const span = findSpanById(trace.root, spanId); // walk the span tree
|
|
51
|
-
const prompt = extractLastSystemPrompt(span); // undefined if none captured
|
|
52
|
-
if (prompt) {
|
|
53
|
-
const verdict = await evaluateSystemPrompt(prompt, evaluateConfig, "Per-run override, optional");
|
|
54
|
-
// verdict: { score?: number; issues: string[] }
|
|
55
|
-
}
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
- **`extractLastSystemPrompt(span)`** — reads `span.input` (only present under `captureContent`). Handles both shapes `report-to-span.ts` produces: the `[system, user]` first-trip pair, and — under `fullHistory` — the full `CapturedMessage[]` conversation, where it takes the LAST system-role turn (a long-running agent can carry more than one; the most recent one is the one actually in effect). Returns `undefined` for a tool span, an agent with no system prompt, or when content capture is off.
|
|
59
|
-
- **`findSpanById(root, spanId)`** — depth-first lookup inside a trace's span tree; the route uses it to resolve which node the drawer had selected.
|
|
60
|
-
- **`evaluateSystemPrompt(text, config, instructionsOverride?)`** — the grading call. `instructionsOverride` (trimmed; blank-after-trim is treated as absent, never as "grade against nothing") wins over `config.instructions`. Reuses `@warlock.js/ai`'s `judgePromptBody` verbatim — the SAME LLM-as-judge machinery `ai.prompts().validate({ criteria })` runs — so there's exactly one judging implementation in the whole framework, not two. `judgePromptBody` itself never throws (a broken judge degrades to an issues-only outcome with no `score`); the only step that CAN throw is resolving `config.model` when it's a factory (e.g. constructing an SDK client) — the dashboard route catches that and returns `502`.
|
|
61
|
-
|
|
62
|
-
## The route
|
|
63
|
-
|
|
64
|
-
`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.
|
|
65
|
-
|
|
66
|
-
| Status | Meaning |
|
|
67
|
-
|---|---|
|
|
68
|
-
| `200` | `EvaluateVerdict` — `{ score?: number; issues: string[] }` |
|
|
69
|
-
| `404` | trace or span not found |
|
|
70
|
-
| `422` | the span carried no captured system prompt to grade |
|
|
71
|
-
| `413` | request body over 64 KB |
|
|
72
|
-
| `400` | request body isn't valid JSON |
|
|
73
|
-
| `502` | `config.model` (a factory) threw resolving the judge model |
|
|
74
|
-
| `405` | POST hit this path but `evaluate` isn't configured |
|
|
75
|
-
|
|
76
|
-
```sh
|
|
77
|
-
curl -X POST 'http://127.0.0.1:4319/api/traces/tr_1/spans/sp_1/evaluate' \
|
|
78
|
-
-H 'content-type: application/json' \
|
|
79
|
-
-d '{"instructions": "Must never invent prices."}'
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
## See also
|
|
83
|
-
|
|
84
|
-
- [`@warlock.js/ai-panoptic/use-local-dashboard/SKILL.md`](@warlock.js/ai-panoptic/use-local-dashboard/SKILL.md) — the dashboard this feature is a drawer action of; its security note now says "every route is GET-only and read-only EXCEPT the one opt-in write route `evaluate` enables."
|
|
85
|
-
- [`@warlock.js/ai-panoptic/export-traces/SKILL.md`](@warlock.js/ai-panoptic/export-traces/SKILL.md) — `captureContent` (and `fullHistory`), the prerequisite for `span.input` to carry anything gradable.
|
|
86
|
-
- `ai.prompts().validate({ criteria })` in `@warlock.js/ai` — the sibling capability this reuses: grading raw prompt TEXT you already have in code, no trace or dashboard involved.
|
|
1
|
+
---
|
|
2
|
+
name: evaluate-system-prompt
|
|
3
|
+
description: 'Grade a single trace''s LAST captured system prompt from the dashboard drawer — the dashboard''s only write-capable feature, config-gated via `DashboardOptions.evaluate` (`ai.config({ panoptic: { dashboard: { evaluate: { model, instructions } } } })`). Triggers: `evaluate`, `EvaluateConfig`, `EvaluateVerdict`, `evaluateSystemPrompt`, `extractLastSystemPrompt`, `findSpanById`, "Evaluate system prompt" button, `POST .../spans/:spanId/evaluate`, grading a prompt from a real trace, judging a captured system prompt, per-run instructions override, judge model factory; ''evaluate this trace''s prompt'', ''grade the system prompt from the dashboard'', ''judge a captured run'', ''score a live system prompt''. Skip: grading raw prompt TEXT you already have in code (no trace involved) — `@warlock.js/ai`''s `ai.prompts().validate({ criteria })`, the same judge machinery this wraps; a DATASET of test cases run through an agent — `@warlock.js/ai`''s `ai.dataset` + `agent.eval`; viewing/filtering/grouping traces without grading them — `@warlock.js/ai-panoptic/use-local-dashboard/SKILL.md`; capturing the content this feature reads — `@warlock.js/ai-panoptic/export-traces/SKILL.md` (`captureContent`).'
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Evaluate a trace's system prompt from the dashboard
|
|
7
|
+
|
|
8
|
+
A drawer button that grades the LAST `{role: "system"}` message captured on the selected span — not the whole conversation, not the tool code, just the system prompt that was actually in effect for that run. It's the dashboard's only write-capable feature: every other route is `GET`-only and read-only by design (see `use-local-dashboard/SKILL.md`'s security note); this one POSTs, and it's off unless you explicitly opt in.
|
|
9
|
+
|
|
10
|
+
## Enable it
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { ai } from "@warlock.js/ai";
|
|
14
|
+
import "@warlock.js/ai-panoptic";
|
|
15
|
+
|
|
16
|
+
ai.config({
|
|
17
|
+
panoptic: {
|
|
18
|
+
dashboard: {
|
|
19
|
+
evaluate: {
|
|
20
|
+
model: openai.model("gpt-4o-mini"),
|
|
21
|
+
instructions: "Must address the user by name. Never invent prices.",
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
observeAll: true,
|
|
25
|
+
captureContent: true, // required — evaluate reads the captured span.input
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Absent `evaluate` ⇒ the drawer never renders the button and a `POST` to its route 405s like any other route would (the path is never even pattern-matched) — the dashboard stays fully read-only unless you opt in.
|
|
31
|
+
|
|
32
|
+
### `EvaluateConfig`
|
|
33
|
+
|
|
34
|
+
| Field | Required | Notes |
|
|
35
|
+
|---|---|---|
|
|
36
|
+
| `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). |
|
|
37
|
+
| `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. |
|
|
38
|
+
|
|
39
|
+
## The UI
|
|
40
|
+
|
|
41
|
+
Open a trace, select the span whose prompt you want graded (any span carrying a captured `[system, user]` pair or `fullHistory` message array — an agent span, not a tool span). An "Evaluate system prompt" button appears under the input/output block **only when that span actually has a system prompt to grade** (`extractLastSystemPrompt` finds one) — no dead button on tool spans or system-prompt-less runs.
|
|
42
|
+
|
|
43
|
+
Click it to open an inline panel: a textarea pre-filled with the configured `instructions` (fully editable — type over it, append to it, or clear it to fall back to the built-in rubric) and a "Run" button. The result — `score` (0–1, shown as a percentage) and `issues` (the judge's reasoning, as a list) — renders inline once it lands. State (open/closed, typed text, last result) is kept per-span in client memory, so switching to a sibling span and back doesn't lose it; it does NOT persist to the trace store or survive a page reload.
|
|
44
|
+
|
|
45
|
+
## The building blocks (for embedding elsewhere)
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import { evaluateSystemPrompt, extractLastSystemPrompt, findSpanById } from "@warlock.js/ai-panoptic";
|
|
49
|
+
|
|
50
|
+
const span = findSpanById(trace.root, spanId); // walk the span tree
|
|
51
|
+
const prompt = extractLastSystemPrompt(span); // undefined if none captured
|
|
52
|
+
if (prompt) {
|
|
53
|
+
const verdict = await evaluateSystemPrompt(prompt, evaluateConfig, "Per-run override, optional");
|
|
54
|
+
// verdict: { score?: number; issues: string[] }
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
- **`extractLastSystemPrompt(span)`** — reads `span.input` (only present under `captureContent`). Handles both shapes `report-to-span.ts` produces: the `[system, user]` first-trip pair, and — under `fullHistory` — the full `CapturedMessage[]` conversation, where it takes the LAST system-role turn (a long-running agent can carry more than one; the most recent one is the one actually in effect). Returns `undefined` for a tool span, an agent with no system prompt, or when content capture is off.
|
|
59
|
+
- **`findSpanById(root, spanId)`** — depth-first lookup inside a trace's span tree; the route uses it to resolve which node the drawer had selected.
|
|
60
|
+
- **`evaluateSystemPrompt(text, config, instructionsOverride?)`** — the grading call. `instructionsOverride` (trimmed; blank-after-trim is treated as absent, never as "grade against nothing") wins over `config.instructions`. Reuses `@warlock.js/ai`'s `judgePromptBody` verbatim — the SAME LLM-as-judge machinery `ai.prompts().validate({ criteria })` runs — so there's exactly one judging implementation in the whole framework, not two. `judgePromptBody` itself never throws (a broken judge degrades to an issues-only outcome with no `score`); the only step that CAN throw is resolving `config.model` when it's a factory (e.g. constructing an SDK client) — the dashboard route catches that and returns `502`.
|
|
61
|
+
|
|
62
|
+
## The route
|
|
63
|
+
|
|
64
|
+
`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.
|
|
65
|
+
|
|
66
|
+
| Status | Meaning |
|
|
67
|
+
|---|---|
|
|
68
|
+
| `200` | `EvaluateVerdict` — `{ score?: number; issues: string[] }` |
|
|
69
|
+
| `404` | trace or span not found |
|
|
70
|
+
| `422` | the span carried no captured system prompt to grade |
|
|
71
|
+
| `413` | request body over 64 KB |
|
|
72
|
+
| `400` | request body isn't valid JSON |
|
|
73
|
+
| `502` | `config.model` (a factory) threw resolving the judge model |
|
|
74
|
+
| `405` | POST hit this path but `evaluate` isn't configured |
|
|
75
|
+
|
|
76
|
+
```sh
|
|
77
|
+
curl -X POST 'http://127.0.0.1:4319/api/traces/tr_1/spans/sp_1/evaluate' \
|
|
78
|
+
-H 'content-type: application/json' \
|
|
79
|
+
-d '{"instructions": "Must never invent prices."}'
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## See also
|
|
83
|
+
|
|
84
|
+
- [`@warlock.js/ai-panoptic/use-local-dashboard/SKILL.md`](@warlock.js/ai-panoptic/use-local-dashboard/SKILL.md) — the dashboard this feature is a drawer action of; its security note now says "every route is GET-only and read-only EXCEPT the one opt-in write route `evaluate` enables."
|
|
85
|
+
- [`@warlock.js/ai-panoptic/export-traces/SKILL.md`](@warlock.js/ai-panoptic/export-traces/SKILL.md) — `captureContent` (and `fullHistory`), the prerequisite for `span.input` to carry anything gradable.
|
|
86
|
+
- `ai.prompts().validate({ criteria })` in `@warlock.js/ai` — the sibling capability this reuses: grading raw prompt TEXT you already have in code, no trace or dashboard involved.
|
|
@@ -1,184 +1,184 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: export-traces
|
|
3
|
-
description: 'Send @warlock.js/ai-panoptic traces to an observability backend via pluggable exporters, including capturing prompt/response content onto spans. Triggers: `consoleExporter`, `fileExporter`, `otelExporter`, `langfuseExporter`, `ExporterContract`, `ContentCaptureOptions`, `captureContent`, `redactContent`, `fullHistory`, `ContentRedactor`, `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'', ''capture the full prompt/conversation onto a span'', ''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
|
-
## Content capture — what reaches the `input` / `output` fields
|
|
98
|
-
|
|
99
|
-
Exporters surface `span.input` / `span.output` (console `in:` / `out:`, file JSON, OTel `gen_ai.prompt` / `gen_ai.completion`, Langfuse native `input` / `output`) only when **content capture** is enabled on the collector or subscriber. It's `ContentCaptureOptions`, set on `createCollector(...)` or `panoptic(...)`:
|
|
100
|
-
|
|
101
|
-
```ts
|
|
102
|
-
panoptic({
|
|
103
|
-
exporters: [langfuseExporter({ /* ... */ })],
|
|
104
|
-
captureContent: true, // copy prompts/responses + tool args/results onto spans
|
|
105
|
-
redactContent: (value, { field }) => // optional masker, per value
|
|
106
|
-
field === "input" ? "[redacted]" : value,
|
|
107
|
-
fullHistory: true, // emit the WHOLE conversation as span.input (see below)
|
|
108
|
-
});
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
Off by default — payloads are large and frequently sensitive. With `captureContent`, an agent span's `input` is a `[system, user]` chat array (or the bare user string when there's no system prompt) and its `output` is the last non-empty trip's text; a tool span carries its call args + result.
|
|
112
|
-
|
|
113
|
-
### `fullHistory` — emit the entire `CapturedMessage[]`
|
|
114
|
-
|
|
115
|
-
`fullHistory: true` replaces the `[system, user]` first-trip array on `span.input` with the agent's **complete assembled conversation** — every trip, every role (system / user / assistant / tool). The `output` stays the final response text.
|
|
116
|
-
|
|
117
|
-
```ts
|
|
118
|
-
// upstream: the agent run MUST opt into capturing its messages
|
|
119
|
-
const result = await agent.execute(input, { captureMessages: true });
|
|
120
|
-
// ^ core @warlock.js/ai — makes report.messages present
|
|
121
|
-
|
|
122
|
-
panoptic({ exporters: [...], captureContent: true, fullHistory: true });
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
**Requires the upstream `captureMessages`.** `fullHistory` reads `report.messages` (the captured `CapturedMessage[]`). When the run did NOT opt into `captureMessages`, `report.messages` is absent and the branch **degrades gracefully** to today's first-trip `[system, user]` capture — never an error, just less history. The whole array is passed through `ContentRedactor` as a single value, so masking still applies to captured history.
|
|
126
|
-
|
|
127
|
-
Reach for `fullHistory` when a backend (Langfuse) should show the full multi-turn / tool-loop transcript a run actually sent, not just its opening prompt.
|
|
128
|
-
|
|
129
|
-
## Shared utilities
|
|
130
|
-
|
|
131
|
-
Reusable when authoring a **custom exporter**:
|
|
132
|
-
|
|
133
|
-
```ts
|
|
134
|
-
import { walkSpans, toGenAiAttributes, totalCostUsd, GEN_AI_ATTRIBUTES, WARLOCK_ATTRIBUTES } from "@warlock.js/ai-panoptic";
|
|
135
|
-
|
|
136
|
-
for (const span of walkSpans(trace.root)) { // depth-first pre-order
|
|
137
|
-
const attributes = toGenAiAttributes(span); // gen_ai.* / warlock.* attribute map
|
|
138
|
-
const cost = totalCostUsd(span.usage); // single USD scalar, or undefined
|
|
139
|
-
}
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
`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.
|
|
143
|
-
|
|
144
|
-
## Write a custom exporter
|
|
145
|
-
|
|
146
|
-
Implement `ExporterContract` directly — `name` + `export` are the only required members:
|
|
147
|
-
|
|
148
|
-
```ts
|
|
149
|
-
import type { ExporterContract, Trace } from "@warlock.js/ai-panoptic";
|
|
150
|
-
|
|
151
|
-
const myExporter: ExporterContract = {
|
|
152
|
-
name: "my-backend",
|
|
153
|
-
async export(trace: Trace) {
|
|
154
|
-
await sendSomewhere(trace);
|
|
155
|
-
},
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
collector.use(myExporter);
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
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.
|
|
162
|
-
|
|
163
|
-
## Lower-level building blocks
|
|
164
|
-
|
|
165
|
-
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:
|
|
166
|
-
|
|
167
|
-
```ts
|
|
168
|
-
import {
|
|
169
|
-
reportToTrace, // (report: BaseReport) => Trace — project a finished report into a vendor-neutral Trace
|
|
170
|
-
reportToSpan, // (report: BaseReport) => TraceSpan — project one report node into a span (recurses into children)
|
|
171
|
-
extractSpanAttributes, // (report: BaseReport) => Record<string, unknown> | undefined — the per-primitive attribute bag (agent.model.name, workflow.steps, …)
|
|
172
|
-
normalizeError, // (error: unknown) => TraceSpanError | undefined — coerce any thrown value into the typed span-error shape
|
|
173
|
-
formatSpanLine, // (span: TraceSpan, depth?: number) => string — render one indented console line (consoleExporter's tree mode uses this)
|
|
174
|
-
createPanopticMiddleware, // (collector: CollectorContract, name?: string) => AgentMiddleware — build a middleware from a collector you already hold (panoptic(...) wires this for you)
|
|
175
|
-
} from "@warlock.js/ai-panoptic";
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
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.
|
|
179
|
-
|
|
180
|
-
## See also
|
|
181
|
-
|
|
182
|
-
- [`@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.
|
|
183
|
-
- [`@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.
|
|
184
|
-
- [`@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`.
|
|
1
|
+
---
|
|
2
|
+
name: export-traces
|
|
3
|
+
description: 'Send @warlock.js/ai-panoptic traces to an observability backend via pluggable exporters, including capturing prompt/response content onto spans. Triggers: `consoleExporter`, `fileExporter`, `otelExporter`, `langfuseExporter`, `ExporterContract`, `ContentCaptureOptions`, `captureContent`, `redactContent`, `fullHistory`, `ContentRedactor`, `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'', ''capture the full prompt/conversation onto a span'', ''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
|
+
## Content capture — what reaches the `input` / `output` fields
|
|
98
|
+
|
|
99
|
+
Exporters surface `span.input` / `span.output` (console `in:` / `out:`, file JSON, OTel `gen_ai.prompt` / `gen_ai.completion`, Langfuse native `input` / `output`) only when **content capture** is enabled on the collector or subscriber. It's `ContentCaptureOptions`, set on `createCollector(...)` or `panoptic(...)`:
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
panoptic({
|
|
103
|
+
exporters: [langfuseExporter({ /* ... */ })],
|
|
104
|
+
captureContent: true, // copy prompts/responses + tool args/results onto spans
|
|
105
|
+
redactContent: (value, { field }) => // optional masker, per value
|
|
106
|
+
field === "input" ? "[redacted]" : value,
|
|
107
|
+
fullHistory: true, // emit the WHOLE conversation as span.input (see below)
|
|
108
|
+
});
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Off by default — payloads are large and frequently sensitive. With `captureContent`, an agent span's `input` is a `[system, user]` chat array (or the bare user string when there's no system prompt) and its `output` is the last non-empty trip's text; a tool span carries its call args + result.
|
|
112
|
+
|
|
113
|
+
### `fullHistory` — emit the entire `CapturedMessage[]`
|
|
114
|
+
|
|
115
|
+
`fullHistory: true` replaces the `[system, user]` first-trip array on `span.input` with the agent's **complete assembled conversation** — every trip, every role (system / user / assistant / tool). The `output` stays the final response text.
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
// upstream: the agent run MUST opt into capturing its messages
|
|
119
|
+
const result = await agent.execute(input, { captureMessages: true });
|
|
120
|
+
// ^ core @warlock.js/ai — makes report.messages present
|
|
121
|
+
|
|
122
|
+
panoptic({ exporters: [...], captureContent: true, fullHistory: true });
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
**Requires the upstream `captureMessages`.** `fullHistory` reads `report.messages` (the captured `CapturedMessage[]`). When the run did NOT opt into `captureMessages`, `report.messages` is absent and the branch **degrades gracefully** to today's first-trip `[system, user]` capture — never an error, just less history. The whole array is passed through `ContentRedactor` as a single value, so masking still applies to captured history.
|
|
126
|
+
|
|
127
|
+
Reach for `fullHistory` when a backend (Langfuse) should show the full multi-turn / tool-loop transcript a run actually sent, not just its opening prompt.
|
|
128
|
+
|
|
129
|
+
## Shared utilities
|
|
130
|
+
|
|
131
|
+
Reusable when authoring a **custom exporter**:
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
import { walkSpans, toGenAiAttributes, totalCostUsd, GEN_AI_ATTRIBUTES, WARLOCK_ATTRIBUTES } from "@warlock.js/ai-panoptic";
|
|
135
|
+
|
|
136
|
+
for (const span of walkSpans(trace.root)) { // depth-first pre-order
|
|
137
|
+
const attributes = toGenAiAttributes(span); // gen_ai.* / warlock.* attribute map
|
|
138
|
+
const cost = totalCostUsd(span.usage); // single USD scalar, or undefined
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
`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.
|
|
143
|
+
|
|
144
|
+
## Write a custom exporter
|
|
145
|
+
|
|
146
|
+
Implement `ExporterContract` directly — `name` + `export` are the only required members:
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
import type { ExporterContract, Trace } from "@warlock.js/ai-panoptic";
|
|
150
|
+
|
|
151
|
+
const myExporter: ExporterContract = {
|
|
152
|
+
name: "my-backend",
|
|
153
|
+
async export(trace: Trace) {
|
|
154
|
+
await sendSomewhere(trace);
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
collector.use(myExporter);
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
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.
|
|
162
|
+
|
|
163
|
+
## Lower-level building blocks
|
|
164
|
+
|
|
165
|
+
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:
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
import {
|
|
169
|
+
reportToTrace, // (report: BaseReport) => Trace — project a finished report into a vendor-neutral Trace
|
|
170
|
+
reportToSpan, // (report: BaseReport) => TraceSpan — project one report node into a span (recurses into children)
|
|
171
|
+
extractSpanAttributes, // (report: BaseReport) => Record<string, unknown> | undefined — the per-primitive attribute bag (agent.model.name, workflow.steps, …)
|
|
172
|
+
normalizeError, // (error: unknown) => TraceSpanError | undefined — coerce any thrown value into the typed span-error shape
|
|
173
|
+
formatSpanLine, // (span: TraceSpan, depth?: number) => string — render one indented console line (consoleExporter's tree mode uses this)
|
|
174
|
+
createPanopticMiddleware, // (collector: CollectorContract, name?: string) => AgentMiddleware — build a middleware from a collector you already hold (panoptic(...) wires this for you)
|
|
175
|
+
} from "@warlock.js/ai-panoptic";
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
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.
|
|
179
|
+
|
|
180
|
+
## See also
|
|
181
|
+
|
|
182
|
+
- [`@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.
|
|
183
|
+
- [`@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.
|
|
184
|
+
- [`@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`.
|