@warlock.js/ai-panoptic 4.8.0 → 4.8.2

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 (41) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/cjs/index.cjs +312 -14
  3. package/cjs/index.cjs.map +1 -1
  4. package/esm/config/apply-panoptic-config.d.mts.map +1 -1
  5. package/esm/config/apply-panoptic-config.mjs +3 -1
  6. package/esm/config/apply-panoptic-config.mjs.map +1 -1
  7. package/esm/config/panoptic-config.type.d.mts +11 -0
  8. package/esm/config/panoptic-config.type.d.mts.map +1 -1
  9. package/esm/dashboard/dashboard.mjs +2 -1
  10. package/esm/dashboard/dashboard.mjs.map +1 -1
  11. package/esm/dashboard/dashboard.type.d.mts +15 -2
  12. package/esm/dashboard/dashboard.type.d.mts.map +1 -1
  13. package/esm/dashboard/index.mjs +7 -0
  14. package/esm/dashboard/serve.mjs +123 -8
  15. package/esm/dashboard/serve.mjs.map +1 -1
  16. package/esm/dashboard/ui.html.mjs +133 -4
  17. package/esm/dashboard/ui.html.mjs.map +1 -1
  18. package/esm/evaluate/evaluate-system-prompt.d.mts +19 -0
  19. package/esm/evaluate/evaluate-system-prompt.d.mts.map +1 -0
  20. package/esm/evaluate/evaluate-system-prompt.mjs +22 -0
  21. package/esm/evaluate/evaluate-system-prompt.mjs.map +1 -0
  22. package/esm/evaluate/evaluate.type.d.mts +47 -0
  23. package/esm/evaluate/evaluate.type.d.mts.map +1 -0
  24. package/esm/evaluate/extract-last-system-prompt.d.mts +19 -0
  25. package/esm/evaluate/extract-last-system-prompt.d.mts.map +1 -0
  26. package/esm/evaluate/extract-last-system-prompt.mjs +24 -0
  27. package/esm/evaluate/extract-last-system-prompt.mjs.map +1 -0
  28. package/esm/evaluate/find-span-by-id.d.mts +8 -0
  29. package/esm/evaluate/find-span-by-id.d.mts.map +1 -0
  30. package/esm/evaluate/find-span-by-id.mjs +13 -0
  31. package/esm/evaluate/find-span-by-id.mjs.map +1 -0
  32. package/esm/evaluate/index.d.mts +4 -0
  33. package/esm/evaluate/index.mjs +5 -0
  34. package/esm/index.d.mts +5 -1
  35. package/esm/index.mjs +6 -1
  36. package/llms-full.txt +95 -3
  37. package/llms.txt +1 -0
  38. package/package.json +4 -3
  39. package/skills/README.md +4 -0
  40. package/skills/evaluate-system-prompt/SKILL.md +86 -0
  41. package/skills/use-local-dashboard/SKILL.md +5 -3
package/CHANGELOG.md CHANGED
@@ -4,6 +4,18 @@ All notable changes to `@warlock.js/ai-panoptic` are documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). `@warlock.js/*` packages are released in lockstep — every package shares the same version number, so a version below may list only the changes that affected this package.
6
6
 
