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