@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.
Files changed (57) hide show
  1. package/CHANGELOG.md +15 -3
  2. package/README.md +2 -2
  3. package/cjs/index.cjs +22 -19
  4. package/cjs/index.cjs.map +1 -1
  5. package/esm/collector/extract-span-attributes.d.mts.map +1 -1
  6. package/esm/collector/extract-span-attributes.mjs +1 -0
  7. package/esm/collector/extract-span-attributes.mjs.map +1 -1
  8. package/esm/collector/report-to-span.mjs.map +1 -1
  9. package/esm/config/apply-panoptic-config.mjs.map +1 -1
  10. package/esm/dashboard/dashboard.mjs.map +1 -1
  11. package/esm/dashboard/parse-query.mjs +1 -0
  12. package/esm/dashboard/parse-query.mjs.map +1 -1
  13. package/esm/dashboard/serve.mjs.map +1 -1
  14. package/esm/dashboard/trace-filter.d.mts.map +1 -1
  15. package/esm/dashboard/trace-filter.mjs +1 -1
  16. package/esm/dashboard/trace-filter.mjs.map +1 -1
  17. package/esm/dashboard/ui.html.mjs +5 -4
  18. package/esm/dashboard/ui.html.mjs.map +1 -1
  19. package/esm/exporters/console/console-exporter.mjs +4 -3
  20. package/esm/exporters/console/console-exporter.mjs.map +1 -1
  21. package/esm/exporters/console/format-span-io.d.mts.map +1 -1
  22. package/esm/exporters/console/format-span-io.mjs.map +1 -1
  23. package/esm/exporters/console/format-span-line.mjs +2 -0
  24. package/esm/exporters/console/format-span-line.mjs.map +1 -1
  25. package/esm/exporters/console/index.mjs +5 -0
  26. package/esm/exporters/file/file-exporter.mjs.map +1 -1
  27. package/esm/exporters/index.mjs +2 -0
  28. package/esm/exporters/langfuse/langfuse-exporter.mjs +3 -2
  29. package/esm/exporters/langfuse/langfuse-exporter.mjs.map +1 -1
  30. package/esm/exporters/otel/otel-api.shim.type.d.mts.map +1 -1
  31. package/esm/exporters/otel/otel-exporter.mjs +2 -1
  32. package/esm/exporters/otel/otel-exporter.mjs.map +1 -1
  33. package/esm/exporters/utils/index.mjs +5 -0
  34. package/esm/exporters/utils/total-cost.d.mts +2 -4
  35. package/esm/exporters/utils/total-cost.d.mts.map +1 -1
  36. package/esm/exporters/utils/total-cost.mjs +5 -7
  37. package/esm/exporters/utils/total-cost.mjs.map +1 -1
  38. package/esm/panoptic/panoptic-middleware.mjs.map +1 -1
  39. package/esm/panoptic/panoptic.d.mts.map +1 -1
  40. package/esm/panoptic/panoptic.mjs.map +1 -1
  41. package/esm/store/cache-trace-store.d.mts.map +1 -1
  42. package/esm/store/cache-trace-store.mjs +1 -0
  43. package/esm/store/cache-trace-store.mjs.map +1 -1
  44. package/esm/store/in-memory-trace-store.d.mts.map +1 -1
  45. package/esm/store/in-memory-trace-store.mjs +3 -1
  46. package/esm/store/in-memory-trace-store.mjs.map +1 -1
  47. package/esm/store/sum-usage.mjs.map +1 -1
  48. package/esm/store/trace-aggregate.type.d.mts +2 -6
  49. package/esm/store/trace-aggregate.type.d.mts.map +1 -1
  50. package/llms-full.txt +134 -117
  51. package/package.json +4 -4
  52. package/skills/evaluate-system-prompt/SKILL.md +21 -17
  53. package/skills/export-traces/SKILL.md +32 -24
  54. package/skills/index.md +3 -0
  55. package/skills/observe-with-panoptic/SKILL.md +18 -18
  56. package/skills/query-traces/SKILL.md +31 -33
  57. package/skills/use-local-dashboard/SKILL.md +33 -26