7
+ ## 4.8.2 - 2026-07-22
8
+
9
+ ### Added
10
+
11
+ - **`DashboardOptions.evaluate`** — an "Evaluate system prompt" drawer action that grades a trace's last captured system prompt via an LLM judge, editable per-run instructions included; the dashboard's first and only write-capable route (`POST .../spans/:spanId/evaluate`), off unless configured and gated by the same `authToken` / `allowedHosts` checks as every other route
12
+ - `evaluateSystemPrompt` / `extractLastSystemPrompt` / `findSpanById` — the building blocks behind the drawer action, exported for scripting a grade outside the UI
13
+
14
+ ### Fixed
15
+
16
+ - The dashboard's client-side poll (`GET {basePath}api/aggregate` / `api/traces`) now carries the page's `?token=` as an `Authorization: Bearer` header on every request — previously, once `authToken` was configured the initial page load succeeded (the browser's navigation request carries the query string) but every subsequent 2s poll had no auth attached and 401'd forever, leaving the dashboard stuck on an empty/error state despite loading successfully
17
+ - `PanopticConfig.cache` failures (hydrate-on-startup or write-through — bad URL, unreachable Redis, auth failure) are no longer swallowed into total silence: a new `PanopticConfig.onError` hook fires on every failure, defaulting to `log.error("ai-panoptic", "cacheStore", error)` when not supplied, so a misconfigured cache driver now surfaces in logs instead of leaving the dashboard permanently empty with zero diagnostic
18
+
7
19
  ## 4.5.0 - 2026-07-01
8
20
 
9
21
  ### Added
package/cjs/index.cjs CHANGED
@@ -3,6 +3,7 @@ let _warlock_js_ai = require("@warlock.js/ai");
3
3
  let node_fs_promises = require("node:fs/promises");
4
4
  let node_path = require("node:path");
5
5
  let node_http = require("node:http");
6
+ let _warlock_js_logger = require("@warlock.js/logger");
6
7
 
7
8
  //#region ../@warlock.js/ai-panoptic/src/exporters/utils/walk-spans.ts
8
9
  /**
@@ -1917,6 +1918,57 @@ function panoptic(options = {}) {
1917
1918
  return new PanopticSubscriber(options);
1918
1919
  }
1919
1920
 
1921
+ //#endregion
1922
+ //#region ../@warlock.js/ai-panoptic/src/evaluate/evaluate-system-prompt.ts
1923
+ /**
1924
+ * Grade `systemPrompt` with the configured judge model, reusing
1925
+ * `ai.prompts().validate()`'s own `judgePromptBody` — never a second
1926
+ * judging implementation. `instructionsOverride` (the dashboard's
1927
+ * per-run textarea) wins over `config.instructions`; with neither, the
1928
+ * judge falls back to `judgePromptBody`'s built-in prompt-quality rubric.
1929
+ *
1930
+ * The judge itself never throws (`judgePromptBody` degrades to an
1931
+ * issues-only outcome on failure) — the only thing that CAN throw here is
1932
+ * resolving `config.model` (a factory constructing an SDK client), which
1933
+ * the caller (the dashboard route) is expected to catch.
1934
+ */
1935
+ async function evaluateSystemPrompt(systemPrompt, config, instructionsOverride) {
1936
+ return (0, _warlock_js_ai.judgePromptBody)(systemPrompt, typeof config.model === "function" ? await config.model() : config.model, instructionsOverride?.trim() || config.instructions);
1937
+ }
1938
+
1939
+ //#endregion
1940
+ //#region ../@warlock.js/ai-panoptic/src/evaluate/extract-last-system-prompt.ts
1941
+ /**
1942
+ * Pull the LAST `{role: "system"}` message off a span's captured `input`.
1943
+ *
1944
+ * Only present under `captureContent` (off by default), and only shaped
1945
+ * this way for non-tool spans — either the `[system, user]` first-trip pair
1946
+ * or, under `fullHistory`, the full `CapturedMessage[]` conversation (see
1947
+ * `collector/report-to-span.ts`). "Last" (not "only") matters for
1948
+ * `fullHistory`: a long-running agent can carry more than one system-role
1949
+ * turn, and the most recent one is the one actually in effect. Returns
1950
+ * `undefined` for a tool span, an agent with no system prompt, or when
1951
+ * content capture is off — the caller treats that as "nothing to evaluate."
1952
+ */
1953
+ function extractLastSystemPrompt(span) {
1954
+ if (!Array.isArray(span.input)) return;
1955
+ for (let index = span.input.length - 1; index >= 0; index -= 1) {
1956
+ const entry = span.input[index];
1957
+ if (entry && typeof entry === "object" && entry.role === "system") return typeof entry.content === "string" ? entry.content : void 0;
1958
+ }
1959
+ }
1960
+
1961
+ //#endregion
1962
+ //#region ../@warlock.js/ai-panoptic/src/evaluate/find-span-by-id.ts
1963
+ /** Depth-first search for the span with `spanId` inside a trace's span tree. */
1964
+ function findSpanById(root, spanId) {
1965
+ if (root.spanId === spanId) return root;
1966
+ for (const child of root.children) {
1967
+ const found = findSpanById(child, spanId);
1968
+ if (found !== void 0) return found;
1969
+ }
1970
+ }
1971
+
1920
1972
  //#endregion
1921
1973
  //#region ../@warlock.js/ai-panoptic/src/dashboard/parse-query.ts
1922
1974
  /**
@@ -2021,7 +2073,7 @@ const WARLOCK_LOGO_DATA_URI = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEA
2021
2073
  * @param basePath Normalized mount path ending in `/` (e.g. `"/"`).
2022
2074
  * @param title Header title shown in the page.
2023
2075
  */
2024
- function dashboardHtml(basePath, title) {
2076
+ function dashboardHtml(basePath, title, evaluateEnabled = false, evaluateDefaultInstructions = "") {
2025
2077
  const apiBase = `${basePath}api`;
2026
2078
  const safeTitle = escapeHtml(title);
2027
2079
  return `<!doctype html>
@@ -2202,6 +2254,18 @@ function dashboardHtml(basePath, title) {
2202
2254
  .legend { margin-left: auto; display: flex; align-items: center; gap: 6px; font-size: 11px; color: var(--dim); }
2203
2255
  .legend .grad { width: 64px; height: 8px; border-radius: 4px; background: linear-gradient(90deg, var(--surface2), var(--cost)); border: 1px solid var(--border); }
2204
2256
  .gantt { padding: 8px 10px; }
2257
+
2258
+ /* Evaluate — the drawer's one write action (config-gated). */
2259
+ .eval-btn { background: transparent; border: 1px solid var(--border); color: var(--text2); cursor: pointer; font-size: 12px; padding: 4px 10px; border-radius: 6px; margin: 10px 0 0; }
2260
+ .eval-btn:hover:not(:disabled) { color: var(--text); border-color: var(--border2); }
2261
+ .eval-btn:disabled { opacity: .6; cursor: default; }
2262
+ .eval-panel { margin-top: 8px; padding: 10px; border: 1px solid var(--border); border-radius: 8px; background: var(--surface2); }
2263
+ .eval-panel textarea { width: 100%; min-height: 64px; background: var(--panel); color: var(--text); border: 1px solid var(--border); border-radius: 6px; padding: 8px; font: 12px/1.5 ui-sans-serif, system-ui, sans-serif; resize: vertical; }
2264
+ .eval-actions { display: flex; align-items: center; gap: 8px; margin-top: 8px; }
2265
+ .eval-error { color: var(--fail); font-size: 12px; }
2266
+ .eval-result { margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--border); font-size: 12px; }
2267
+ .eval-score { font-weight: 600; }
2268
+ .eval-issues { margin: 6px 0 0; padding-left: 18px; }
2205
2269
  .grow { display: flex; align-items: center; gap: 8px; padding: 2px 0; font-size: 12px; cursor: pointer; border-radius: 4px; }
2206
2270
  .grow:hover { background: var(--surface); }
2207
2271
  .grow.selected { background: var(--sel-bg); }
@@ -2266,7 +2330,21 @@ function dashboardHtml(basePath, title) {
2266
2330
  <script>
2267
2331
  (function () {
2268
2332
  var API = ${JSON.stringify(apiBase)};
2333
+ var EVALUATE_ENABLED = ${JSON.stringify(evaluateEnabled)};
2269
2334
  var BT = String.fromCharCode(96);
2335
+ var EVALUATE_DEFAULT_INSTRUCTIONS = ${encodeForInlineScript(evaluateDefaultInstructions)};
2336
+
2337
+ // Carry the ?token= the page itself was loaded with onto every
2338
+ // subsequent poll — otherwise the API calls below inherit no auth and
2339
+ // 401 forever once authToken is configured (the initial page load is
2340
+ // the only request the URL's query string naturally reaches).
2341
+ var TOKEN = new URLSearchParams(window.location.search).get("token");
2342
+ function fetchAuthed(url, options) {
2343
+ var opts = options || {};
2344
+ var headers = opts.headers || {};
2345
+ if (TOKEN) headers = Object.assign({}, headers, { Authorization: "Bearer " + TOKEN });
2346
+ return fetch(url, Object.assign({}, opts, { headers: headers }));
2347
+ }
2270
2348
 
2271
2349
  var state = {
2272
2350
  traces: [], selectedId: null, selectedSpanId: null, collapsed: {}, sig: null,
@@ -2278,7 +2356,8 @@ function dashboardHtml(basePath, title) {
2278
2356
  showStats: false, // per-type aggregate-stats panel toggle (independent of grouping)
2279
2357
  collapsedGroups: {}, // collapsed group headers (session, prompt, or type)
2280
2358
  view: "tree", // drawer left pane: "tree" | "timeline"
2281
- hashApplied: false // guards one-time deep-link open on load
2359
+ hashApplied: false, // guards one-time deep-link open on load
2360
+ evaluate: {} // per-span evaluate UI state, keyed by spanId
2282
2361
  };
2283
2362
 
2284
2363
  var STATUS_FILTERS = ["completed", "failed", "cancelled"];
@@ -2987,6 +3066,80 @@ function dashboardHtml(basePath, title) {
2987
3066
  return '<div class="meta-sec"><div class="meta-title">metadata</div><div class="kv">' + kv + "</div></div>";
2988
3067
  }
2989
3068
 
3069
+ // --- Evaluate: grade a span's last captured system prompt -------------
3070
+ // Config-gated (EVALUATE_ENABLED) — the drawer's only write action, POSTing
3071
+ // to a route that itself only exists when the server was configured with
3072
+ // evaluate. UI state lives in state.evaluate, keyed by spanId, so it
3073
+ // survives a re-render (e.g. switching to a sibling span and back).
3074
+ function extractLastSystemPrompt(span) {
3075
+ if (!Array.isArray(span.input)) return null;
3076
+ for (var i = span.input.length - 1; i >= 0; i--) {
3077
+ var m = span.input[i];
3078
+ if (m && typeof m === "object" && m.role === "system" && typeof m.content === "string") return m.content;
3079
+ }
3080
+ return null;
3081
+ }
3082
+ function evalState(spanId) {
3083
+ return state.evaluate[spanId] || (state.evaluate[spanId] = {
3084
+ open: false, instructions: EVALUATE_DEFAULT_INSTRUCTIONS, status: "idle", result: null, error: null
3085
+ });
3086
+ }
3087
+ function evalResultHtml(result) {
3088
+ var score = typeof result.score === "number" ? Math.round(result.score * 100) + "%" : "n/a";
3089
+ var issues = (result.issues || []).map(function (i) { return "<li>" + esc(i) + "</li>"; }).join("");
3090
+ return '<div class="eval-result"><span class="eval-score">Score: ' + score + "</span>"
3091
+ + (issues ? '<ul class="eval-issues">' + issues + "</ul>" : "") + "</div>";
3092
+ }
3093
+ function evalSectionHtml(span) {
3094
+ if (!EVALUATE_ENABLED) return "";
3095
+ var sysPrompt = extractLastSystemPrompt(span);
3096
+ if (!sysPrompt) return "";
3097
+ var st = evalState(span.spanId);
3098
+ var btn = '<button type="button" class="eval-btn" data-evaluate-toggle="' + esc(span.spanId) + '">'
3099
+ + (st.open ? "Hide evaluate" : "Evaluate system prompt") + "</button>";
3100
+ if (!st.open) return btn;
3101
+ var running = st.status === "running";
3102
+ return btn + '<div class="eval-panel">'
3103
+ + '<textarea id="eval-instructions" placeholder="Grading instructions (optional — falls back to the configured default)"' + (running ? " disabled" : "") + ">" + esc(st.instructions || "") + "</textarea>"
3104
+ + '<div class="eval-actions">'
3105
+ + '<button type="button" class="eval-btn" data-evaluate-run="' + esc(span.spanId) + '"' + (running ? " disabled" : "") + ">" + (running ? "Evaluating…" : "Run") + "</button>"
3106
+ + (st.error ? '<span class="eval-error">' + esc(st.error) + "</span>" : "")
3107
+ + "</div>"
3108
+ + (st.result ? evalResultHtml(st.result) : "")
3109
+ + "</div>";
3110
+ }
3111
+ function rerenderDetail() {
3112
+ var t = findTrace(state.selectedId);
3113
+ if (!t) return;
3114
+ var sel = findSpan(t.root, state.selectedSpanId) || t.root;
3115
+ document.getElementById("drawer-detail").innerHTML = renderDetail(sel, t);
3116
+ }
3117
+ function toggleEvalPanel(spanId) {
3118
+ evalState(spanId).open = !evalState(spanId).open;
3119
+ rerenderDetail();
3120
+ }
3121
+ function runEvaluate(traceId, spanId) {
3122
+ var st = evalState(spanId);
3123
+ var textarea = document.getElementById("eval-instructions");
3124
+ if (textarea) st.instructions = textarea.value;
3125
+ st.status = "running"; st.error = null;
3126
+ rerenderDetail();
3127
+ fetchAuthed(API + "/traces/" + encodeURIComponent(traceId) + "/spans/" + encodeURIComponent(spanId) + "/evaluate", {
3128
+ method: "POST",
3129
+ headers: { "content-type": "application/json" },
3130
+ body: JSON.stringify({ instructions: st.instructions })
3131
+ }).then(function (r) {
3132
+ return r.json().then(function (data) { return { ok: r.ok, data: data }; });
3133
+ }).then(function (res) {
3134
+ if (res.ok) { st.status = "done"; st.result = res.data; st.error = null; }
3135
+ else { st.status = "error"; st.error = (res.data && (res.data.message || res.data.error)) || "evaluate failed"; st.result = null; }
3136
+ rerenderDetail();
3137
+ }).catch(function () {
3138
+ st.status = "error"; st.error = "network error"; st.result = null;
3139
+ rerenderDetail();
3140
+ });
3141
+ }
3142
+
2990
3143
  function renderDetail(span, trace) {
2991
3144
  var path = findPath(trace.root, span.spanId) || [span];
2992
3145
  var crumb = path.map(function (p, i) { return (i ? '<span class="sep"> › </span>' : "") + "<span>" + esc(p.name) + "</span>"; }).join("");
@@ -3009,6 +3162,7 @@ function dashboardHtml(basePath, title) {
3009
3162
  + '<span class="dim">· ' + dur(span.duration) + "</span></div>"
3010
3163
  + metaLine
3011
3164
  + io
3165
+ + evalSectionHtml(span, trace)
3012
3166
  + renderMeta(span, trace);
3013
3167
  }
3014
3168
 
@@ -3171,9 +3325,20 @@ function dashboardHtml(basePath, title) {
3171
3325
  if (vb) { setView(vb.getAttribute("data-view")); return; }
3172
3326
  var tog = e.target.closest ? e.target.closest("[data-toggle]") : null;
3173
3327
  if (tog) { toggleSpan(tog.getAttribute("data-toggle")); return; }
3328
+ var evalToggle = e.target.closest ? e.target.closest("[data-evaluate-toggle]") : null;
3329
+ if (evalToggle) { toggleEvalPanel(evalToggle.getAttribute("data-evaluate-toggle")); return; }
3330
+ var evalRun = e.target.closest ? e.target.closest("[data-evaluate-run]") : null;
3331
+ if (evalRun && state.selectedId) { runEvaluate(state.selectedId, evalRun.getAttribute("data-evaluate-run")); return; }
3174
3332
  var node = e.target.closest ? e.target.closest(".tnode[data-span], .grow[data-span]") : null;
3175
3333
  if (node) selectSpan(node.getAttribute("data-span"));
3176
3334
  });
3335
+ // Track the instructions textarea live (no re-render on keystroke, so
3336
+ // typing never loses focus/cursor position).
3337
+ document.getElementById("drawer-body").addEventListener("input", function (e) {
3338
+ if (e.target && e.target.id === "eval-instructions" && state.selectedSpanId) {
3339
+ evalState(state.selectedSpanId).instructions = e.target.value;
3340
+ }
3341
+ });
3177
3342
  document.getElementById("backdrop").addEventListener("click", closeDrawer);
3178
3343
  document.addEventListener("keydown", function (e) { if (e.key === "Escape" || e.keyCode === 27) closeDrawer(); });
3179
3344
 
@@ -3269,8 +3434,8 @@ function dashboardHtml(basePath, title) {
3269
3434
 
3270
3435
  function poll() {
3271
3436
  Promise.all([
3272
- fetch(API + "/aggregate").then(function (r) { return r.json(); }),
3273
- fetch(API + "/traces").then(function (r) { return r.json(); })
3437
+ fetchAuthed(API + "/aggregate").then(function (r) { return r.json(); }),
3438
+ fetchAuthed(API + "/traces").then(function (r) { return r.json(); })
3274
3439
  ]).then(function (res) {
3275
3440
  renderStats(res[0]);
3276
3441
  state.traces = res[1] || [];
@@ -3305,9 +3470,27 @@ function dashboardHtml(basePath, title) {
3305
3470
  function escapeHtml(value) {
3306
3471
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
3307
3472
  }
3473
+ /**
3474
+ * Encode free-form text (e.g. `evaluate.instructions`, which an operator
3475
+ * could type anything into) as a JS EXPRESSION that reconstructs it at
3476
+ * runtime, WITHOUT ever emitting a literal backtick into the served page —
3477
+ * the client script is itself built from a TS template literal, so a raw
3478
+ * backtick in the output would be a real syntax hazard (see the `BT =
3479
+ * String.fromCharCode(96)` construction already in the client script for
3480
+ * the same reason). `JSON.stringify` alone doesn't escape backticks (they
3481
+ * aren't JSON-significant), so a value containing one is split around it
3482
+ * and rejoined with the client's own `BT` constant. No backticks in the
3483
+ * input ⇒ a single plain `JSON.stringify(value)` — no unnecessary
3484
+ * concatenation in the common case.
3485
+ */
3486
+ function encodeForInlineScript(value) {
3487
+ return value.split("`").map((part) => JSON.stringify(part)).join(" + BT + ");
3488
+ }
3308
3489
 
3309
3490
  //#endregion
3310
3491
  //#region ../@warlock.js/ai-panoptic/src/dashboard/serve.ts
3492
+ /** Hard cap on the evaluate route's request body — well beyond a real instructions string. */
3493
+ const MAX_EVALUATE_BODY_BYTES = 64 * 1024;
3311
3494
  /**
3312
3495
  * Security response headers added to every dashboard response (S4):
3313
3496
  * block MIME-sniffing, framing, referrer leakage, and lock the page's
@@ -3337,15 +3520,22 @@ function isAuthorized(req, url, token) {
3337
3520
  * trace store. Kept separate from the server lifecycle so it can be unit
3338
3521
  * tested by feeding it a fake `req`/`res` without binding a port.
3339
3522
  *
3340
- * Routes (all under `config.basePath`, all read-only):
3523
+ * Routes (all under `config.basePath`):
3341
3524
  *
3342
3525
  * - `GET api/traces` → `store.query(parseQuery(searchParams))`
3343
3526
  * - `GET api/traces/:id` → `store.get(id)` or `404`
3344
3527
  * - `GET api/aggregate` → `store.aggregate(parseQuery(searchParams))`
3345
3528
  * - `GET {basePath}` → the self-contained HTML page
3529
+ * - `POST api/traces/:traceId/spans/:spanId/evaluate` → ONLY when
3530
+ * `config.evaluate` is set; grades the span's last captured system
3531
+ * prompt and returns a {@link EvaluateVerdict}. The dashboard's one
3532
+ * write route — absent config, POST 405s like it would against any
3533
+ * other route (the path is never even pattern-matched).
3346
3534
  *
3347
- * Anything else → `404`. Non-`GET` methods → `405`. The store shapes
3348
- * ({@link Trace}/{@link TraceSpan}) are already JSON-safe, so responses
3535
+ * Anything else → `404`. A method the matched route doesn't accept → `405`.
3536
+ * Host allowlist + bearer-token auth (S4) are checked for EVERY request,
3537
+ * regardless of method, before any routing. The store shapes
3538
+ * ({@link Trace}/{@link TraceSpan}) are already JSON-safe, so GET responses
3349
3539
  * are a plain `JSON.stringify` with no serializer.
3350
3540
  *
3351
3541
  * @example
@@ -3356,10 +3546,6 @@ function createRequestHandler(store, config) {
3356
3546
  const base = config.basePath;
3357
3547
  const apiPrefix = `${base}api`;
3358
3548
  return function handle(req, res) {
3359
- if (req.method !== "GET") {
3360
- sendJson(res, 405, { error: "method_not_allowed" });
3361
- return;
3362
- }
3363
3549
  const url = new URL(req.url ?? "/", "http://localhost");
3364
3550
  const pathname = url.pathname;
3365
3551
  const host = hostHeaderName(req.headers.host);
@@ -3371,6 +3557,17 @@ function createRequestHandler(store, config) {
3371
3557
  sendJson(res, 401, { error: "unauthorized" });
3372
3558
  return;
3373
3559
  }
3560
+ if (req.method === "POST" && config.evaluate) {
3561
+ const match = matchEvaluatePath(pathname, apiPrefix);
3562
+ if (match) {
3563
+ handleEvaluate(store, config.evaluate, match.traceId, match.spanId, req, res);
3564
+ return;
3565
+ }
3566
+ }
3567
+ if (req.method !== "GET") {
3568
+ sendJson(res, 405, { error: "method_not_allowed" });
3569
+ return;
3570
+ }
3374
3571
  if (pathname === `${apiPrefix}/traces`) {
3375
3572
  sendJson(res, 200, store.query(parseQuery(url.searchParams)));
3376
3573
  return;
@@ -3397,7 +3594,7 @@ function createRequestHandler(store, config) {
3397
3594
  "content-type": "text/html; charset=utf-8",
3398
3595
  ...SECURITY_HEADERS
3399
3596
  });
3400
- res.end(dashboardHtml(base, config.title));
3597
+ res.end(dashboardHtml(base, config.title, Boolean(config.evaluate), config.evaluate?.instructions ?? ""));
3401
3598
  return;
3402
3599
  }
3403
3600
  sendJson(res, 404, { error: "not_found" });
@@ -3411,6 +3608,102 @@ function sendJson(res, status, body) {
3411
3608
  });
3412
3609
  res.end(JSON.stringify(body));
3413
3610
  }
3611
+ /**
3612
+ * Match `{apiPrefix}/traces/:traceId/spans/:spanId/evaluate` — the one
3613
+ * write route. Returns `undefined` for anything else, including the plain
3614
+ * `{apiPrefix}/traces/:id` read route (no `/spans/.../evaluate` suffix),
3615
+ * so the two never collide.
3616
+ */
3617
+ function matchEvaluatePath(pathname, apiPrefix) {
3618
+ const prefix = `${apiPrefix}/traces/`;
3619
+ if (!pathname.startsWith(prefix)) return;
3620
+ const match = /^([^/]+)\/spans\/([^/]+)\/evaluate$/.exec(pathname.slice(prefix.length));
3621
+ if (!match) return;
3622
+ return {
3623
+ traceId: decodeURIComponent(match[1]),
3624
+ spanId: decodeURIComponent(match[2])
3625
+ };
3626
+ }
3627
+ /**
3628
+ * Collect and JSON-parse a request body, capped at
3629
+ * {@link MAX_EVALUATE_BODY_BYTES} so an oversized body can't hold the
3630
+ * connection open indefinitely. An empty body resolves to `undefined` —
3631
+ * the evaluate route treats that as "no per-run override".
3632
+ */
3633
+ function readJsonBody(req) {
3634
+ return new Promise((resolve, reject) => {
3635
+ const chunks = [];
3636
+ let size = 0;
3637
+ req.on("data", (chunk) => {
3638
+ size += chunk.length;
3639
+ if (size > MAX_EVALUATE_BODY_BYTES) {
3640
+ req.destroy();
3641
+ reject(/* @__PURE__ */ new Error("payload_too_large"));
3642
+ return;
3643
+ }
3644
+ chunks.push(chunk);
3645
+ });
3646
+ req.on("end", () => {
3647
+ if (chunks.length === 0) {
3648
+ resolve(void 0);
3649
+ return;
3650
+ }
3651
+ try {
3652
+ resolve(JSON.parse(Buffer.concat(chunks).toString("utf-8")));
3653
+ } catch {
3654
+ reject(/* @__PURE__ */ new Error("invalid_json"));
3655
+ }
3656
+ });
3657
+ req.on("error", reject);
3658
+ });
3659
+ }
3660
+ /**
3661
+ * Handle `POST {apiPrefix}/traces/:traceId/spans/:spanId/evaluate`. Looks
3662
+ * up the trace + span, extracts its last captured system prompt, and
3663
+ * grades it via {@link evaluateSystemPrompt}. `judgePromptBody` itself
3664
+ * never throws (a broken judge degrades to an issues-only outcome) — the
3665
+ * try/catch here only guards `config.evaluate.model` resolution, the one
3666
+ * step that CAN throw (e.g. a factory constructing an SDK client).
3667
+ */
3668
+ async function handleEvaluate(store, evaluate, traceId, spanId, req, res) {
3669
+ const trace = store.get(traceId);
3670
+ if (trace === void 0) {
3671
+ sendJson(res, 404, {
3672
+ error: "trace_not_found",
3673
+ traceId
3674
+ });
3675
+ return;
3676
+ }
3677
+ const span = findSpanById(trace.root, spanId);
3678
+ if (span === void 0) {
3679
+ sendJson(res, 404, {
3680
+ error: "span_not_found",
3681
+ spanId
3682
+ });
3683
+ return;
3684
+ }
3685
+ const systemPrompt = extractLastSystemPrompt(span);
3686
+ if (systemPrompt === void 0) {
3687
+ sendJson(res, 422, { error: "no_system_prompt" });
3688
+ return;
3689
+ }
3690
+ let body;
3691
+ try {
3692
+ body = await readJsonBody(req);
3693
+ } catch (error) {
3694
+ const message = error instanceof Error ? error.message : "invalid_json";
3695
+ sendJson(res, message === "payload_too_large" ? 413 : 400, { error: message });
3696
+ return;
3697
+ }
3698
+ try {
3699
+ sendJson(res, 200, await evaluateSystemPrompt(systemPrompt, evaluate, body?.instructions));
3700
+ } catch (error) {
3701
+ sendJson(res, 502, {
3702
+ error: "evaluate_failed",
3703
+ message: error instanceof Error ? error.message : String(error)
3704
+ });
3705
+ }
3706
+ }
3414
3707
 
3415
3708
  //#endregion
3416
3709
  //#region ../@warlock.js/ai-panoptic/src/dashboard/dashboard.ts
@@ -3452,7 +3745,8 @@ function dashboard(store, options = {}) {
3452
3745
  basePath,
3453
3746
  title,
3454
3747
  authToken: options.authToken,
3455
- allowedHosts
3748
+ allowedHosts,
3749
+ evaluate: options.evaluate
3456
3750
  }));
