@warlock.js/ai-panoptic 4.3.0 → 4.5.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 +46 -30
- package/cjs/index.cjs +2636 -67
- package/cjs/index.cjs.map +1 -1
- package/esm/collector/collector.d.mts +20 -2
- package/esm/collector/collector.d.mts.map +1 -1
- package/esm/collector/collector.mjs +47 -7
- package/esm/collector/collector.mjs.map +1 -1
- package/esm/collector/content-capture.type.d.mts +52 -0
- package/esm/collector/content-capture.type.d.mts.map +1 -0
- package/esm/collector/extract-span-attributes.d.mts.map +1 -1
- package/esm/collector/extract-span-attributes.mjs +3 -0
- package/esm/collector/extract-span-attributes.mjs.map +1 -1
- package/esm/collector/index.d.mts +2 -1
- package/esm/collector/index.mjs +7 -0
- package/esm/collector/normalize-error.d.mts.map +1 -1
- package/esm/collector/normalize-error.mjs +6 -3
- package/esm/collector/normalize-error.mjs.map +1 -1
- package/esm/collector/report-to-span.d.mts +8 -1
- package/esm/collector/report-to-span.d.mts.map +1 -1
- package/esm/collector/report-to-span.mjs +87 -2
- package/esm/collector/report-to-span.mjs.map +1 -1
- package/esm/collector/report-to-trace.d.mts +10 -6
- package/esm/collector/report-to-trace.d.mts.map +1 -1
- package/esm/collector/report-to-trace.mjs +10 -7
- package/esm/collector/report-to-trace.mjs.map +1 -1
- package/esm/config/apply-panoptic-config.d.mts +27 -0
- package/esm/config/apply-panoptic-config.d.mts.map +1 -0
- package/esm/config/apply-panoptic-config.mjs +89 -0
- package/esm/config/apply-panoptic-config.mjs.map +1 -0
- package/esm/config/index.d.mts +2 -0
- package/esm/config/index.mjs +3 -0
- package/esm/config/panoptic-config.type.d.mts +84 -0
- package/esm/config/panoptic-config.type.d.mts.map +1 -0
- package/esm/contracts/trace.type.d.mts +31 -5
- package/esm/contracts/trace.type.d.mts.map +1 -1
- package/esm/dashboard/dashboard.d.mts +32 -0
- package/esm/dashboard/dashboard.d.mts.map +1 -0
- package/esm/dashboard/dashboard.mjs +132 -0
- package/esm/dashboard/dashboard.mjs.map +1 -0
- package/esm/dashboard/dashboard.type.d.mts +63 -0
- package/esm/dashboard/dashboard.type.d.mts.map +1 -0
- package/esm/dashboard/index.d.mts +3 -0
- package/esm/dashboard/parse-query.d.mts +1 -0
- package/esm/dashboard/parse-query.mjs +54 -0
- package/esm/dashboard/parse-query.mjs.map +1 -0
- package/esm/dashboard/serve.d.mts +1 -0
- package/esm/dashboard/serve.mjs +111 -0
- package/esm/dashboard/serve.mjs.map +1 -0
- package/esm/dashboard/trace-filter.d.mts +187 -0
- package/esm/dashboard/trace-filter.d.mts.map +1 -0
- package/esm/dashboard/trace-filter.mjs +270 -0
- package/esm/dashboard/trace-filter.mjs.map +1 -0
- package/esm/dashboard/ui.html.mjs +1330 -0
- package/esm/dashboard/ui.html.mjs.map +1 -0
- package/esm/dashboard/warlock-logo.mjs +13 -0
- package/esm/dashboard/warlock-logo.mjs.map +1 -0
- package/esm/exporters/console/console-exporter.d.mts +11 -3
- package/esm/exporters/console/console-exporter.d.mts.map +1 -1
- package/esm/exporters/console/console-exporter.mjs +31 -13
- package/esm/exporters/console/console-exporter.mjs.map +1 -1
- package/esm/exporters/console/console-exporter.type.d.mts +15 -0
- package/esm/exporters/console/console-exporter.type.d.mts.map +1 -1
- package/esm/exporters/console/format-span-io.d.mts +27 -0
- package/esm/exporters/console/format-span-io.d.mts.map +1 -0
- package/esm/exporters/console/format-span-io.mjs +52 -0
- package/esm/exporters/console/format-span-io.mjs.map +1 -0
- package/esm/exporters/console/index.d.mts +2 -1
- package/esm/exporters/file/file-exporter.mjs +5 -2
- package/esm/exporters/file/file-exporter.mjs.map +1 -1
- package/esm/exporters/index.d.mts +1 -0
- package/esm/exporters/index.mjs +1 -0
- package/esm/exporters/langfuse/langfuse-exporter.d.mts +10 -6
- package/esm/exporters/langfuse/langfuse-exporter.d.mts.map +1 -1
- package/esm/exporters/langfuse/langfuse-exporter.mjs +80 -30
- package/esm/exporters/langfuse/langfuse-exporter.mjs.map +1 -1
- package/esm/exporters/langfuse/langfuse-exporter.type.d.mts +6 -2
- package/esm/exporters/langfuse/langfuse-exporter.type.d.mts.map +1 -1
- package/esm/exporters/otel/otel-api.shim.type.d.mts +60 -0
- package/esm/exporters/otel/otel-api.shim.type.d.mts.map +1 -0
- package/esm/exporters/otel/otel-exporter.d.mts.map +1 -1
- package/esm/exporters/otel/otel-exporter.mjs +15 -0
- package/esm/exporters/otel/otel-exporter.mjs.map +1 -1
- package/esm/exporters/otel/otel-exporter.type.d.mts +7 -3
- package/esm/exporters/otel/otel-exporter.type.d.mts.map +1 -1
- package/esm/exporters/utils/gen-ai-attributes.d.mts +3 -1
- package/esm/exporters/utils/gen-ai-attributes.d.mts.map +1 -1
- package/esm/exporters/utils/gen-ai-attributes.mjs +5 -1
- package/esm/exporters/utils/gen-ai-attributes.mjs.map +1 -1
- package/esm/index.d.mts +11 -2
- package/esm/index.mjs +10 -1
- package/esm/panoptic/index.mjs +4 -0
- package/esm/panoptic/panoptic.d.mts.map +1 -1
- package/esm/panoptic/panoptic.mjs +9 -1
- package/esm/panoptic/panoptic.mjs.map +1 -1
- package/esm/panoptic/panoptic.type.d.mts +32 -0
- package/esm/panoptic/panoptic.type.d.mts.map +1 -1
- package/esm/register.mjs +12 -0
- package/esm/register.mjs.map +1 -0
- package/esm/store/cache-trace-store.d.mts +81 -0
- package/esm/store/cache-trace-store.d.mts.map +1 -0
- package/esm/store/cache-trace-store.mjs +290 -0
- package/esm/store/cache-trace-store.mjs.map +1 -0
- package/esm/store/in-memory-trace-store.mjs +9 -2
- package/esm/store/in-memory-trace-store.mjs.map +1 -1
- package/esm/store/index.d.mts +1 -0
- package/esm/store/index.mjs +1 -0
- package/llms-full.txt +351 -4
- package/llms.txt +4 -3
- package/package.json +6 -2
- package/skills/README.md +7 -3
- package/skills/export-traces/SKILL.md +33 -1
- package/skills/observe-with-panoptic/SKILL.md +49 -2
- package/skills/query-traces/SKILL.md +52 -1
- package/skills/use-local-dashboard/SKILL.md +213 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { parseQuery } from "./parse-query.mjs";
|
|
2
|
+
import { dashboardHtml } from "./ui.html.mjs";
|
|
3
|
+
|
|
4
|
+
//#region ../@warlock.js/ai-panoptic/src/dashboard/serve.ts
|
|
5
|
+
/**
|
|
6
|
+
* Security response headers added to every dashboard response (S4):
|
|
7
|
+
* block MIME-sniffing, framing, referrer leakage, and lock the page's
|
|
8
|
+
* content sources down to itself (the UI is fully self-contained — no CDN).
|
|
9
|
+
*/
|
|
10
|
+
const SECURITY_HEADERS = {
|
|
11
|
+
"x-content-type-options": "nosniff",
|
|
12
|
+
"x-frame-options": "DENY",
|
|
13
|
+
"referrer-policy": "no-referrer",
|
|
14
|
+
"content-security-policy": "default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; connect-src 'self'; img-src 'self' data:; base-uri 'none'; form-action 'none'; frame-ancestors 'none'"
|
|
15
|
+
};
|
|
16
|
+
/** Extract the host (no port) from a `Host` header value. */
|
|
17
|
+
function hostHeaderName(hostHeader) {
|
|
18
|
+
if (!hostHeader) return void 0;
|
|
19
|
+
if (hostHeader.startsWith("[")) return hostHeader.slice(0, hostHeader.indexOf("]") + 1);
|
|
20
|
+
const colon = hostHeader.indexOf(":");
|
|
21
|
+
return colon === -1 ? hostHeader : hostHeader.slice(0, colon);
|
|
22
|
+
}
|
|
23
|
+
/** Constant-ish bearer-token check from `Authorization` header or `?token=`. */
|
|
24
|
+
function isAuthorized(req, url, token) {
|
|
25
|
+
const header = req.headers.authorization;
|
|
26
|
+
if (header && header === `Bearer ${token}`) return true;
|
|
27
|
+
return url.searchParams.get("token") === token;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Build the `node:http` request handler for the dashboard over a given
|
|
31
|
+
* trace store. Kept separate from the server lifecycle so it can be unit
|
|
32
|
+
* tested by feeding it a fake `req`/`res` without binding a port.
|
|
33
|
+
*
|
|
34
|
+
* Routes (all under `config.basePath`, all read-only):
|
|
35
|
+
*
|
|
36
|
+
* - `GET api/traces` → `store.query(parseQuery(searchParams))`
|
|
37
|
+
* - `GET api/traces/:id` → `store.get(id)` or `404`
|
|
38
|
+
* - `GET api/aggregate` → `store.aggregate(parseQuery(searchParams))`
|
|
39
|
+
* - `GET {basePath}` → the self-contained HTML page
|
|
40
|
+
*
|
|
41
|
+
* Anything else → `404`. Non-`GET` methods → `405`. The store shapes
|
|
42
|
+
* ({@link Trace}/{@link TraceSpan}) are already JSON-safe, so responses
|
|
43
|
+
* are a plain `JSON.stringify` with no serializer.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* const handler = createRequestHandler(store, { basePath: "/", title: "Panoptic" });
|
|
47
|
+
* http.createServer(handler).listen(4319, "127.0.0.1");
|
|
48
|
+
*/
|
|
49
|
+
function createRequestHandler(store, config) {
|
|
50
|
+
const base = config.basePath;
|
|
51
|
+
const apiPrefix = `${base}api`;
|
|
52
|
+
return function handle(req, res) {
|
|
53
|
+
if (req.method !== "GET") {
|
|
54
|
+
sendJson(res, 405, { error: "method_not_allowed" });
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
58
|
+
const pathname = url.pathname;
|
|
59
|
+
const host = hostHeaderName(req.headers.host);
|
|
60
|
+
if (!host || !config.allowedHosts.includes(host.toLowerCase())) {
|
|
61
|
+
sendJson(res, 403, { error: "host_not_allowed" });
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (config.authToken && !isAuthorized(req, url, config.authToken)) {
|
|
65
|
+
sendJson(res, 401, { error: "unauthorized" });
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (pathname === `${apiPrefix}/traces`) {
|
|
69
|
+
sendJson(res, 200, store.query(parseQuery(url.searchParams)));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (pathname.startsWith(`${apiPrefix}/traces/`)) {
|
|
73
|
+
const traceId = decodeURIComponent(pathname.slice(`${apiPrefix}/traces/`.length));
|
|
74
|
+
const trace = traceId.length > 0 ? store.get(traceId) : void 0;
|
|
75
|
+
if (trace === void 0) {
|
|
76
|
+
sendJson(res, 404, {
|
|
77
|
+
error: "trace_not_found",
|
|
78
|
+
traceId
|
|
79
|
+
});
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
sendJson(res, 200, trace);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (pathname === `${apiPrefix}/aggregate`) {
|
|
86
|
+
sendJson(res, 200, store.aggregate(parseQuery(url.searchParams)));
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (pathname === base || pathname === base.replace(/\/$/, "")) {
|
|
90
|
+
res.writeHead(200, {
|
|
91
|
+
"content-type": "text/html; charset=utf-8",
|
|
92
|
+
...SECURITY_HEADERS
|
|
93
|
+
});
|
|
94
|
+
res.end(dashboardHtml(base, config.title));
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
sendJson(res, 404, { error: "not_found" });
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/** Write a JSON response with the given status code and security headers. */
|
|
101
|
+
function sendJson(res, status, body) {
|
|
102
|
+
res.writeHead(status, {
|
|
103
|
+
"content-type": "application/json; charset=utf-8",
|
|
104
|
+
...SECURITY_HEADERS
|
|
105
|
+
});
|
|
106
|
+
res.end(JSON.stringify(body));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
//#endregion
|
|
110
|
+
export { createRequestHandler };
|
|
111
|
+
//# sourceMappingURL=serve.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serve.mjs","names":[],"sources":["../../../../../../../@warlock.js/ai-panoptic/src/dashboard/serve.ts"],"sourcesContent":["import type { IncomingMessage, ServerResponse } from \"node:http\";\nimport type { TraceStoreContract } from \"../store/trace-store.contract\";\nimport { parseQuery } from \"./parse-query\";\nimport { dashboardHtml } from \"./ui.html\";\n\n/** Fully-resolved routing config the request handler closes over. */\nexport type ServeConfig = {\n /** Normalized mount path, always ending in `/` (e.g. `\"/\"`). */\n basePath: string;\n /** Header title baked into the served page. */\n title: string;\n /** Bearer token required on every request when set (S4). */\n authToken?: string;\n /** `Host` header allowlist — defends against DNS-rebinding (S4). */\n allowedHosts: string[];\n};\n\n/**\n * Security response headers added to every dashboard response (S4):\n * block MIME-sniffing, framing, referrer leakage, and lock the page's\n * content sources down to itself (the UI is fully self-contained — no CDN).\n */\nconst SECURITY_HEADERS: Record<string, string> = {\n \"x-content-type-options\": \"nosniff\",\n \"x-frame-options\": \"DENY\",\n \"referrer-policy\": \"no-referrer\",\n \"content-security-policy\":\n \"default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; connect-src 'self'; img-src 'self' data:; base-uri 'none'; form-action 'none'; frame-ancestors 'none'\",\n};\n\n/** Extract the host (no port) from a `Host` header value. */\nfunction hostHeaderName(hostHeader: string | undefined): string | undefined {\n if (!hostHeader) return undefined;\n // IPv6 literal `[::1]:4319` → `[::1]`; otherwise strip `:port`.\n if (hostHeader.startsWith(\"[\")) {\n return hostHeader.slice(0, hostHeader.indexOf(\"]\") + 1);\n }\n const colon = hostHeader.indexOf(\":\");\n return colon === -1 ? hostHeader : hostHeader.slice(0, colon);\n}\n\n/** Constant-ish bearer-token check from `Authorization` header or `?token=`. */\nfunction isAuthorized(req: IncomingMessage, url: URL, token: string): boolean {\n const header = req.headers.authorization;\n if (header && header === `Bearer ${token}`) return true;\n return url.searchParams.get(\"token\") === token;\n}\n\n/**\n * Build the `node:http` request handler for the dashboard over a given\n * trace store. Kept separate from the server lifecycle so it can be unit\n * tested by feeding it a fake `req`/`res` without binding a port.\n *\n * Routes (all under `config.basePath`, all read-only):\n *\n * - `GET api/traces` → `store.query(parseQuery(searchParams))`\n * - `GET api/traces/:id` → `store.get(id)` or `404`\n * - `GET api/aggregate` → `store.aggregate(parseQuery(searchParams))`\n * - `GET {basePath}` → the self-contained HTML page\n *\n * Anything else → `404`. Non-`GET` methods → `405`. The store shapes\n * ({@link Trace}/{@link TraceSpan}) are already JSON-safe, so responses\n * are a plain `JSON.stringify` with no serializer.\n *\n * @example\n * const handler = createRequestHandler(store, { basePath: \"/\", title: \"Panoptic\" });\n * http.createServer(handler).listen(4319, \"127.0.0.1\");\n */\nexport function createRequestHandler(\n store: TraceStoreContract,\n config: ServeConfig,\n): (req: IncomingMessage, res: ServerResponse) => void {\n const base = config.basePath;\n const apiPrefix = `${base}api`;\n\n return function handle(req: IncomingMessage, res: ServerResponse): void {\n if (req.method !== \"GET\") {\n sendJson(res, 405, { error: \"method_not_allowed\" });\n\n return;\n }\n\n // `req.url` is path + query only; a dummy origin lets URL parse it.\n const url = new URL(req.url ?? \"/\", \"http://localhost\");\n const pathname = url.pathname;\n\n // Host-header allowlist — blocks DNS-rebinding attacks that point a\n // hostile domain at this loopback port (S4).\n const host = hostHeaderName(req.headers.host);\n if (!host || !config.allowedHosts.includes(host.toLowerCase())) {\n sendJson(res, 403, { error: \"host_not_allowed\" });\n\n return;\n }\n\n // Bearer-token auth when configured (always required off loopback, S4).\n if (config.authToken && !isAuthorized(req, url, config.authToken)) {\n sendJson(res, 401, { error: \"unauthorized\" });\n\n return;\n }\n\n if (pathname === `${apiPrefix}/traces`) {\n sendJson(res, 200, store.query(parseQuery(url.searchParams)));\n\n return;\n }\n\n if (pathname.startsWith(`${apiPrefix}/traces/`)) {\n const traceId = decodeURIComponent(pathname.slice(`${apiPrefix}/traces/`.length));\n const trace = traceId.length > 0 ? store.get(traceId) : undefined;\n\n if (trace === undefined) {\n sendJson(res, 404, { error: \"trace_not_found\", traceId });\n\n return;\n }\n\n sendJson(res, 200, trace);\n\n return;\n }\n\n if (pathname === `${apiPrefix}/aggregate`) {\n sendJson(res, 200, store.aggregate(parseQuery(url.searchParams)));\n\n return;\n }\n\n if (pathname === base || pathname === base.replace(/\\/$/, \"\")) {\n res.writeHead(200, {\n \"content-type\": \"text/html; charset=utf-8\",\n ...SECURITY_HEADERS,\n });\n res.end(dashboardHtml(base, config.title));\n\n return;\n }\n\n sendJson(res, 404, { error: \"not_found\" });\n };\n}\n\n/** Write a JSON response with the given status code and security headers. */\nfunction sendJson(res: ServerResponse, status: number, body: unknown): void {\n res.writeHead(status, {\n \"content-type\": \"application/json; charset=utf-8\",\n ...SECURITY_HEADERS,\n });\n res.end(JSON.stringify(body));\n}\n"],"mappings":";;;;;;;;;AAsBA,MAAM,mBAA2C;CAC/C,0BAA0B;CAC1B,mBAAmB;CACnB,mBAAmB;CACnB,2BACE;AACJ;;AAGA,SAAS,eAAe,YAAoD;CAC1E,IAAI,CAAC,YAAY,OAAO;CAExB,IAAI,WAAW,WAAW,GAAG,GAC3B,OAAO,WAAW,MAAM,GAAG,WAAW,QAAQ,GAAG,IAAI,CAAC;CAExD,MAAM,QAAQ,WAAW,QAAQ,GAAG;CACpC,OAAO,UAAU,KAAK,aAAa,WAAW,MAAM,GAAG,KAAK;AAC9D;;AAGA,SAAS,aAAa,KAAsB,KAAU,OAAwB;CAC5E,MAAM,SAAS,IAAI,QAAQ;CAC3B,IAAI,UAAU,WAAW,UAAU,SAAS,OAAO;CACnD,OAAO,IAAI,aAAa,IAAI,OAAO,MAAM;AAC3C;;;;;;;;;;;;;;;;;;;;;AAsBA,SAAgB,qBACd,OACA,QACqD;CACrD,MAAM,OAAO,OAAO;CACpB,MAAM,YAAY,GAAG,KAAK;CAE1B,OAAO,SAAS,OAAO,KAAsB,KAA2B;EACtE,IAAI,IAAI,WAAW,OAAO;GACxB,SAAS,KAAK,KAAK,EAAE,OAAO,qBAAqB,CAAC;GAElD;EACF;EAGA,MAAM,MAAM,IAAI,IAAI,IAAI,OAAO,KAAK,kBAAkB;EACtD,MAAM,WAAW,IAAI;EAIrB,MAAM,OAAO,eAAe,IAAI,QAAQ,IAAI;EAC5C,IAAI,CAAC,QAAQ,CAAC,OAAO,aAAa,SAAS,KAAK,YAAY,CAAC,GAAG;GAC9D,SAAS,KAAK,KAAK,EAAE,OAAO,mBAAmB,CAAC;GAEhD;EACF;EAGA,IAAI,OAAO,aAAa,CAAC,aAAa,KAAK,KAAK,OAAO,SAAS,GAAG;GACjE,SAAS,KAAK,KAAK,EAAE,OAAO,eAAe,CAAC;GAE5C;EACF;EAEA,IAAI,aAAa,GAAG,UAAU,UAAU;GACtC,SAAS,KAAK,KAAK,MAAM,MAAM,WAAW,IAAI,YAAY,CAAC,CAAC;GAE5D;EACF;EAEA,IAAI,SAAS,WAAW,GAAG,UAAU,SAAS,GAAG;GAC/C,MAAM,UAAU,mBAAmB,SAAS,MAAM,GAAG,UAAU,UAAU,MAAM,CAAC;GAChF,MAAM,QAAQ,QAAQ,SAAS,IAAI,MAAM,IAAI,OAAO,IAAI;GAExD,IAAI,UAAU,QAAW;IACvB,SAAS,KAAK,KAAK;KAAE,OAAO;KAAmB;IAAQ,CAAC;IAExD;GACF;GAEA,SAAS,KAAK,KAAK,KAAK;GAExB;EACF;EAEA,IAAI,aAAa,GAAG,UAAU,aAAa;GACzC,SAAS,KAAK,KAAK,MAAM,UAAU,WAAW,IAAI,YAAY,CAAC,CAAC;GAEhE;EACF;EAEA,IAAI,aAAa,QAAQ,aAAa,KAAK,QAAQ,OAAO,EAAE,GAAG;GAC7D,IAAI,UAAU,KAAK;IACjB,gBAAgB;IAChB,GAAG;GACL,CAAC;GACD,IAAI,IAAI,cAAc,MAAM,OAAO,KAAK,CAAC;GAEzC;EACF;EAEA,SAAS,KAAK,KAAK,EAAE,OAAO,YAAY,CAAC;CAC3C;AACF;;AAGA,SAAS,SAAS,KAAqB,QAAgB,MAAqB;CAC1E,IAAI,UAAU,QAAQ;EACpB,gBAAgB;EAChB,GAAG;CACL,CAAC;CACD,IAAI,IAAI,KAAK,UAAU,IAAI,CAAC;AAC9B"}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { Trace, TraceSpan } from "../contracts/trace.type.mjs";
|
|
2
|
+
import { ReportStatus, ReportType } from "@warlock.js/ai";
|
|
3
|
+
|
|
4
|
+
//#region ../@warlock.js/ai-panoptic/src/dashboard/trace-filter.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Client-side trace-list filter criteria. The dashboard polls the full
|
|
7
|
+
* trace list every 2s and narrows it in the browser with these — search
|
|
8
|
+
* box, filter chips, and the errors-only header toggle all feed one
|
|
9
|
+
* {@link TraceFilter}. Kept as a pure, framework-free module so the exact
|
|
10
|
+
* matching rules are unit-tested here and the inlined dashboard JS in
|
|
11
|
+
* {@link import("./ui.html").dashboardHtml} mirrors them 1:1.
|
|
12
|
+
*
|
|
13
|
+
* Every field is optional — an absent field is "don't care". An empty
|
|
14
|
+
* filter therefore matches every trace.
|
|
15
|
+
*/
|
|
16
|
+
type TraceFilter = {
|
|
17
|
+
/**
|
|
18
|
+
* Free-text needle matched case-insensitively against a trace's root
|
|
19
|
+
* span name and session id. Whitespace-trimmed; an empty/blank string
|
|
20
|
+
* is treated as absent.
|
|
21
|
+
*/
|
|
22
|
+
text?: string;
|
|
23
|
+
/**
|
|
24
|
+
* Restrict to these terminal statuses (root span status). Empty array
|
|
25
|
+
* is treated as "don't care" (matches every status).
|
|
26
|
+
*/
|
|
27
|
+
statuses?: ReportStatus[];
|
|
28
|
+
/**
|
|
29
|
+
* Restrict to these executable types (root span type). Empty array is
|
|
30
|
+
* treated as "don't care".
|
|
31
|
+
*/
|
|
32
|
+
types?: ReportType[];
|
|
33
|
+
/**
|
|
34
|
+
* Exact session-id chip. When set, only traces whose `sessionId`
|
|
35
|
+
* equals this value match. Distinct from {@link TraceFilter.text},
|
|
36
|
+
* which is a fuzzy substring across name + session.
|
|
37
|
+
*/
|
|
38
|
+
sessionId?: string;
|
|
39
|
+
/**
|
|
40
|
+
* Exact prompt chip — a `name@version` key (see {@link tracePromptKey}).
|
|
41
|
+
* When set, only traces whose root span resolved that exact named prompt
|
|
42
|
+
* version match. Mirrors {@link TraceFilter.sessionId} but over the
|
|
43
|
+
* prompt-version-linkage attributes the collector stamps on agent spans.
|
|
44
|
+
*/
|
|
45
|
+
promptKey?: string;
|
|
46
|
+
/**
|
|
47
|
+
* Errors-only header toggle. When `true`, only `failed` / `cancelled`
|
|
48
|
+
* traces match — independent of (and intersected with) `statuses`.
|
|
49
|
+
*/
|
|
50
|
+
errorsOnly?: boolean;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* The `name@version` prompt key a trace falls under, read from its root
|
|
54
|
+
* span's prompt-version-linkage attributes (`agent.promptName` /
|
|
55
|
+
* `agent.promptVersion`). Returns `undefined` when the run carried no named
|
|
56
|
+
* prompt (raw-string / anonymous / no prompt), so callers can bucket those
|
|
57
|
+
* under a synthetic "no prompt" key or skip them.
|
|
58
|
+
*
|
|
59
|
+
* The version is included so two runs of the same prompt name at different
|
|
60
|
+
* versions are distinct keys — that is the whole point of the linkage: to
|
|
61
|
+
* group / filter by the *exact* prompt revision that produced a run.
|
|
62
|
+
*/
|
|
63
|
+
declare function tracePromptKey(trace: Trace): string | undefined;
|
|
64
|
+
/**
|
|
65
|
+
* Does a single trace pass the given filter? Pure predicate — all
|
|
66
|
+
* conditions are ANDed; an unset field never excludes. Used by
|
|
67
|
+
* {@link filterTraces} and mirrored by the dashboard's inlined JS.
|
|
68
|
+
*/
|
|
69
|
+
declare function matchesFilter(trace: Trace, filter: TraceFilter): boolean;
|
|
70
|
+
/** Narrow a trace list to those matching the filter, order preserved. */
|
|
71
|
+
declare function filterTraces(traces: Trace[], filter: TraceFilter): Trace[];
|
|
72
|
+
/**
|
|
73
|
+
* The grouping key a trace falls under when the list is grouped by
|
|
74
|
+
* session. Traces without a `sessionId` collapse into one synthetic
|
|
75
|
+
* "(no session)" bucket so they remain visible.
|
|
76
|
+
*/
|
|
77
|
+
declare const NO_SESSION_KEY = "(no session)";
|
|
78
|
+
/** One session bucket — its key plus the traces under it, order preserved. */
|
|
79
|
+
type SessionGroup = {
|
|
80
|
+
/** The `sessionId`, or {@link NO_SESSION_KEY} for sessionless traces. */sessionId: string; /** Traces in this group, in the order they appeared in the input. */
|
|
81
|
+
traces: Trace[];
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* Group an (already filtered, newest-first) trace list by session id,
|
|
85
|
+
* preserving first-seen order of both the groups and the traces within
|
|
86
|
+
* each. Sessionless traces bucket under {@link NO_SESSION_KEY}.
|
|
87
|
+
*/
|
|
88
|
+
declare function groupBySession(traces: Trace[]): SessionGroup[];
|
|
89
|
+
/**
|
|
90
|
+
* The grouping key a trace falls under when the list is grouped by prompt
|
|
91
|
+
* version. Runs that resolved no named prompt collapse into one synthetic
|
|
92
|
+
* "(no prompt)" bucket so they remain visible.
|
|
93
|
+
*/
|
|
94
|
+
declare const NO_PROMPT_KEY = "(no prompt)";
|
|
95
|
+
/** One prompt-version bucket — its `name@version` key plus its traces. */
|
|
96
|
+
type PromptGroup = {
|
|
97
|
+
/** The `name@version` key, or {@link NO_PROMPT_KEY} for unlinked traces. */promptKey: string; /** Traces in this group, in the order they appeared in the input. */
|
|
98
|
+
traces: Trace[];
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* Group an (already filtered, newest-first) trace list by prompt version —
|
|
102
|
+
* the `name@version` key {@link tracePromptKey} derives from each run's
|
|
103
|
+
* prompt-version-linkage attributes. Preserves first-seen order of both the
|
|
104
|
+
* groups and the traces within each. Runs with no named prompt bucket under
|
|
105
|
+
* {@link NO_PROMPT_KEY}.
|
|
106
|
+
*
|
|
107
|
+
* The dashboard offers this as a second group-by dimension beside session,
|
|
108
|
+
* so a reviewer can see every run of "support@2" together and compare its
|
|
109
|
+
* cost / failure rate against "support@3".
|
|
110
|
+
*/
|
|
111
|
+
declare function groupByPrompt(traces: Trace[]): PromptGroup[];
|
|
112
|
+
/**
|
|
113
|
+
* The grouping key a trace falls under when the list is grouped by root
|
|
114
|
+
* type. `root.type` is always present, so this is only a defensive
|
|
115
|
+
* fallback for a malformed trace with no typed root.
|
|
116
|
+
*/
|
|
117
|
+
declare const NO_TYPE_KEY = "(no type)";
|
|
118
|
+
/** One root-type bucket — its type discriminator plus its traces. */
|
|
119
|
+
type TypeGroup = {
|
|
120
|
+
/** The root span's `type` (e.g. `agent` / `workflow` / `planner`), or {@link NO_TYPE_KEY}. */type: string; /** Traces in this group, in the order they appeared in the input. */
|
|
121
|
+
traces: Trace[];
|
|
122
|
+
};
|
|
123
|
+
/**
|
|
124
|
+
* Group an (already filtered, newest-first) trace list by root type —
|
|
125
|
+
* the `agent` / `workflow` / `supervisor` / `orchestrator` / `planner` /
|
|
126
|
+
* `tool` / `batch` / `callback` discriminator on each trace's root span.
|
|
127
|
+
* Preserves first-seen order of both the groups and the traces within each.
|
|
128
|
+
*
|
|
129
|
+
* The dashboard offers this as the coarsest group-by dimension beside
|
|
130
|
+
* session and prompt: when a benchmark floods the flat list with hundreds
|
|
131
|
+
* of one primitive (e.g. standalone `agent` runs), this collapses them
|
|
132
|
+
* behind a per-type header so the structural primitives (the lone planner,
|
|
133
|
+
* the team, the supervisors) are one click away instead of a scroll past.
|
|
134
|
+
*/
|
|
135
|
+
declare function groupByType(traces: Trace[]): TypeGroup[];
|
|
136
|
+
/** One row of the per-type aggregate-stats panel. */
|
|
137
|
+
type TypeStat = {
|
|
138
|
+
/** Root type discriminator, or {@link NO_TYPE_KEY}. */type: string; /** Number of traces of this type. */
|
|
139
|
+
count: number; /** Traces whose status is `failed` / `cancelled`. */
|
|
140
|
+
failed: number; /** `failed / count` in `[0, 1]`. */
|
|
141
|
+
failRate: number; /** Median (p50) root duration in ms. */
|
|
142
|
+
p50: number; /** p95 root duration in ms. */
|
|
143
|
+
p95: number; /** Summed `usage.total` tokens across the bucket. */
|
|
144
|
+
tokens: number; /** Summed per-trace USD cost across the bucket. */
|
|
145
|
+
cost: number;
|
|
146
|
+
};
|
|
147
|
+
/**
|
|
148
|
+
* Nearest-rank percentile of `values` at `p` (0–100). Returns 0 for an
|
|
149
|
+
* empty input and sorts a copy so the caller's array is left untouched.
|
|
150
|
+
*/
|
|
151
|
+
declare function percentile(values: number[], p: number): number;
|
|
152
|
+
/**
|
|
153
|
+
* One trace's total USD cost: its explicit root-usage cost when priced,
|
|
154
|
+
* else the rollup of its subtree. Mirrors the dashboard's `traceCost` so the
|
|
155
|
+
* stats panel and the trace rows agree to the cent.
|
|
156
|
+
*/
|
|
157
|
+
declare function traceCost(trace: Trace): number;
|
|
158
|
+
/**
|
|
159
|
+
* Aggregate an (already filtered) trace list into one {@link TypeStat} row
|
|
160
|
+
* per root type — count, failure rate, p50/p95 latency, total tokens, total
|
|
161
|
+
* cost — preserving first-seen type order. Powers the dashboard's per-type
|
|
162
|
+
* stats panel; computed over the same filtered list the trace view shows, so
|
|
163
|
+
* the panel always reflects the active filters.
|
|
164
|
+
*/
|
|
165
|
+
declare function aggregateByType(traces: Trace[]): TypeStat[];
|
|
166
|
+
/**
|
|
167
|
+
* Rollup-aware subtree cost: a node's own cost when it carries one
|
|
168
|
+
* (it already rolls up its trips), otherwise the sum of its children.
|
|
169
|
+
* Avoids double-counting wrapper nodes (workflow/supervisor roots carry
|
|
170
|
+
* tokens but no cost). Mirrors the dashboard's `rollupCost`.
|
|
171
|
+
*/
|
|
172
|
+
declare function rollupCost(span: TraceSpan): number;
|
|
173
|
+
/**
|
|
174
|
+
* Heatmap intensity in `[0, 1]` for a node's `rollupCost` relative to the
|
|
175
|
+
* trace's most-expensive node. The dashboard tints each tree node's left
|
|
176
|
+
* accent by this. `max <= 0` (a free trace) yields `0` for every node so
|
|
177
|
+
* the heatmap simply stays cold rather than dividing by zero.
|
|
178
|
+
*/
|
|
179
|
+
declare function heatIntensity(nodeCost: number, maxCost: number): number;
|
|
180
|
+
/**
|
|
181
|
+
* The largest single-node {@link rollupCost} anywhere in a span tree —
|
|
182
|
+
* the denominator for {@link heatIntensity}. Walks the whole subtree.
|
|
183
|
+
*/
|
|
184
|
+
declare function maxNodeCost(root: TraceSpan): number;
|
|
185
|
+
//#endregion
|
|
186
|
+
export { NO_PROMPT_KEY, NO_SESSION_KEY, NO_TYPE_KEY, PromptGroup, SessionGroup, TraceFilter, TypeGroup, TypeStat, aggregateByType, filterTraces, groupByPrompt, groupBySession, groupByType, heatIntensity, matchesFilter, maxNodeCost, percentile, rollupCost, traceCost, tracePromptKey };
|
|
187
|
+
//# sourceMappingURL=trace-filter.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"trace-filter.d.mts","names":[],"sources":["../../../../../../../@warlock.js/ai-panoptic/src/dashboard/trace-filter.ts"],"mappings":";;;;;;AAcA;;;;;;;;;KAAY,WAAA;EA6BV;;;AAKU;AAyBZ;EArDE,IAAA;;;AAqDyC;AAwB3C;EAxEE,QAAA,GAAW,YAAA;;;;;EAKX,KAAA,GAAQ,UAAU;EAmEwB;;AAAmB;AA2C/D;;EAxGE,SAAA;EAwGmC;;;;;;EAjGnC,SAAA;EAiGoD;;;;EA5FpD,UAAA;AAAA;;;;AAqGyB;AAG3B;;;;;;;iBA/EgB,cAAA,CAAe,KAAY,EAAL,KAAK;AAmF5B;AAQf;;;;AARe,iBA3DC,aAAA,CAAc,KAAA,EAAO,KAAA,EAAO,MAAA,EAAQ,WAAW;;iBA2C/C,YAAA,CAAa,MAAA,EAAQ,KAAA,IAAS,MAAA,EAAQ,WAAA,GAAc,KAAA;;AAwBP;AAyB7D;;;cAxCa,cAAA;AAwCa;AAAA,KArCd,YAAA;EAwCW,yEAtCrB,SAAA,UA0Ca;EAxCb,MAAA,EAAQ,KAAK;AAAA;;;AAwCA;AAcf;;iBA9CgB,cAAA,CAAe,MAAA,EAAQ,KAAA,KAAU,YAAY;;;;;;cAyBhD,aAAA;AA8Cb;AAAA,KA3CY,WAAA;8EAEV,SAAA,UAyCsB;EAvCtB,MAAA,EAAQ,KAAK;AAAA;;;;;;;AA8CA;AAef;;;;iBA/CgB,aAAA,CAAc,MAAA,EAAQ,KAAA,KAAU,WAAW;;;;AA+CJ;AAqBvD;cA3Ca,WAAA;;KAGD,SAAA;EA0CV,8FAxCA,IAAA,UA4CA;EA1CA,MAAA,EAAQ,KAAK;AAAA;;;;;AAoDT;AAON;;;;AAAsD;AAiBtD;;iBA7DgB,WAAA,CAAY,MAAA,EAAQ,KAAA,KAAU,SAAS;;KAqB3C,QAAA;EAoDI,uDAlDd,IAAA;EAEA,KAAA,UAgDsC;EA9CtC,MAAA,UA8CgD;EA5ChD,QAAA,UA4CwD;EA1CxD,GAAA,UAiHwB;EA/GxB,GAAA,UA+G+B;EA7G/B,MAAA,UAiIc;EA/Hd,IAAA;AAAA;;AA+H6D;AAc/D;;iBAtIgB,UAAA,CAAW,MAAA,YAAkB,CAAS;;AAsIX;;;;iBArH3B,SAAA,CAAU,KAAY,EAAL,KAAK;;;;;;;;iBAYtB,eAAA,CAAgB,MAAA,EAAQ,KAAA,KAAU,QAAQ;;;;;;;iBAuE1C,UAAA,CAAW,IAAe,EAAT,SAAS;;;;;;;iBAoB1B,aAAA,CAAc,QAAA,UAAkB,OAAe;;;;;iBAc/C,WAAA,CAAY,IAAe,EAAT,SAAS"}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
//#region ../@warlock.js/ai-panoptic/src/dashboard/trace-filter.ts
|
|
2
|
+
/** Statuses the errors-only toggle keeps. */
|
|
3
|
+
const ERROR_STATUSES = ["failed", "cancelled"];
|
|
4
|
+
/**
|
|
5
|
+
* Span-attribute keys the collector stamps with prompt-version linkage when
|
|
6
|
+
* an agent ran against a *named* `ai.prompts` builder. Read here (not
|
|
7
|
+
* imported) so this pure module stays decoupled from the collector.
|
|
8
|
+
*/
|
|
9
|
+
const PROMPT_NAME_ATTR = "agent.promptName";
|
|
10
|
+
const PROMPT_VERSION_ATTR = "agent.promptVersion";
|
|
11
|
+
/**
|
|
12
|
+
* The `name@version` prompt key a trace falls under, read from its root
|
|
13
|
+
* span's prompt-version-linkage attributes (`agent.promptName` /
|
|
14
|
+
* `agent.promptVersion`). Returns `undefined` when the run carried no named
|
|
15
|
+
* prompt (raw-string / anonymous / no prompt), so callers can bucket those
|
|
16
|
+
* under a synthetic "no prompt" key or skip them.
|
|
17
|
+
*
|
|
18
|
+
* The version is included so two runs of the same prompt name at different
|
|
19
|
+
* versions are distinct keys — that is the whole point of the linkage: to
|
|
20
|
+
* group / filter by the *exact* prompt revision that produced a run.
|
|
21
|
+
*/
|
|
22
|
+
function tracePromptKey(trace) {
|
|
23
|
+
const attributes = trace.root.attributes;
|
|
24
|
+
if (attributes === void 0) return;
|
|
25
|
+
const name = attributes[PROMPT_NAME_ATTR];
|
|
26
|
+
if (typeof name !== "string" || name.length === 0) return;
|
|
27
|
+
const version = attributes[PROMPT_VERSION_ATTR];
|
|
28
|
+
return `${name}@${typeof version === "string" && version.length > 0 ? version : "1"}`;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Does a single trace pass the given filter? Pure predicate — all
|
|
32
|
+
* conditions are ANDed; an unset field never excludes. Used by
|
|
33
|
+
* {@link filterTraces} and mirrored by the dashboard's inlined JS.
|
|
34
|
+
*/
|
|
35
|
+
function matchesFilter(trace, filter) {
|
|
36
|
+
const root = trace.root;
|
|
37
|
+
if (filter.errorsOnly === true && !ERROR_STATUSES.includes(root.status)) return false;
|
|
38
|
+
if (filter.statuses !== void 0 && filter.statuses.length > 0) {
|
|
39
|
+
if (!filter.statuses.includes(root.status)) return false;
|
|
40
|
+
}
|
|
41
|
+
if (filter.types !== void 0 && filter.types.length > 0) {
|
|
42
|
+
if (!filter.types.includes(root.type)) return false;
|
|
43
|
+
}
|
|
44
|
+
if (filter.sessionId !== void 0 && filter.sessionId.length > 0) {
|
|
45
|
+
if (trace.sessionId !== filter.sessionId) return false;
|
|
46
|
+
}
|
|
47
|
+
if (filter.promptKey !== void 0 && filter.promptKey.length > 0) {
|
|
48
|
+
if (tracePromptKey(trace) !== filter.promptKey) return false;
|
|
49
|
+
}
|
|
50
|
+
const text = filter.text?.trim().toLowerCase();
|
|
51
|
+
if (text !== void 0 && text.length > 0) {
|
|
52
|
+
if (!`${root.name} ${trace.sessionId ?? ""}`.toLowerCase().includes(text)) return false;
|
|
53
|
+
}
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
/** Narrow a trace list to those matching the filter, order preserved. */
|
|
57
|
+
function filterTraces(traces, filter) {
|
|
58
|
+
return traces.filter((trace) => matchesFilter(trace, filter));
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The grouping key a trace falls under when the list is grouped by
|
|
62
|
+
* session. Traces without a `sessionId` collapse into one synthetic
|
|
63
|
+
* "(no session)" bucket so they remain visible.
|
|
64
|
+
*/
|
|
65
|
+
const NO_SESSION_KEY = "(no session)";
|
|
66
|
+
/**
|
|
67
|
+
* Group an (already filtered, newest-first) trace list by session id,
|
|
68
|
+
* preserving first-seen order of both the groups and the traces within
|
|
69
|
+
* each. Sessionless traces bucket under {@link NO_SESSION_KEY}.
|
|
70
|
+
*/
|
|
71
|
+
function groupBySession(traces) {
|
|
72
|
+
const order = [];
|
|
73
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
74
|
+
for (const trace of traces) {
|
|
75
|
+
const key = trace.sessionId ?? "(no session)";
|
|
76
|
+
let bucket = byKey.get(key);
|
|
77
|
+
if (bucket === void 0) {
|
|
78
|
+
bucket = [];
|
|
79
|
+
byKey.set(key, bucket);
|
|
80
|
+
order.push(key);
|
|
81
|
+
}
|
|
82
|
+
bucket.push(trace);
|
|
83
|
+
}
|
|
84
|
+
return order.map((sessionId) => ({
|
|
85
|
+
sessionId,
|
|
86
|
+
traces: byKey.get(sessionId) ?? []
|
|
87
|
+
}));
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* The grouping key a trace falls under when the list is grouped by prompt
|
|
91
|
+
* version. Runs that resolved no named prompt collapse into one synthetic
|
|
92
|
+
* "(no prompt)" bucket so they remain visible.
|
|
93
|
+
*/
|
|
94
|
+
const NO_PROMPT_KEY = "(no prompt)";
|
|
95
|
+
/**
|
|
96
|
+
* Group an (already filtered, newest-first) trace list by prompt version —
|
|
97
|
+
* the `name@version` key {@link tracePromptKey} derives from each run's
|
|
98
|
+
* prompt-version-linkage attributes. Preserves first-seen order of both the
|
|
99
|
+
* groups and the traces within each. Runs with no named prompt bucket under
|
|
100
|
+
* {@link NO_PROMPT_KEY}.
|
|
101
|
+
*
|
|
102
|
+
* The dashboard offers this as a second group-by dimension beside session,
|
|
103
|
+
* so a reviewer can see every run of "support@2" together and compare its
|
|
104
|
+
* cost / failure rate against "support@3".
|
|
105
|
+
*/
|
|
106
|
+
function groupByPrompt(traces) {
|
|
107
|
+
const order = [];
|
|
108
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
109
|
+
for (const trace of traces) {
|
|
110
|
+
const key = tracePromptKey(trace) ?? "(no prompt)";
|
|
111
|
+
let bucket = byKey.get(key);
|
|
112
|
+
if (bucket === void 0) {
|
|
113
|
+
bucket = [];
|
|
114
|
+
byKey.set(key, bucket);
|
|
115
|
+
order.push(key);
|
|
116
|
+
}
|
|
117
|
+
bucket.push(trace);
|
|
118
|
+
}
|
|
119
|
+
return order.map((promptKey) => ({
|
|
120
|
+
promptKey,
|
|
121
|
+
traces: byKey.get(promptKey) ?? []
|
|
122
|
+
}));
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* The grouping key a trace falls under when the list is grouped by root
|
|
126
|
+
* type. `root.type` is always present, so this is only a defensive
|
|
127
|
+
* fallback for a malformed trace with no typed root.
|
|
128
|
+
*/
|
|
129
|
+
const NO_TYPE_KEY = "(no type)";
|
|
130
|
+
/**
|
|
131
|
+
* Group an (already filtered, newest-first) trace list by root type —
|
|
132
|
+
* the `agent` / `workflow` / `supervisor` / `orchestrator` / `planner` /
|
|
133
|
+
* `tool` / `batch` / `callback` discriminator on each trace's root span.
|
|
134
|
+
* Preserves first-seen order of both the groups and the traces within each.
|
|
135
|
+
*
|
|
136
|
+
* The dashboard offers this as the coarsest group-by dimension beside
|
|
137
|
+
* session and prompt: when a benchmark floods the flat list with hundreds
|
|
138
|
+
* of one primitive (e.g. standalone `agent` runs), this collapses them
|
|
139
|
+
* behind a per-type header so the structural primitives (the lone planner,
|
|
140
|
+
* the team, the supervisors) are one click away instead of a scroll past.
|
|
141
|
+
*/
|
|
142
|
+
function groupByType(traces) {
|
|
143
|
+
const order = [];
|
|
144
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
145
|
+
for (const trace of traces) {
|
|
146
|
+
const key = trace.root.type || "(no type)";
|
|
147
|
+
let bucket = byKey.get(key);
|
|
148
|
+
if (bucket === void 0) {
|
|
149
|
+
bucket = [];
|
|
150
|
+
byKey.set(key, bucket);
|
|
151
|
+
order.push(key);
|
|
152
|
+
}
|
|
153
|
+
bucket.push(trace);
|
|
154
|
+
}
|
|
155
|
+
return order.map((type) => ({
|
|
156
|
+
type,
|
|
157
|
+
traces: byKey.get(type) ?? []
|
|
158
|
+
}));
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Nearest-rank percentile of `values` at `p` (0–100). Returns 0 for an
|
|
162
|
+
* empty input and sorts a copy so the caller's array is left untouched.
|
|
163
|
+
*/
|
|
164
|
+
function percentile(values, p) {
|
|
165
|
+
if (values.length === 0) return 0;
|
|
166
|
+
const sorted = values.slice().sort((a, b) => a - b);
|
|
167
|
+
const rank = Math.ceil(p / 100 * sorted.length) - 1;
|
|
168
|
+
return sorted[Math.min(Math.max(rank, 0), sorted.length - 1)];
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* One trace's total USD cost: its explicit root-usage cost when priced,
|
|
172
|
+
* else the rollup of its subtree. Mirrors the dashboard's `traceCost` so the
|
|
173
|
+
* stats panel and the trace rows agree to the cent.
|
|
174
|
+
*/
|
|
175
|
+
function traceCost(trace) {
|
|
176
|
+
const own = costSumObj(trace.usage.cost);
|
|
177
|
+
return own > 0 ? own : rollupCost(trace.root);
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Aggregate an (already filtered) trace list into one {@link TypeStat} row
|
|
181
|
+
* per root type — count, failure rate, p50/p95 latency, total tokens, total
|
|
182
|
+
* cost — preserving first-seen type order. Powers the dashboard's per-type
|
|
183
|
+
* stats panel; computed over the same filtered list the trace view shows, so
|
|
184
|
+
* the panel always reflects the active filters.
|
|
185
|
+
*/
|
|
186
|
+
function aggregateByType(traces) {
|
|
187
|
+
const order = [];
|
|
188
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
189
|
+
for (const trace of traces) {
|
|
190
|
+
const key = trace.root.type || "(no type)";
|
|
191
|
+
let bucket = byKey.get(key);
|
|
192
|
+
if (bucket === void 0) {
|
|
193
|
+
bucket = [];
|
|
194
|
+
byKey.set(key, bucket);
|
|
195
|
+
order.push(key);
|
|
196
|
+
}
|
|
197
|
+
bucket.push(trace);
|
|
198
|
+
}
|
|
199
|
+
return order.map((type) => {
|
|
200
|
+
const bucket = byKey.get(type) ?? [];
|
|
201
|
+
const durations = bucket.map((trace) => trace.duration);
|
|
202
|
+
let failed = 0;
|
|
203
|
+
let tokens = 0;
|
|
204
|
+
let cost = 0;
|
|
205
|
+
for (const trace of bucket) {
|
|
206
|
+
if (ERROR_STATUSES.includes(trace.root.status)) failed += 1;
|
|
207
|
+
tokens += trace.usage.total ?? 0;
|
|
208
|
+
cost += traceCost(trace);
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
type,
|
|
212
|
+
count: bucket.length,
|
|
213
|
+
failed,
|
|
214
|
+
failRate: bucket.length > 0 ? failed / bucket.length : 0,
|
|
215
|
+
p50: percentile(durations, 50),
|
|
216
|
+
p95: percentile(durations, 95),
|
|
217
|
+
tokens,
|
|
218
|
+
cost
|
|
219
|
+
};
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Sum every priced lane of a usage cost object into one USD number.
|
|
224
|
+
* Mirrors the dashboard's `costSumObj` so heatmap intensities computed
|
|
225
|
+
* here match what the page renders.
|
|
226
|
+
*/
|
|
227
|
+
function costSumObj(cost) {
|
|
228
|
+
if (cost === void 0) return 0;
|
|
229
|
+
return (cost.input ?? 0) + (cost.output ?? 0) + (cost.cachedInput ?? 0) + (cost.cachedOutput ?? 0) + (cost.reasoning ?? 0);
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Rollup-aware subtree cost: a node's own cost when it carries one
|
|
233
|
+
* (it already rolls up its trips), otherwise the sum of its children.
|
|
234
|
+
* Avoids double-counting wrapper nodes (workflow/supervisor roots carry
|
|
235
|
+
* tokens but no cost). Mirrors the dashboard's `rollupCost`.
|
|
236
|
+
*/
|
|
237
|
+
function rollupCost(span) {
|
|
238
|
+
const own = costSumObj(span.usage.cost);
|
|
239
|
+
if (own > 0) return own;
|
|
240
|
+
let sum = 0;
|
|
241
|
+
for (const child of span.children) sum += rollupCost(child);
|
|
242
|
+
return sum;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Heatmap intensity in `[0, 1]` for a node's `rollupCost` relative to the
|
|
246
|
+
* trace's most-expensive node. The dashboard tints each tree node's left
|
|
247
|
+
* accent by this. `max <= 0` (a free trace) yields `0` for every node so
|
|
248
|
+
* the heatmap simply stays cold rather than dividing by zero.
|
|
249
|
+
*/
|
|
250
|
+
function heatIntensity(nodeCost, maxCost) {
|
|
251
|
+
if (maxCost <= 0 || nodeCost <= 0) return 0;
|
|
252
|
+
const ratio = nodeCost / maxCost;
|
|
253
|
+
return ratio > 1 ? 1 : ratio;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* The largest single-node {@link rollupCost} anywhere in a span tree —
|
|
257
|
+
* the denominator for {@link heatIntensity}. Walks the whole subtree.
|
|
258
|
+
*/
|
|
259
|
+
function maxNodeCost(root) {
|
|
260
|
+
let max = rollupCost(root);
|
|
261
|
+
for (const child of root.children) {
|
|
262
|
+
const childMax = maxNodeCost(child);
|
|
263
|
+
if (childMax > max) max = childMax;
|
|
264
|
+
}
|
|
265
|
+
return max;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
//#endregion
|
|
269
|
+
export { NO_PROMPT_KEY, NO_SESSION_KEY, NO_TYPE_KEY, aggregateByType, filterTraces, groupByPrompt, groupBySession, groupByType, heatIntensity, matchesFilter, maxNodeCost, percentile, rollupCost, traceCost, tracePromptKey };
|
|
270
|
+
//# sourceMappingURL=trace-filter.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"trace-filter.mjs","names":[],"sources":["../../../../../../../@warlock.js/ai-panoptic/src/dashboard/trace-filter.ts"],"sourcesContent":["import type { ReportStatus, ReportType } from \"@warlock.js/ai\";\nimport type { Trace, TraceSpan } from \"../contracts/trace.type\";\n\n/**\n * Client-side trace-list filter criteria. The dashboard polls the full\n * trace list every 2s and narrows it in the browser with these — search\n * box, filter chips, and the errors-only header toggle all feed one\n * {@link TraceFilter}. Kept as a pure, framework-free module so the exact\n * matching rules are unit-tested here and the inlined dashboard JS in\n * {@link import(\"./ui.html\").dashboardHtml} mirrors them 1:1.\n *\n * Every field is optional — an absent field is \"don't care\". An empty\n * filter therefore matches every trace.\n */\nexport type TraceFilter = {\n /**\n * Free-text needle matched case-insensitively against a trace's root\n * span name and session id. Whitespace-trimmed; an empty/blank string\n * is treated as absent.\n */\n text?: string;\n /**\n * Restrict to these terminal statuses (root span status). Empty array\n * is treated as \"don't care\" (matches every status).\n */\n statuses?: ReportStatus[];\n /**\n * Restrict to these executable types (root span type). Empty array is\n * treated as \"don't care\".\n */\n types?: ReportType[];\n /**\n * Exact session-id chip. When set, only traces whose `sessionId`\n * equals this value match. Distinct from {@link TraceFilter.text},\n * which is a fuzzy substring across name + session.\n */\n sessionId?: string;\n /**\n * Exact prompt chip — a `name@version` key (see {@link tracePromptKey}).\n * When set, only traces whose root span resolved that exact named prompt\n * version match. Mirrors {@link TraceFilter.sessionId} but over the\n * prompt-version-linkage attributes the collector stamps on agent spans.\n */\n promptKey?: string;\n /**\n * Errors-only header toggle. When `true`, only `failed` / `cancelled`\n * traces match — independent of (and intersected with) `statuses`.\n */\n errorsOnly?: boolean;\n};\n\n/** Statuses the errors-only toggle keeps. */\nconst ERROR_STATUSES: readonly ReportStatus[] = [\"failed\", \"cancelled\"] as const;\n\n/**\n * Span-attribute keys the collector stamps with prompt-version linkage when\n * an agent ran against a *named* `ai.prompts` builder. Read here (not\n * imported) so this pure module stays decoupled from the collector.\n */\nconst PROMPT_NAME_ATTR = \"agent.promptName\";\nconst PROMPT_VERSION_ATTR = \"agent.promptVersion\";\n\n/**\n * The `name@version` prompt key a trace falls under, read from its root\n * span's prompt-version-linkage attributes (`agent.promptName` /\n * `agent.promptVersion`). Returns `undefined` when the run carried no named\n * prompt (raw-string / anonymous / no prompt), so callers can bucket those\n * under a synthetic \"no prompt\" key or skip them.\n *\n * The version is included so two runs of the same prompt name at different\n * versions are distinct keys — that is the whole point of the linkage: to\n * group / filter by the *exact* prompt revision that produced a run.\n */\nexport function tracePromptKey(trace: Trace): string | undefined {\n const attributes = trace.root.attributes;\n\n if (attributes === undefined) {\n return undefined;\n }\n\n const name = attributes[PROMPT_NAME_ATTR];\n\n if (typeof name !== \"string\" || name.length === 0) {\n return undefined;\n }\n\n const version = attributes[PROMPT_VERSION_ATTR];\n const versionLabel = typeof version === \"string\" && version.length > 0 ? version : \"1\";\n\n return `${name}@${versionLabel}`;\n}\n\n/**\n * Does a single trace pass the given filter? Pure predicate — all\n * conditions are ANDed; an unset field never excludes. Used by\n * {@link filterTraces} and mirrored by the dashboard's inlined JS.\n */\nexport function matchesFilter(trace: Trace, filter: TraceFilter): boolean {\n const root = trace.root;\n\n if (filter.errorsOnly === true && !ERROR_STATUSES.includes(root.status)) {\n return false;\n }\n\n if (filter.statuses !== undefined && filter.statuses.length > 0) {\n if (!filter.statuses.includes(root.status)) {\n return false;\n }\n }\n\n if (filter.types !== undefined && filter.types.length > 0) {\n if (!filter.types.includes(root.type)) {\n return false;\n }\n }\n\n if (filter.sessionId !== undefined && filter.sessionId.length > 0) {\n if (trace.sessionId !== filter.sessionId) {\n return false;\n }\n }\n\n if (filter.promptKey !== undefined && filter.promptKey.length > 0) {\n if (tracePromptKey(trace) !== filter.promptKey) {\n return false;\n }\n }\n\n const text = filter.text?.trim().toLowerCase();\n if (text !== undefined && text.length > 0) {\n const haystack = `${root.name} ${trace.sessionId ?? \"\"}`.toLowerCase();\n if (!haystack.includes(text)) {\n return false;\n }\n }\n\n return true;\n}\n\n/** Narrow a trace list to those matching the filter, order preserved. */\nexport function filterTraces(traces: Trace[], filter: TraceFilter): Trace[] {\n return traces.filter((trace) => matchesFilter(trace, filter));\n}\n\n/**\n * The grouping key a trace falls under when the list is grouped by\n * session. Traces without a `sessionId` collapse into one synthetic\n * \"(no session)\" bucket so they remain visible.\n */\nexport const NO_SESSION_KEY = \"(no session)\";\n\n/** One session bucket — its key plus the traces under it, order preserved. */\nexport type SessionGroup = {\n /** The `sessionId`, or {@link NO_SESSION_KEY} for sessionless traces. */\n sessionId: string;\n /** Traces in this group, in the order they appeared in the input. */\n traces: Trace[];\n};\n\n/**\n * Group an (already filtered, newest-first) trace list by session id,\n * preserving first-seen order of both the groups and the traces within\n * each. Sessionless traces bucket under {@link NO_SESSION_KEY}.\n */\nexport function groupBySession(traces: Trace[]): SessionGroup[] {\n const order: string[] = [];\n const byKey = new Map<string, Trace[]>();\n\n for (const trace of traces) {\n const key = trace.sessionId ?? NO_SESSION_KEY;\n let bucket = byKey.get(key);\n\n if (bucket === undefined) {\n bucket = [];\n byKey.set(key, bucket);\n order.push(key);\n }\n\n bucket.push(trace);\n }\n\n return order.map((sessionId) => ({ sessionId, traces: byKey.get(sessionId) ?? [] }));\n}\n\n/**\n * The grouping key a trace falls under when the list is grouped by prompt\n * version. Runs that resolved no named prompt collapse into one synthetic\n * \"(no prompt)\" bucket so they remain visible.\n */\nexport const NO_PROMPT_KEY = \"(no prompt)\";\n\n/** One prompt-version bucket — its `name@version` key plus its traces. */\nexport type PromptGroup = {\n /** The `name@version` key, or {@link NO_PROMPT_KEY} for unlinked traces. */\n promptKey: string;\n /** Traces in this group, in the order they appeared in the input. */\n traces: Trace[];\n};\n\n/**\n * Group an (already filtered, newest-first) trace list by prompt version —\n * the `name@version` key {@link tracePromptKey} derives from each run's\n * prompt-version-linkage attributes. Preserves first-seen order of both the\n * groups and the traces within each. Runs with no named prompt bucket under\n * {@link NO_PROMPT_KEY}.\n *\n * The dashboard offers this as a second group-by dimension beside session,\n * so a reviewer can see every run of \"support@2\" together and compare its\n * cost / failure rate against \"support@3\".\n */\nexport function groupByPrompt(traces: Trace[]): PromptGroup[] {\n const order: string[] = [];\n const byKey = new Map<string, Trace[]>();\n\n for (const trace of traces) {\n const key = tracePromptKey(trace) ?? NO_PROMPT_KEY;\n let bucket = byKey.get(key);\n\n if (bucket === undefined) {\n bucket = [];\n byKey.set(key, bucket);\n order.push(key);\n }\n\n bucket.push(trace);\n }\n\n return order.map((promptKey) => ({ promptKey, traces: byKey.get(promptKey) ?? [] }));\n}\n\n/**\n * The grouping key a trace falls under when the list is grouped by root\n * type. `root.type` is always present, so this is only a defensive\n * fallback for a malformed trace with no typed root.\n */\nexport const NO_TYPE_KEY = \"(no type)\";\n\n/** One root-type bucket — its type discriminator plus its traces. */\nexport type TypeGroup = {\n /** The root span's `type` (e.g. `agent` / `workflow` / `planner`), or {@link NO_TYPE_KEY}. */\n type: string;\n /** Traces in this group, in the order they appeared in the input. */\n traces: Trace[];\n};\n\n/**\n * Group an (already filtered, newest-first) trace list by root type —\n * the `agent` / `workflow` / `supervisor` / `orchestrator` / `planner` /\n * `tool` / `batch` / `callback` discriminator on each trace's root span.\n * Preserves first-seen order of both the groups and the traces within each.\n *\n * The dashboard offers this as the coarsest group-by dimension beside\n * session and prompt: when a benchmark floods the flat list with hundreds\n * of one primitive (e.g. standalone `agent` runs), this collapses them\n * behind a per-type header so the structural primitives (the lone planner,\n * the team, the supervisors) are one click away instead of a scroll past.\n */\nexport function groupByType(traces: Trace[]): TypeGroup[] {\n const order: string[] = [];\n const byKey = new Map<string, Trace[]>();\n\n for (const trace of traces) {\n const key = trace.root.type || NO_TYPE_KEY;\n let bucket = byKey.get(key);\n\n if (bucket === undefined) {\n bucket = [];\n byKey.set(key, bucket);\n order.push(key);\n }\n\n bucket.push(trace);\n }\n\n return order.map((type) => ({ type, traces: byKey.get(type) ?? [] }));\n}\n\n/** One row of the per-type aggregate-stats panel. */\nexport type TypeStat = {\n /** Root type discriminator, or {@link NO_TYPE_KEY}. */\n type: string;\n /** Number of traces of this type. */\n count: number;\n /** Traces whose status is `failed` / `cancelled`. */\n failed: number;\n /** `failed / count` in `[0, 1]`. */\n failRate: number;\n /** Median (p50) root duration in ms. */\n p50: number;\n /** p95 root duration in ms. */\n p95: number;\n /** Summed `usage.total` tokens across the bucket. */\n tokens: number;\n /** Summed per-trace USD cost across the bucket. */\n cost: number;\n};\n\n/**\n * Nearest-rank percentile of `values` at `p` (0–100). Returns 0 for an\n * empty input and sorts a copy so the caller's array is left untouched.\n */\nexport function percentile(values: number[], p: number): number {\n if (values.length === 0) {\n return 0;\n }\n\n const sorted = values.slice().sort((a, b) => a - b);\n const rank = Math.ceil((p / 100) * sorted.length) - 1;\n const index = Math.min(Math.max(rank, 0), sorted.length - 1);\n\n return sorted[index];\n}\n\n/**\n * One trace's total USD cost: its explicit root-usage cost when priced,\n * else the rollup of its subtree. Mirrors the dashboard's `traceCost` so the\n * stats panel and the trace rows agree to the cent.\n */\nexport function traceCost(trace: Trace): number {\n const own = costSumObj(trace.usage.cost);\n return own > 0 ? own : rollupCost(trace.root);\n}\n\n/**\n * Aggregate an (already filtered) trace list into one {@link TypeStat} row\n * per root type — count, failure rate, p50/p95 latency, total tokens, total\n * cost — preserving first-seen type order. Powers the dashboard's per-type\n * stats panel; computed over the same filtered list the trace view shows, so\n * the panel always reflects the active filters.\n */\nexport function aggregateByType(traces: Trace[]): TypeStat[] {\n const order: string[] = [];\n const byKey = new Map<string, Trace[]>();\n\n for (const trace of traces) {\n const key = trace.root.type || NO_TYPE_KEY;\n let bucket = byKey.get(key);\n\n if (bucket === undefined) {\n bucket = [];\n byKey.set(key, bucket);\n order.push(key);\n }\n\n bucket.push(trace);\n }\n\n return order.map((type) => {\n const bucket = byKey.get(type) ?? [];\n const durations = bucket.map((trace) => trace.duration);\n let failed = 0;\n let tokens = 0;\n let cost = 0;\n\n for (const trace of bucket) {\n if (ERROR_STATUSES.includes(trace.root.status)) {\n failed += 1;\n }\n\n tokens += trace.usage.total ?? 0;\n cost += traceCost(trace);\n }\n\n return {\n type,\n count: bucket.length,\n failed,\n failRate: bucket.length > 0 ? failed / bucket.length : 0,\n p50: percentile(durations, 50),\n p95: percentile(durations, 95),\n tokens,\n cost,\n };\n });\n}\n\n/**\n * Sum every priced lane of a usage cost object into one USD number.\n * Mirrors the dashboard's `costSumObj` so heatmap intensities computed\n * here match what the page renders.\n */\nfunction costSumObj(cost: TraceSpan[\"usage\"][\"cost\"]): number {\n if (cost === undefined) {\n return 0;\n }\n\n return (\n (cost.input ?? 0) +\n (cost.output ?? 0) +\n (cost.cachedInput ?? 0) +\n (cost.cachedOutput ?? 0) +\n (cost.reasoning ?? 0)\n );\n}\n\n/**\n * Rollup-aware subtree cost: a node's own cost when it carries one\n * (it already rolls up its trips), otherwise the sum of its children.\n * Avoids double-counting wrapper nodes (workflow/supervisor roots carry\n * tokens but no cost). Mirrors the dashboard's `rollupCost`.\n */\nexport function rollupCost(span: TraceSpan): number {\n const own = costSumObj(span.usage.cost);\n if (own > 0) {\n return own;\n }\n\n let sum = 0;\n for (const child of span.children) {\n sum += rollupCost(child);\n }\n\n return sum;\n}\n\n/**\n * Heatmap intensity in `[0, 1]` for a node's `rollupCost` relative to the\n * trace's most-expensive node. The dashboard tints each tree node's left\n * accent by this. `max <= 0` (a free trace) yields `0` for every node so\n * the heatmap simply stays cold rather than dividing by zero.\n */\nexport function heatIntensity(nodeCost: number, maxCost: number): number {\n if (maxCost <= 0 || nodeCost <= 0) {\n return 0;\n }\n\n const ratio = nodeCost / maxCost;\n\n return ratio > 1 ? 1 : ratio;\n}\n\n/**\n * The largest single-node {@link rollupCost} anywhere in a span tree —\n * the denominator for {@link heatIntensity}. Walks the whole subtree.\n */\nexport function maxNodeCost(root: TraceSpan): number {\n let max = rollupCost(root);\n\n for (const child of root.children) {\n const childMax = maxNodeCost(child);\n if (childMax > max) {\n max = childMax;\n }\n }\n\n return max;\n}\n"],"mappings":"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"}
|