package/CHANGELOG.md CHANGED
@@ -4,6 +4,17 @@ 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
+ ## 5.31.0 - 2026-10-18
8
+
9
+ ### Fixed
10
+
11
+ - Trace cost totals (exporters, the dashboard and trace filters) include the new cache-write cost channel.
12
+
13
+ ### Added
14
+
15
+ - Tool spans carry the provider's tool-call id as the `tool.callId` attribute.
16
+ - Agent runs with `status: "incomplete"` (their final answer was cut off) are exported as errors: OTel `ERROR` status, Langfuse level `ERROR`, the console exporter's error stream (marked `INCOMPLETE`), the dashboard's error filter, and the `failed` count of trace-store aggregates. `?status=incomplete` is accepted by the dashboard API.
17
+
7
18
  ## 5.30.0 - 2026-10-16
8
19
 
9
20
  ### Changed
@@ -81,6 +92,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
81
92
  ### Changed
82
93
 
83
94
  - Lockstep release maintenance and dependency refresh.
95
+
84
96
  ## 5.20.0 - 2026-09-24
85
97
 
86
98
  ### Changed
@@ -146,9 +158,9 @@ changelogs.
146
158
 
147
159
  - **Dashboard stored XSS via `javascript:` markdown links — fixed.** The dashboard's markdown link renderer (`mdInline` in `ui.html.ts`) rewrote `[text](url)` into a live `<a href>` without validating the URL scheme, so a `javascript:` URL embedded in captured trace content (`span.input` / `span.output` under `captureContent` — i.e. prompt-injected model output or attacker-controlled tool results) became a stored XSS that fired when an operator clicked the link; the page's CSP (`script-src 'unsafe-inline'`) permits `javascript:` URI execution and does not restrict top-level navigation, putting the in-page bearer token in reach. Link URLs are now checked against a scheme allowlist (`http:`, `https:`, `mailto:`, plus relative/anchor URLs) after normalizing the URL the way a browser will — attribute-entity decode, strip of ignored control chars/whitespace (`java\tscript:`), lowercase — and scheme-relative `//host` links are rejected too; a rejected URL renders its label as plain text with no `href` at all. Regression-tested by executing the actual inlined client renderer against `javascript:`/`data:`/`vbscript:` payloads and their case/whitespace/entity obfuscations (`ui.html.md-links.spec.ts`)
148
160
  - **Dashboard token-handling hardening (3 residual findings from the same audit as the XSS above).** None of these are exploitable on their own today, but each widened the blast radius of a future bug:
149
- - *Bearer token no longer a page-global.* `ui.html.ts`'s client script kept `TOKEN` as a `var` shared across its whole ~1000-line closure. It's now sealed inside its own inner IIFE that exposes only the `fetchAuthed` helper — code elsewhere in that closure (present or future) can no longer read the raw token by name.
150
- - *Constant-time token comparison.* `serve.ts`'s `isAuthorized` compared the header/query token with plain `===`, which short-circuits on the first differing byte and is a textbook timing side-channel — the one scenario the package's own docs call out as realistic (`authToken` is required specifically when bound off-loopback, i.e. reachable over a network). Both the header and query-string checks now go through `crypto.timingSafeEqual` on length-checked, equal-length buffers.
151
- - *Token-in-URL narrowed to the one route that needs it.* `?token=` is no longer accepted on the JSON API routes, only on the HTML page route. The page's own polling already re-sends the token as an `Authorization` header (fixed in 4.8.2), so the only request that structurally *can't* carry a header is the initial browser navigation that loads the HTML shell — that's the sole remaining query-string exposure, narrowing the token's footprint in access/proxy logs and browser history from every poll to one request.
161
+ - _Bearer token no longer a page-global._ `ui.html.ts`'s client script kept `TOKEN` as a `var` shared across its whole ~1000-line closure. It's now sealed inside its own inner IIFE that exposes only the `fetchAuthed` helper — code elsewhere in that closure (present or future) can no longer read the raw token by name.
162
+ - _Constant-time token comparison._ `serve.ts`'s `isAuthorized` compared the header/query token with plain `===`, which short-circuits on the first differing byte and is a textbook timing side-channel — the one scenario the package's own docs call out as realistic (`authToken` is required specifically when bound off-loopback, i.e. reachable over a network). Both the header and query-string checks now go through `crypto.timingSafeEqual` on length-checked, equal-length buffers.
163
+ - _Token-in-URL narrowed to the one route that needs it._ `?token=` is no longer accepted on the JSON API routes, only on the HTML page route. The page's own polling already re-sends the token as an `Authorization` header (fixed in 4.8.2), so the only request that structurally _can't_ carry a header is the initial browser navigation that loads the HTML shell — that's the sole remaining query-string exposure, narrowing the token's footprint in access/proxy logs and browser history from every poll to one request.
152
164
 