3457
3751
  return new Promise((resolve, reject) => {
3458
3752
  const onError = (error) => {
@@ -3839,7 +4133,8 @@ function applyPanopticConfig(config) {
3839
4133
  let cacheStore;
3840
4134
  if (store === void 0 && config.dashboard) {
3841
4135
  if (config.cache !== void 0) {
3842
- cacheStore = createCacheTraceStore(config.cache);
4136
+ const onError = config.onError ?? ((error) => _warlock_js_logger.log.error("ai-panoptic", "cacheStore", error));
4137
+ cacheStore = createCacheTraceStore(config.cache, { onError });
3843
4138
  store = cacheStore;
3844
4139
  } else store = createInMemoryTraceStore();
3845
4140
  exporters.push(store);
@@ -3910,9 +4205,12 @@ exports.createInMemoryTraceStore = createInMemoryTraceStore;
3910
4205
  exports.createPanopticMiddleware = createPanopticMiddleware;
3911
4206
  exports.dashboard = dashboard;
3912
4207
  exports.emptyUsage = emptyUsage;
4208
+ exports.evaluateSystemPrompt = evaluateSystemPrompt;
4209
+ exports.extractLastSystemPrompt = extractLastSystemPrompt;
3913
4210
  exports.extractSpanAttributes = extractSpanAttributes;
3914
4211
  exports.fileExporter = fileExporter;
3915
4212
  exports.filterTraces = filterTraces;
4213
+ exports.findSpanById = findSpanById;
3916
4214
  exports.formatSpanIO = formatSpanIO;
3917
4215
  exports.formatSpanLine = formatSpanLine;
3918
4216
  exports.groupByPrompt = groupByPrompt;