153
165
  ## 4.12.0
154
166
 
package/README.md CHANGED
@@ -81,8 +81,8 @@ Four exporters ship today, each a factory returning an `ExporterContract`:
81
81
  ```ts
82
82
  import {
83
83
  consoleExporter, // zero-dep — print trace summary or full span tree
84
- fileExporter, // zero-dep — append traces as JSON Lines, buffered
85
- otelExporter, // OpenTelemetry — gen_ai.* semantic conventions (lazy @opentelemetry/api)
84
+ fileExporter, // zero-dep — append traces as JSON Lines, buffered
85
+ otelExporter, // OpenTelemetry — gen_ai.* semantic conventions (lazy @opentelemetry/api)
86
86
  langfuseExporter, // Langfuse — traces + generations (lazy langfuse)
87
87
  } from "@warlock.js/ai-panoptic";
88
88
 
package/cjs/index.cjs CHANGED
@@ -146,6 +146,7 @@ function addOrchestratorAttributes(attributes, extensions) {
146
146
  }
147
147
  function addToolAttributes(attributes, extensions) {
148
148
  if (extensions.tripIndex !== void 0) attributes["tool.tripIndex"] = extensions.tripIndex;
149
+ if (extensions.callId !== void 0) attributes["tool.callId"] = extensions.callId;
149
150
  if (extensions.recoveredFrom !== void 0) attributes["tool.recoveredFrom"] = extensions.recoveredFrom;
150
151
  }
151
152
 
@@ -781,6 +782,7 @@ var CacheTraceStore = class {
781
782
  aggregate.completed += 1;
782
783
  break;
783
784
  case "failed":
785
+ case "incomplete":
784
786
  aggregate.failed += 1;
785
787
  break;
786
788
  case "cancelled":
@@ -927,7 +929,8 @@ var InMemoryTraceStore = class {
927
929
  * Non-terminal statuses (`awaiting-input`, `max-iterations`) are
928
930
  * counted in `traces` but tracked by none of the three headline
929
931
  * counters — intentional, those three answer the common
930
- * "succeeded / errored / aborted" question.
932
+ * "succeeded / errored / aborted" question. An `incomplete` agent run
933
+ * (its answer was cut off) errored, so it counts as `failed`.
931
934
  */
932
935
  countStatus(aggregate, trace) {
933
936
  switch (trace.root.status) {
@@ -935,6 +938,7 @@ var InMemoryTraceStore = class {
935
938
  aggregate.completed += 1;
936
939
  break;
937
940
  case "failed":
941
+ case "incomplete":
938
942
  aggregate.failed += 1;
939
943
  break;
940
944
  case "cancelled":
@@ -985,10 +989,8 @@ function createInMemoryTraceStore(options) {
985
989
  //#region ../../packages/ai-panoptic/src/exporters/utils/total-cost.ts
986
990
  /**
987
991
  * Collapse a {@link Usage.cost} breakdown into a single USD scalar by
988
- * summing every populated field. Mirrors the formula documented on
989
- * `Usage.cost` (input + output + cachedInput + cachedOutput), and also
990
- * folds in `reasoning` for forward-safety when a provider prices
991
- * reasoning tokens as a separate channel. Returns `undefined` when no
992
+ * summing every populated channel (input, output, cache reads, cache
993
+ * writes, reasoning) via `totalCost` from `@warlock.js/ai`. Returns `undefined` when no
992
994
  * pricing was attached, so exporters can omit the cost attribute
993
995
  * entirely rather than reporting a misleading `0`.
994
996
  *
@@ -997,9 +999,7 @@ function createInMemoryTraceStore(options) {
997
999
  * // => 0.05
998
1000
  */
999
1001
  function totalCostUsd(usage) {
1000
- const cost = usage.cost;
1001
- if (!cost) return;
1002
- return (cost.input ?? 0) + (cost.output ?? 0) + (cost.cachedInput ?? 0) + (cost.cachedOutput ?? 0) + (cost.reasoning ?? 0);
1002
+ return (0, _warlock_js_ai.totalCost)(usage.cost);
1003
1003
  }
1004
1004
 
1005
1005
  //#endregion
@@ -1171,6 +1171,7 @@ function statusMarker(status) {
1171
1171
  case "completed": return "ok";
1172
1172
  case "failed": return "ERR";
1173
1173
  case "cancelled": return "cancel";
1174
+ case "incomplete": return "INCOMPLETE";
1174
1175
  case "max-iterations": return "max-iter";
1175
1176
  case "awaiting-input": return "await";
1176
1177
  default: return status;
@@ -1245,11 +1246,11 @@ function writeSpan(sink, span, depth, io, ioMaxChars) {
1245
1246
  if (io) for (const line of formatSpanIO(span, depth, ioMaxChars)) writeAtSeverity(sink, span.status, line);
1246
1247
  }
1247
1248
  /**
1248
- * Route a line to `error` when the span failed/cancelled, otherwise to
1249
- * `log`. Keeps healthy traces out of the error stream.
1249
+ * Route a line to `error` when the span failed/cancelled/incomplete,
1250
+ * otherwise to `log`. Keeps healthy traces out of the error stream.
1250
1251
  */
1251
1252
  function writeAtSeverity(sink, status, line) {
1252
- if (status === "failed" || status === "cancelled") {
1253
+ if (status === "failed" || status === "cancelled" || status === "incomplete") {
1253
1254
  sink.error(line);
1254
1255
  return;
1255
1256
  }
@@ -1556,10 +1557,10 @@ function langfuseMetadata(span) {
1556
1557
  }
1557
1558
  /**
1558
1559
  * Map the Panoptic span status onto a Langfuse observation level —
1559
- * failed/cancelled spans surface as `ERROR`, everything else `DEFAULT`.
1560
+ * failed/cancelled/incomplete spans surface as `ERROR`, everything else `DEFAULT`.
1560
1561
  */
1561
1562
  function toLevel(span) {
1562
- if (span.status === "failed" || span.status === "cancelled") return "ERROR";
1563
+ if (span.status === "failed" || span.status === "cancelled" || span.status === "incomplete") return "ERROR";
1563
1564
  return "DEFAULT";
1564
1565
  }
1565
1566
 
@@ -1692,7 +1693,7 @@ function applyStatus(otelSpan, span) {
1692
1693
  message: span.error.message,
1693
1694
  stack: span.error.stack
1694
1695
  });
1695
- if (span.status === "failed" || span.status === "cancelled") {
1696
+ if (span.status === "failed" || span.status === "cancelled" || span.status === "incomplete") {
1696
1697
  otelSpan.setStatus({
1697
1698
  code: codes.ERROR,
1698
1699
  message: span.error?.message
@@ -1980,6 +1981,7 @@ const KNOWN_STATUSES = [
1980
1981
  "completed",
1981
1982
  "failed",
1982
1983
  "cancelled",
1984
+ "incomplete",
1983
1985
  "max-iterations",
1984
1986
  "awaiting-input",
1985
1987
  "awaiting-approval"
@@ -2370,13 +2372,14 @@ function dashboardHtml(basePath, title, evaluateEnabled = false, evaluateDefault
2370
2372
 
2371
2373
  var STATUS_FILTERS = ["completed", "failed", "cancelled"];
2372
2374
  var TYPE_FILTERS = ["agent", "tool", "model", "supervisor", "team", "workflow", "orchestrator", "planner", "batch", "callback"];
2373
- var ERROR_STATUSES = { failed: 1, cancelled: 1 };
2375
+ var ERROR_STATUSES = { failed: 1, cancelled: 1, incomplete: 1 };
2374
2376
  var NO_SESSION_KEY = "(no session)";
2375
2377
  var NO_PROMPT_KEY = "(no prompt)";
2376
2378
  var NO_TYPE_KEY = "(no type)";
2377
2379
 
2378
2380
  var statusClass = function (s) {
2379
2381
  if (s === "completed" || s === "failed" || s === "cancelled") return s;
2382
+ if (s === "incomplete") return "failed";
2380
2383
  return "other";
2381
2384
  };
2382
2385
  var esc = function (v) {
@@ -2464,7 +2467,7 @@ function dashboardHtml(basePath, title, evaluateEnabled = false, evaluateDefault
2464
2467
  // Sum every priced lane of a cost object into one USD number.
2465
2468
  function costSumObj(c) {
2466
2469
  if (!c) return 0;
2467
- return (c.input || 0) + (c.output || 0) + (c.cachedInput || 0) + (c.cachedOutput || 0) + (c.reasoning || 0);
2470
+ return (c.input || 0) + (c.output || 0) + (c.cachedInput || 0) + (c.cacheWrite || 0) + (c.cachedOutput || 0) + (c.reasoning || 0);
2468
2471
  }
2469
2472
  // The cost a single span directly carries (on its rolled-up usage).
2470
2473
  function usageCost(usage) { return usage ? costSumObj(usage.cost) : 0; }
@@ -2798,7 +2801,7 @@ function dashboardHtml(basePath, title, evaluateEnabled = false, evaluateDefault
2798
2801
  }
2799
2802
 
2800
2803
  function renderStats(a) {
2801
- var cost = a.cost ? (((a.cost.input || 0) + (a.cost.output || 0) + (a.cost.cachedInput || 0) + (a.cost.cachedOutput || 0)).toFixed(4)) : "—";
2804
+ var cost = a.cost ? (((a.cost.input || 0) + (a.cost.output || 0) + (a.cost.cachedInput || 0) + (a.cost.cacheWrite || 0) + (a.cost.cachedOutput || 0) + (a.cost.reasoning || 0)).toFixed(4)) : "—";
2802
2805
  var u = a.usage || {};
2803
2806
  var cells = [
2804
2807
  ["traces", num(a.traces), null],
@@ -3055,7 +3058,7 @@ function dashboardHtml(basePath, title, evaluateEnabled = false, evaluateDefault
3055
3058
  // "Supervisor Terminated By", "span id" → "Span ID", "agent.trips" →
3056
3059
  // "Agent Trips". Underlying attribute keys are untouched.
3057
3060
  // NB: this whole script is a template literal — regex backslash classes
3058
- // MUST be double-escaped (\\s, not \s) or "\\s" collapses to a literal "s".
3061
+ // MUST be double-escaped (\\s) so the rendered script retains its backslash.
3059
3062
  function humanizeKey(key) {
3060
3063
  return String(key).split(/[.\\s]+/).map(function (seg) {
3061
3064
  return seg.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/\\s+/).map(function (w) {
@@ -4124,7 +4127,7 @@ function aggregateByType(traces) {
4124
4127
  */
4125
4128
  function costSumObj(cost) {
4126
4129
  if (cost === void 0) return 0;
4127
- return (cost.input ?? 0) + (cost.output ?? 0) + (cost.cachedInput ?? 0) + (cost.cachedOutput ?? 0) + (cost.reasoning ?? 0);
4130
+ return (cost.input ?? 0) + (cost.output ?? 0) + (cost.cachedInput ?? 0) + (cost.cacheWrite ?? 0) + (cost.cachedOutput ?? 0) + (cost.reasoning ?? 0);
4128
4131
  }
4129
4132
  /**
4130
4133
  * Rollup-aware subtree cost: a node's own cost when it carries one